diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1857519..3a324f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,9 @@ jobs: with: persist-credentials: false - name: Publication inputs - run: python3 scripts/check_publication.py + run: | + python3 scripts/check_publication.py + python3 scripts/check_localizations.py - name: Publication check regressions run: python3 -m unittest discover -s Tests/PublicationChecks - name: Swift tests @@ -45,6 +47,8 @@ jobs: with: persist-credentials: false - name: Publication inputs - run: python3 scripts/check_publication.py + run: | + python3 scripts/check_publication.py + python3 scripts/check_localizations.py - name: Publication check regressions run: python3 -m unittest discover -s Tests/PublicationChecks diff --git a/AGENTS.md b/AGENTS.md index f08d72b..8ed8991 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,9 @@ These principles apply in every session, including work based on research, commu - Every added feature requires human review and manual validation by the maintainer (the user). Automated tests, successful compilation and agent reviews support that decision; they do not replace it. - Complete the implementation and appropriate automated checks, then provide a concrete build and focused manual checks covering the intended behavior, relevant failure triggers and regressions. Until the user confirms the outcome, report the feature as **awaiting human validation**, not complete or release-ready. Preserve that status in handoffs so another session cannot assume approval. - Finish and evaluate the current feature before expanding its scope or starting unrelated additions. Any research backlog remains a set of proposals until deliberately selected for work. +- For each substantial milestone, compare plausible approaches and record the chosen responsibilities, data contracts, tradeoffs and acceptance checks before implementation. Keep abstractions proportionate to verified needs; do not grow a central coordinator or invent a universal framework merely to finish a feature quickly. +- Diagnose failures before patching them and fix the cause at the responsible layer. A workaround must have an explicit reason, limits and regression evidence; do not rely on incidental redraws, state resets or swallowed errors to make a flow appear to work. +- Verify integration assumptions against current primary documentation and focused probes. Distinguish confirmed behavior from inference, preserve useful research/decision evidence, and bring consequential product or architecture choices back to the maintainer. Never claim maximum performance, scalability or accuracy without appropriate evidence. ## Upstream engine upgrade diff --git a/CHANGELOG.md b/CHANGELOG.md index c93880f..ceccdbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,23 @@ User-visible changes to NX Library are recorded here. Entries are grouped by release, with the newest first. Engine changes also have their own [upgrade notes](docs/upstream-upgrades/README.md). +## 0.1.4.3 — 2026-11-12 + +### Added + +- **Six interface languages and independent region preferences.** English, Italian, French, German, Spanish and Brazilian Portuguese, selected from the Mac automatically or in **Settings → Start-up → Language & region**. Dates, numbers and sizes use the selected region. Language is selected directly from a list; Regional formats explains its scope and previews dates, numbers and sizes. Game language stays separate. Known catalog genres translate locally without changing saved metadata. Supported Nlib descriptions are requested in the interface language, with cached descriptions reused across regions sharing a language and an explicitly labelled fallback when unavailable. Language changes preserve the visible page and reuse fresh metadata instead of repeating English searches; Switch 2 IDs are included in localized lookups. Translation resources and placeholders are checked in packaged apps. +- **Storage by game.** Settings → Storage lists each game's app, build cache and logs, with the largest caches first, including hidden games. **Delete cache** removes only that game's build cache after confirmation, preserving the app, original game files, saves and logs. A shared cache is explained before deletion; unavailable folders are labelled rather than shown as empty. Cleanup runs without blocking the interface and waits until builds finish. File sizes include hidden files, shared paths count once in totals, and free space refers to the selected Game apps volume. + +### Fixed + +- **Adding NSP, XCI, NSZ and XCZ files works with the newest engine.** With **Newest engine** at a recent nxnative, every package add stopped with an error about `configure_dotnet`, a function the engine removed. NX Library now reads packages with old and new engines alike. +- **Cancelling or quitting during a build stops the engine's tools.** The engine now runs each tool in its own session. NX Library gives it time to stop them, then stops anything left, so quitting mid-build no longer leaves a shader build or `xcodebuild` running in the background. +- **Saves are not replaced while a cloud download is unfinished.** If the engine's download of a cloud save was cut short, the next game start puts this Mac's save back. Import and restore wait for that start and say so. + +### Changed + +- **Updated nxnative to `8a5c5e7e`.** Building no longer needs .NET, so Setup has no .NET card and its downloader is gone. Upstream reports fixes for rendering, shaders, textures and memory across many games, including Ocarina of Time terrain and Star Fox scene changes, steadier movies and ProMotion full screen at 60 fps. Red Dead Redemption, TUNIC, both Ori games, Metroid Prime 4: Beyond, The Binding of Isaac: Rebirth, PRAGMATA, Diablo III, Diablo II: Resurrected and Nintendo Switch Sports Resort are newly playable. The Metal compiler remains **32023.922**. Every game translates its shaders again at its next build, and installed game apps keep their engine until rebuilt. NX Library starts games without the engine's own settings window, so the engine's new cloud-save question does not appear yet. See the [engine review](docs/upstream-upgrades/2026-11-12-79741184-8a5c5e7e-review.md). + ## 0.1.4.2 — 2026-11-09 ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff70119..ab190b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,3 +33,9 @@ Record user-visible changes under **Unreleased** in [CHANGELOG.md](CHANGELOG.md) Keep the description focused on the resulting behaviour. Move entries into a dated version section when that release is actually published; unreleased work must not appear as an already available release. + +## Interface translations + +The shared source is `Sources/NXLibraryCore/Resources/Localizable.xcstrings`. It includes en, it, fr, de, es and pt-BR. Keep keys stable, preserve every typed placeholder, provide quantity variants, and leave product names, technical identifiers and user-provided content unchanged. Translation-only PRs should identify their language and whether a fluent speaker reviewed them. + +Run `python3 scripts/check_localizations.py` and the publication regressions after editing the catalog. A development build also checks its compiled resources. The initial translations await maintainer validation; no external translation-hosting service is required. diff --git a/Package.swift b/Package.swift index 607b561..48a4a9d 100644 --- a/Package.swift +++ b/Package.swift @@ -3,13 +3,14 @@ import PackageDescription let package = Package( name: "NXLibrary", + defaultLocalization: "en", platforms: [.macOS(.v13)], products: [ .library(name: "NXLibraryCore", targets: ["NXLibraryCore"]), .executable(name: "NXLibrary", targets: ["NXLibrary"]), ], targets: [ - .target(name: "NXLibraryCore", path: "Sources/NXLibraryCore"), + .target(name: "NXLibraryCore", path: "Sources/NXLibraryCore", resources: [.process("Resources")]), .executableTarget( name: "NXLibrary", dependencies: ["NXLibraryCore"], diff --git a/README.md b/README.md index 3221ba6..3b9dcf9 100644 --- a/README.md +++ b/README.md @@ -66,11 +66,11 @@ Game files, keys and firmware are not provided. See [Legal and third-party notic ### Project status -**0.1.4.2 alpha.** Download the packaged app from [Releases](https://github.com/Bium23/NxLibrary/releases/latest), or [build it from source](#building-from-source). +**0.1.4.3 alpha.** Download the packaged app from [Releases](https://github.com/Bium23/NxLibrary/releases/latest), or [build it from source](#building-from-source). Automated checks cover the application code, publication inputs, compilation and development-app packaging. They do not certify game compatibility. Changes are recorded in the [changelog](CHANGELOG.md), and the current checks are available under [Actions](https://github.com/Bium23/NxLibrary/actions/workflows/ci.yml). -The engine is pinned to `79741184`, with Ocarina of Time startup/memory/rendering improvements, Switch 2 Bluetooth controller support in rebuilt games, updated frame-rate profiles and cloud-save race protection, using Xcode 27.2 / Metal 32023.922. See the [engine upgrade notes](docs/upstream-upgrades/README.md). +The engine is pinned to `8a5c5e7e`: builds no longer need .NET, rendering and shader fixes across many games, newly playable titles such as Red Dead Redemption, TUNIC, Metroid Prime 4: Beyond and PRAGMATA, and safer stopping of builds and cloud-save downloads, using Xcode 27.2 / Metal 32023.922. See the [engine upgrade notes](docs/upstream-upgrades/README.md). ## Getting started @@ -80,7 +80,6 @@ The engine is pinned to `79741184`, with Ocarina of Time startup/memory/renderin |---|---| | **Mac** | Apple silicon, macOS 13 or later. | | **Xcode** | 27.2 or later, with the Metal compiler certified for the pinned engine. | -| **.NET** | .NET 10 SDK, used by the engine's extractor. | | **Engine** | nxnative at the pinned commit. Setup can prepare the checkout; an existing folder can be chosen in **Settings → Engine**. | | **Your files** | Your own supported game sources and keys, with the rights and permissions needed to use them. | @@ -117,7 +116,7 @@ Packaged releases check this repository for app updates; no GitHub account is ne Setup has two steps, **Get ready** and **Your games**. -**Get ready** shows the engine, build tools and keys. **Install all** handles the supported steps: the engine checkout, the certified Metal compiler and the official .NET installer. Xcode itself comes from the App Store; its licence and any system prompts remain your responsibility. Choose **Continue** to proceed; incomplete build requirements can be finished later, and existing game apps remain playable. +**Get ready** shows the engine, build tools and keys. **Install all** handles the supported steps: the engine checkout and the certified Metal compiler. The engine's tools are native, so .NET is no longer needed. Xcode itself comes from the App Store; its licence and any system prompts remain your responsibility. Choose **Continue** to proceed; incomplete build requirements can be finished later, and existing game apps remain playable. **Your games** shows **Game apps**: where to keep existing nxnative games (`.app`) and install new builds. It checks the selected folder before reporting an empty library. Use **Change** to choose another folder, **Choose game file** for NSP, XCI, NSZ or XCZ, or **Choose a folder instead** for a source folder, including supported extracted inputs. A single detected title opens the existing build review; multiple titles require a choice. Selecting sources never starts a build by itself. Cancelling the picker stays on this page. @@ -155,6 +154,12 @@ When you quit a game, NX Library comes back to the front, unless you had moved t

Quick access.
Open the menu without leaving your library.

+### Language and region + +The unreleased hotfix adds **English, Italiano, Français, Deutsch, Español and Português (Brasil)**. **Settings → Start-up → Language & region** follows the Mac automatically or lets you choose the app language and region separately. Changes apply immediately and are remembered. The **Regional formats** setting controls only date, number and size formats; **Game options → Language** continues to control each game independently. + +Choose a language directly from its list. Known catalog genres are translated locally, without changing the saved metadata. Descriptions from Nlib are requested in the app language when supported, with cached descriptions shared between regions using the same language. Switching language reuses available artwork and credits; when those are fresh, only the localized description is requested. English remains available when a translated description cannot be found; descriptions, mod text and release notes from external sources are not automatically translated. Personal names, metadata choices, artwork and game settings are preserved. + ### Add or rebuild a game 1. Open **Add game** and choose or drop a supported file or folder. @@ -293,7 +298,7 @@ Before reporting a problem, review copied diagnostics and remove anything person ### Compatibility and limitations - NX Library uses a manually pinned engine and checks its commit, working tree, CLI contract and Metal compiler before new builds. It does not follow upstream `main` automatically. **Settings → Engine → Newest engine** lets you choose `main` yourself before NX Library has tested it: an orange dot marks it, builds are recorded as untested, **Update** moves to a newer `main` when there is one, **Back to tested** returns to the pinned engine, and it is not offered when `main` needs a different Metal compiler. -- The current engine is `79741184` and certifies Metal `32023.922`. **Build anyway with an untested toolchain** requires explicit confirmation and marks the resulting build as untested. A successful launch does not turn it into a certified combination. +- The current engine is `8a5c5e7e` and certifies Metal `32023.922`. **Build anyway with an untested toolchain** requires explicit confirmation and marks the resulting build as untested. A successful launch does not turn it into a certified combination. - Game compatibility depends on nxnative, the game version and the toolchain. - Output follows the window/display; native 1440p/4K internal rendering is not supported. Render scale is 50–100% where the build supports it. MetalFX is spatial upscaling. - Builds run one at a time. iOS/IPA output, automatic engine updates and notarisation are not currently provided. @@ -304,14 +309,16 @@ The [upgrade notes](docs/upstream-upgrades/README.md) record each engine review App data lives in `~/Library/Application Support/NXLibrary/`. Local game data uses `~/Library/Application Support/nxnative//`, with mods in its `mods` folder. Backups are stored under `NXLibrary/SaveBackups/<title ID>/`. -Storage can clear caches and logs and open the shared backups folder. **Remove Sources & Build Cache deletes the selected original source files**, while retaining the installed app and saves. Read that confirmation carefully; it differs from clearing a cache. +**Settings → Storage → By game** shows app, build cache and log sizes for every library entry, including hidden games, with the largest caches first. **Delete cache** permanently removes only the chosen game's build cache after confirmation. Its app, original game files, saves and logs stay in place; the next rebuild may take longer. Shared caches are identified, and their confirmation explains the effect on the other entries. Connect unavailable drives or check folder access, then choose **Recalculate**. Sizes describe file contents; actual space recovered can differ, and shared folders count once in totals. This addition is unreleased; the maintainer confirmed it works in Dev. + +Storage can also clear all build caches or logs and open the shared backups folder. Cleanup runs without blocking navigation and is unavailable during a build or app update. **Remove Sources & Build Cache deletes the selected original source files**, while retaining the installed app and saves. Read that confirmation carefully; it differs from clearing a cache. ## Next steps - **Simpler Settings** — Keep everyday options easy to find, with advanced controls grouped within each category. - **In-app bug reporting** — Make it easier to report a problem directly from NX Library. - **Broader game discovery** — Discover and launch games from Steam, CrossOver and the Mac App Store, alongside nxnative apps. -- **Languages** — NX Library in more languages than English. +- **Community translations** — Review and extend the initial six interface languages. - **Better controller support** — Refine controller settings and offer more flexible button mapping. - **Continued polish** — Fix bugs and improve navigation, feedback and visual details throughout the app. diff --git a/Sources/NXLibrary/Models/AppIconAppearanceMode.swift b/Sources/NXLibrary/Models/AppIconAppearanceMode.swift index d121877..008a4ab 100644 --- a/Sources/NXLibrary/Models/AppIconAppearanceMode.swift +++ b/Sources/NXLibrary/Models/AppIconAppearanceMode.swift @@ -1,3 +1,4 @@ +import NXLibraryCore import Foundation public enum AppIconAppearanceMode: String, CaseIterable, Identifiable, Hashable { @@ -10,5 +11,5 @@ public enum AppIconAppearanceMode: String, CaseIterable, Identifiable, Hashable /// Light and Dark remain readable for existing preferences; today's artwork /// has one colour variant and one monochrome variant. public static let selectable: [Self] = [.auto, .mono] - public var label: String { self == .mono ? "Monochrome" : "Color" } + public var label: String { self == .mono ? L10n.text("Monochrome") : L10n.text("Color") } } diff --git a/Sources/NXLibrary/Models/NXLibraryError.swift b/Sources/NXLibrary/Models/NXLibraryError.swift index d80b51c..110139a 100644 --- a/Sources/NXLibrary/Models/NXLibraryError.swift +++ b/Sources/NXLibrary/Models/NXLibraryError.swift @@ -19,27 +19,27 @@ public enum NXLibraryError: Error, LocalizedError { public var errorDescription: String? { switch self { case .baseRequired: - return "Select a base game. Updates and DLC alone cannot be built." + return L10n.text("Select a base game. Updates and DLC alone cannot be built.") case .missingSources: - return "The source files are missing. Attach or locate the original base, update and DLC files first." + return L10n.text("The source files are missing. Attach or locate the original base, update and DLC files first.") case .missingTitleId: - return "The game has no title ID. Choose a valid NSP, XCI, NSZ or XCZ source." + return L10n.text("The game has no title ID. Choose a valid NSP, XCI, NSZ or XCZ source.") case .missingKeys: - return "prod.keys is missing. Import it in Settings." + return L10n.text("prod.keys is missing. Import it in Settings.") case .engineMissing: - return "The nxnative checkout was not found. Choose it in Settings → Engine." + return L10n.text("The nxnative checkout was not found. Choose it in Settings → Engine.") case .environmentBlocked(let detail): - return "The build environment is incomplete: \(detail)" + return L10n.format("The build environment is incomplete: %@", [String(describing: (detail))]) case .incompatibleToolchain(let detail): - return "The toolchain is not certified: \(detail)" + return L10n.format("The toolchain is not certified: %@", [String(describing: (detail))]) case .buildAlreadyRunning: - return "A build is already running." + return L10n.text("A build is already running.") case .appMissing(let path): - return "The app bundle is missing: \(path)" + return L10n.format("The app bundle is missing: %@", [String(describing: (path))]) case .launchFailed(let name): - return "macOS could not launch \(name)." + return L10n.format("macOS could not launch %@.", [String(describing: (name))]) case .noSelectedGame: - return "Select a game first." + return L10n.text("Select a game first.") case .keyCheck(let reason, let game): return "\(reason.what(game: game)) \(reason.next)" } diff --git a/Sources/NXLibrary/NXLibraryApp.swift b/Sources/NXLibrary/NXLibraryApp.swift index 1d67edb..7cc8371 100644 --- a/Sources/NXLibrary/NXLibraryApp.swift +++ b/Sources/NXLibrary/NXLibraryApp.swift @@ -1,3 +1,4 @@ +import NXLibraryCore import SwiftUI extension Notification.Name { @@ -5,6 +6,7 @@ extension Notification.Name { static let nxLibraryCheckAppUpdates = Notification.Name("NXLibraryCheckAppUpdates") static let nxLibraryOpenMenu = Notification.Name("NXLibraryOpenMenu") static let nxSettingsGamepadAction = Notification.Name("NXSettingsGamepadAction") + static let nxLocalePickerGamepadAction = Notification.Name("NXLocalePickerGamepadAction") static let nxAddGameGamepadAction = Notification.Name("NXAddGameGamepadAction") static let nxConsoleGamepadAction = Notification.Name("NXConsoleGamepadAction") static let nxDetailGamepadAction = Notification.Name("NXDetailGamepadAction") @@ -25,56 +27,65 @@ extension Notification.Name { struct NXLibraryApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate @FocusedValue(\.nxMenuAvailability) private var availability + @ObservedObject private var localization = AppLocalization.shared + private let settingsStore: AppSettingsStore + + init() { + let store = AppSettingsStore() + settingsStore = store + // Publish the initial locale before SwiftUI starts constructing the window graph. + AppLocalization.shared.configure(store.settings) + } var body: some Scene { Window("NX Library", id: "main") { - MainView() + MainView(settingsStore: settingsStore) } .windowStyle(.hiddenTitleBar) .defaultSize(width: 1440, height: 900) .commands { SidebarCommands() CommandGroup(replacing: .newItem) { - Button("Add Game…") { NativeMenuCommands.shared.post(.nxLibraryOpenAddGame) } + Button(L10n.text("Add Game…")) { NativeMenuCommands.shared.post(.nxLibraryOpenAddGame) } .keyboardShortcut("n", modifiers: [.command]) .disabled(availability?.general != true) } CommandGroup(after: .appInfo) { - Button("Check for Updates…") { NativeMenuCommands.shared.post(.nxLibraryCheckAppUpdates) } + Button(L10n.text("Check for Updates…")) { NativeMenuCommands.shared.post(.nxLibraryCheckAppUpdates) } .disabled(availability?.general != true) } CommandGroup(replacing: .appSettings) { - Button("Settings…") { + Button(L10n.text("Settings…")) { NativeMenuCommands.shared.post(.nxLibraryOpenSettings) } .keyboardShortcut(",", modifiers: [.command]) .disabled(availability?.general != true) } CommandMenu("Game") { - Button("Add Game…") { + Button(L10n.text("Add Game…")) { NativeMenuCommands.shared.post(.nxLibraryOpenAddGame) } .disabled(availability?.general != true) Divider() - Button("Play Selected Game") { + Button(L10n.text("Play Selected Game")) { NativeMenuCommands.shared.post(.nxLibraryPlayFocused) } .keyboardShortcut(.return, modifiers: [.command]) .disabled(availability?.play != true) Divider() - Button("Game Details") { + Button(L10n.text("Game Details")) { NativeMenuCommands.shared.post(.nxLibraryOpenDetails) } .keyboardShortcut("d", modifiers: [.command]) .disabled(availability?.details != true) - Button("Game Options…") { + Button(L10n.text("Game Options…")) { NativeMenuCommands.shared.post(.nxLibraryOpenGameOptions) } .keyboardShortcut("o", modifiers: [.command]) .disabled(availability?.options != true) } CommandGroup(after: .sidebar) { - Button("Menu") { + Button(L10n.text("Menu")) { NativeMenuCommands.shared.post(.nxLibraryOpenMenu) } .keyboardShortcut("k", modifiers: [.command]) diff --git a/Sources/NXLibrary/Services/AppUpdater.swift b/Sources/NXLibrary/Services/AppUpdater.swift index 6ba9ec4..cd29c44 100644 --- a/Sources/NXLibrary/Services/AppUpdater.swift +++ b/Sources/NXLibrary/Services/AppUpdater.swift @@ -219,24 +219,24 @@ final class AppUpdater: ObservableObject { nonisolated private static func prepare(zip: URL, release: AppUpdate.Release, folder: URL, expectedID: String, runningOS: OperatingSystemVersion) -> Result<URL, PreparationError> { guard AppUpdate.checkDigest(zip, expected: release.digest) == .match else { - return .failure(.init(message: "The download did not match the release; nothing was changed.")) + return .failure(.init(message: L10n.text("The download did not match the release; nothing was changed."))) } let unpacked = folder.appendingPathComponent("unpacked") guard let resources = Bundle.main.resourceURL, let runtime = BundledRuntime.locate(resourceURL: resources), run(runtime.python.path, ["-I", "-B", resources.appendingPathComponent("validate_update.py").path, zip.path, unpacked.path]) else { - return .failure(.init(message: "The update archive could not be verified; nothing was changed.")) + return .failure(.init(message: L10n.text("The update archive could not be verified; nothing was changed."))) } let staged = unpacked.appendingPathComponent("NXLibrary.app") if let problem = AppUpdate.bundleProblem(staged, expectedID: expectedID, version: release.version, runningOS: runningOS) { if case .needsNewerMacOS(let minimum) = problem { - return .failure(.init(message: "NX Library \(release.version) needs macOS \(minimum) or later; nothing was changed.")) + return .failure(.init(message: L10n.format("NX Library %@ needs macOS %@ or later; nothing was changed.", [String(describing: (release.version)), String(describing: (minimum))]))) } - return .failure(.init(message: "The download is not a valid NX Library \(release.version); nothing was changed.")) + return .failure(.init(message: L10n.format("The download is not a valid NX Library %@; nothing was changed.", [String(describing: (release.version))]))) } guard AppUpdate.validCodeSignature(staged) else { - return .failure(.init(message: "The app signature could not be verified; nothing was changed.")) + return .failure(.init(message: L10n.text("The app signature could not be verified; nothing was changed."))) } return .success(staged) } diff --git a/Sources/NXLibrary/Services/SoundEffectService.swift b/Sources/NXLibrary/Services/SoundEffectService.swift index 3431fe0..5f80cd3 100644 --- a/Sources/NXLibrary/Services/SoundEffectService.swift +++ b/Sources/NXLibrary/Services/SoundEffectService.swift @@ -52,7 +52,7 @@ public final class SoundEffectService: ObservableObject { /// Starting a player blocks for a few milliseconds (the audio server's /// handshake), so the players live and play on a queue of their own: a move /// never holds up the frame that draws it. - private let audio = DispatchQueue(label: "local.nxlibrary.sounds", qos: .userInteractive) + private let audio = DispatchQueue(label: L10n.text("local.nxlibrary.sounds"), qos: .userInteractive) private var players: [SoundCue: AVAudioPlayer] = [:] private var lastMove = Date.distantPast private let resolver = SoundCueResolver( diff --git a/Sources/NXLibrary/ViewModels/AppViewModel+AppIcon.swift b/Sources/NXLibrary/ViewModels/AppViewModel+AppIcon.swift index 86749d2..eab4b3e 100644 --- a/Sources/NXLibrary/ViewModels/AppViewModel+AppIcon.swift +++ b/Sources/NXLibrary/ViewModels/AppViewModel+AppIcon.swift @@ -38,9 +38,9 @@ extension AppViewModel { /// Keep one cancellable Play request while the app is being signed; signing itself must finish safely. func waitForAppIconBeforeLaunching(_ game: PortedGame) { - showSettingsNotice(title: "Preparing \(game.name)", - message: "Preparing the app icon. The game will start when it is ready. This can take a minute for a large game.", - cancelTitle: "Cancel") + showSettingsNotice(title: L10n.format("Preparing %@", [String(describing: (game.name))]), + message: L10n.text("Preparing the app icon. The game will start when it is ready. This can take a minute for a large game."), + cancelTitle: L10n.text("Cancel")) guard let notice = settingsNotice else { return } iconLaunchWait.begin(gameID: game.id, noticeID: notice.id) launchingGameId = game.id @@ -58,7 +58,7 @@ extension AppViewModel { if settingsNotice?.id == noticeID { settingsNotice = nil if case .failure(let error) = outcome { - showSettingsNotice(title: "Could not prepare the game", message: error.localizedDescription) + showSettingsNotice(title: L10n.text("Could not prepare the game"), message: error.localizedDescription) } } if let launchID, let current = libraryGames.first(where: { $0.id == launchID }) { @@ -158,12 +158,12 @@ extension AppViewModel { GameIconProvider.forget(game) GameArtLoader.shared.forget(game) ArtworkPicks.shared.touch() - self.logs.append(LogEntry(level: .info, message: "Updated the app icon of \(game.name).")) + self.logs.append(LogEntry(level: .info, message: L10n.format("Updated the app icon of %@.", [String(describing: (game.name))]))) } case .failure(let error): self.appIconProgress[game.id] = .failed(error.localizedDescription) self.logs.append(LogEntry(level: .error, - message: "Could not change the app icon of \(game.name): \(error.localizedDescription)")) + message: L10n.format("Could not change the app icon of %@: %@", [String(describing: (game.name)), String(describing: (error.localizedDescription))]))) } if !again, let buildLog, self.appIconBuildLogs[game.id] == buildLog, let status = self.appIconProgress[game.id] { diff --git a/Sources/NXLibrary/ViewModels/AppViewModel+Cheats.swift b/Sources/NXLibrary/ViewModels/AppViewModel+Cheats.swift index 34ba808..8529049 100644 --- a/Sources/NXLibrary/ViewModels/AppViewModel+Cheats.swift +++ b/Sources/NXLibrary/ViewModels/AppViewModel+Cheats.swift @@ -96,7 +96,7 @@ extension AppViewModel { } let size = (try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize ?? 0 guard size <= CheatFile.maxBytes else { return .refused("The file is larger than 4 MB, so it is not a cheat file.") } - guard let data = try? Data(contentsOf: url) else { return .refused("NX Library could not read the file.") } + guard let data = try? Data(contentsOf: url) else { return .refused(L10n.text("NX Library could not read the file.")) } return CheatInstall.decide(fileName: url.lastPathComponent, data: data, titleIDInPath: titleIDInPath, buildID: buildID, titleID: titleID) } diff --git a/Sources/NXLibrary/ViewModels/AppViewModel+Diagnostics.swift b/Sources/NXLibrary/ViewModels/AppViewModel+Diagnostics.swift index 62b8d14..607f3e2 100644 --- a/Sources/NXLibrary/ViewModels/AppViewModel+Diagnostics.swift +++ b/Sources/NXLibrary/ViewModels/AppViewModel+Diagnostics.swift @@ -30,7 +30,6 @@ extension AppViewModel { i.metalCertified = ToolchainRequirements.current.metalAssets.keys.sorted().joined(separator: ", ") i.untestedOverride = allowUntestedToolchain let tools = environmentStatus?.tools ?? [] - i.dotnet = tools.first { $0.name.localizedCaseInsensitiveContains(".NET") }?.detail i.python = engineToolchain.python.path i.setupRows = tools.map { ($0.name, $0.ok) } i.keysImported = FileManager.default.fileExists(atPath: keyStore.keyURL.path) diff --git a/Sources/NXLibrary/ViewModels/AppViewModel+NewestEngine.swift b/Sources/NXLibrary/ViewModels/AppViewModel+NewestEngine.swift index 10c8248..6c55668 100644 --- a/Sources/NXLibrary/ViewModels/AppViewModel+NewestEngine.swift +++ b/Sources/NXLibrary/ViewModels/AppViewModel+NewestEngine.swift @@ -66,15 +66,21 @@ extension AppViewModel { let turnedOn = !self.allowUntestedToolchain || previous?.turnedOnOverride == true self.allowUntestedToolchain = true self.newestEngineChoice = NewestEngineChoice(commit: commit, aheadBy: ahead, turnedOnOverride: turnedOn) - let verb = previous.map { "updated from \($0.shortCommit) to" } ?? "switched to" - self.logs.append(LogEntry(level: .warning, - message: "Engine \(verb) nxnative main \(commit.prefix(7)) (\(NewestEngineCopy.ahead(ahead))); builds are recorded as untested.")) + let message: String + if let previous { + message = L10n.format("Engine updated from %@ to nxnative main %@ (%@); builds are recorded as untested.", + [previous.shortCommit, String(commit.prefix(7)), NewestEngineCopy.ahead(ahead)]) + } else { + message = L10n.format("Engine switched to nxnative main %@ (%@); builds are recorded as untested.", + [String(commit.prefix(7)), NewestEngineCopy.ahead(ahead)]) + } + self.logs.append(LogEntry(level: .warning, message: message)) case .failure(let error): if case NewestEngineError.needsNewerApp(let metal) = error { self.newestEngineReadiness = .needsNewerApp(metal: metal) } self.newestEngineProblem = error.localizedDescription - self.logs.append(LogEntry(level: .warning, message: "Newest engine: \(error.localizedDescription)")) + self.logs.append(LogEntry(level: .warning, message: L10n.format("Newest engine: %@", [String(describing: (error.localizedDescription))]))) } self.refreshCompatibility() } @@ -96,10 +102,10 @@ extension AppViewModel { switch result { case .success: self.forgetNewestEngine() - self.logs.append(LogEntry(level: .info, message: "Engine back to the tested \(pinned.prefix(7)).")) + self.logs.append(LogEntry(level: .info, message: L10n.format("Engine back to the tested %@.", [String(describing: (pinned.prefix(7)))]))) case .failure(let error): self.newestEngineProblem = error.localizedDescription - self.logs.append(LogEntry(level: .warning, message: "Back to tested: \(error.localizedDescription)")) + self.logs.append(LogEntry(level: .warning, message: L10n.format("Back to tested: %@", [String(describing: (error.localizedDescription))]))) } self.refreshCompatibility() } diff --git a/Sources/NXLibrary/ViewModels/AppViewModel+PendingInstall.swift b/Sources/NXLibrary/ViewModels/AppViewModel+PendingInstall.swift index b0843fb..dea4956 100644 --- a/Sources/NXLibrary/ViewModels/AppViewModel+PendingInstall.swift +++ b/Sources/NXLibrary/ViewModels/AppViewModel+PendingInstall.swift @@ -24,7 +24,7 @@ extension AppViewModel { do { switch try PendingInstaller.install(&library.games[index]) { case .installed: - logs.append(LogEntry(level: .success, message: "Installed the new build of \(name).")) + logs.append(LogEntry(level: .success, message: L10n.format("Installed the new build of %@.", [String(describing: (name))]))) library.games[index].appIconPending = true if let path = library.games[index].lastBuild?.logPath, !path.isEmpty { appIconBuildLogs[library.games[index].id] = URL(fileURLWithPath: path) @@ -40,7 +40,7 @@ extension AppViewModel { } case .gone: logs.append(LogEntry(level: .warning, - message: "The new build of \(name) that waited for the game to quit is gone. Port it again.")) + message: L10n.format("The new build of %@ that waited for the game to quit is gone. Port it again.", [String(describing: (name))]))) case nil: continue } @@ -56,7 +56,7 @@ extension AppViewModel { } func presentInstallFailure(for game: PortedGame, error: String) { - logs.append(LogEntry(level: .error, message: "Could not install the new build of \(game.name): \(error)")) + logs.append(LogEntry(level: .error, message: L10n.format("Could not install the new build of %@: %@", [String(describing: (game.name)), String(describing: (error))]))) installFailureFocus = .cancel gameWithInstallFailure = game } diff --git a/Sources/NXLibrary/ViewModels/AppViewModel+Saves.swift b/Sources/NXLibrary/ViewModels/AppViewModel+Saves.swift index 1e03efa..4839cff 100644 --- a/Sources/NXLibrary/ViewModels/AppViewModel+Saves.swift +++ b/Sources/NXLibrary/ViewModels/AppViewModel+Saves.swift @@ -7,7 +7,7 @@ extension AppViewModel { try FileManager.default.createDirectory(at: saveBackupRoot, withIntermediateDirectories: true) NSWorkspace.shared.open(saveBackupRoot) } catch { - showSettingsNotice(title: "Folder unavailable", message: "The backup folder could not be opened.") + showSettingsNotice(title: L10n.text("Folder unavailable"), message: L10n.text("The backup folder could not be opened.")) } } @@ -87,9 +87,9 @@ extension AppViewModel { let message = failure?.localizedDescription DispatchQueue.main.async { if let message { - self.logs.append(LogEntry(level: .error, message: "Save recovery for \(game.name): \(message) Backups: \(self.saveBackupRoot.path)")) + self.logs.append(LogEntry(level: .error, message: L10n.format("Save recovery for %@: %@ Backups: %@", [String(describing: (game.name)), String(describing: (message)), String(describing: (self.saveBackupRoot.path))]))) } else { - self.logs.append(LogEntry(level: .info, message: "Save recovery completed for \(game.name).")) + self.logs.append(LogEntry(level: .info, message: L10n.format("Save recovery completed for %@.", [String(describing: (game.name))]))) self.installPendingBuilds() } } diff --git a/Sources/NXLibrary/ViewModels/AppViewModel+SteamGridDB.swift b/Sources/NXLibrary/ViewModels/AppViewModel+SteamGridDB.swift index 3b7c204..5b573f4 100644 --- a/Sources/NXLibrary/ViewModels/AppViewModel+SteamGridDB.swift +++ b/Sources/NXLibrary/ViewModels/AppViewModel+SteamGridDB.swift @@ -13,7 +13,7 @@ extension AppViewModel { return error.localizedDescription } hasSteamGridDBKey = true - logs.append(LogEntry(level: .info, message: "SteamGridDB key saved; Change artwork searches SteamGridDB.")) + logs.append(LogEntry(level: .info, message: L10n.text("SteamGridDB key saved; Change artwork searches SteamGridDB."))) return nil } @@ -22,7 +22,7 @@ extension AppViewModel { try SteamGridDBKey().remove() hasSteamGridDBKey = false } catch { - showSettingsNotice(title: "Key not removed", message: error.localizedDescription) { [weak self] in + showSettingsNotice(title: L10n.text("Key not removed"), message: error.localizedDescription) { [weak self] in self?.removeSteamGridDBKey() } } diff --git a/Sources/NXLibrary/ViewModels/AppViewModel.swift b/Sources/NXLibrary/ViewModels/AppViewModel.swift index 05ba98d..b1d4250 100644 --- a/Sources/NXLibrary/ViewModels/AppViewModel.swift +++ b/Sources/NXLibrary/ViewModels/AppViewModel.swift @@ -24,7 +24,7 @@ public final class AppViewModel: ObservableObject { /// The last app icon change per game, for Change artwork's status line. @Published var appIconProgress: [UUID: AppIconProgress] = [:] let saveGate = SaveOperationGate() - let saveQueue = DispatchQueue(label: "local.nxlibrary.saves", qos: .userInitiated) + let saveQueue = DispatchQueue(label: L10n.text("local.nxlibrary.saves"), qos: .userInitiated) @Published var settingsNotice: SettingsNotice? { didSet { if let waitingGame = iconLaunchWait.gameID, settingsNotice?.id != iconLaunchWait.noticeID { @@ -235,7 +235,7 @@ public final class AppViewModel: ObservableObject { } public var formattedSelectedSourcesSize: String { - ByteCountFormatter.string(fromByteCount: selectedSourcesSizeBytes, countStyle: .file) + selectedSourcesSizeBytes.formatted(.byteCount(style: .file).locale(L10n.context.formatLocale)) } /// The build keeps an extracted copy and writes a temporary app before the @@ -257,10 +257,10 @@ public final class AppViewModel: ObservableObject { public var diskSpaceWarningMessage: String? { guard isDiskSpaceLow, let avail = availableBuildDiskBytes else { return nil } let needed = requiredBuildDiskBytes - avail - let availStr = ByteCountFormatter.string(fromByteCount: avail, countStyle: .file) - let requiredStr = ByteCountFormatter.string(fromByteCount: requiredBuildDiskBytes, countStyle: .file) - let neededStr = ByteCountFormatter.string(fromByteCount: needed, countStyle: .file) - return "Storage warning: this build needs ~\(requiredStr) free (twice the selected sources plus 10 GB), but only \(availStr) is available. Free at least \(neededStr) before adding it." + let availStr = Self.formatBytes(avail) + let requiredStr = Self.formatBytes(requiredBuildDiskBytes) + let neededStr = Self.formatBytes(needed) + return L10n.format("Storage warning: this build needs ~%@ free (twice the selected sources plus 10 GB), but only %@ is available. Free at least %@ before adding it.", [requiredStr, availStr, neededStr]) } public var canAddSelectedGame: Bool { @@ -320,8 +320,14 @@ public final class AppViewModel: ObservableObject { @Published public var storeMetadata: [UUID: NintendoStoreMetadata] = [:] @Published public var storeMetadataLoading: Set<UUID> = [] @Published public var storeMetadataErrors: [UUID: String] = [:] - @Published public var storageUsage: StorageUsage? = nil + @Published public var storageSnapshot: StorageSnapshot? = nil + public var storageUsage: StorageUsage? { storageSnapshot?.total } @Published public var isCalculatingStorage: Bool = false + @Published public private(set) var isCleaningStorage = false + @Published public private(set) var storageCleanupMessage: String? + private var storageRefreshRequested = false + + public var canCleanStorage: Bool { !isBuilding && !isCleaningStorage && !isCalculatingStorage && !isUpdatingApp } @Published public var showSetupCheck: Bool = false /// The Setup check's state and installs; lives with the app so an install survives closing the screen. lazy var setupModel = SetupCheckModel(viewModel: self) @@ -450,7 +456,7 @@ public final class AppViewModel: ObservableObject { .resolve(runtime: .locate(), repo: URL(fileURLWithPath: nxnativePath)) } - public let pinnedEngineCommit = "79741184af1558fa1ada0f61741eeecbd615b3f6" + public let pinnedEngineCommit = "8a5c5e7e1ee606c650fe1aef6959a83009b6a2fc" private let core = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Application Support/NXLibrary") lazy var libraryStore = LibraryStore(directory: core) @@ -460,6 +466,8 @@ public final class AppViewModel: ObservableObject { private let launchService = LaunchService() private lazy var upstreamChecker = UpstreamChecker() private lazy var storeMetadataService = NintendoStoreMetadataService() + private var metadataLocaleGeneration = UUID() + private var localizedMetadataRefresh: Task<Void, Never>? private let upstreamCacheKey = "NXLibrary_UpstreamStatus" private lazy var storageManager = StorageManager() private let sourceCatalog = SourceCatalog() @@ -622,7 +630,7 @@ public final class AppViewModel: ObservableObject { isProdKeysValid = false prodKeysSource = "Invalid stored copy" logs.append(LogEntry(level: .error, - message: "Stored prod.keys is invalid: \(error.localizedDescription)")) + message: L10n.format("Stored prod.keys is invalid: %@", [String(describing: (error.localizedDescription))]))) } return } @@ -643,7 +651,7 @@ public final class AppViewModel: ObservableObject { public func importKeys(from url: URL) { guard !isBuilding, !isUpdatingApp else { - showSettingsNotice(title: "Porting in progress", message: "Change keys after the porting finishes.") + showSettingsNotice(title: L10n.text("Porting in progress"), message: L10n.text("Change keys after the porting finishes.")) return } do { @@ -657,18 +665,18 @@ public final class AppViewModel: ObservableObject { lastError = nil logs.append(LogEntry( level: .success, - message: "prod.keys imported: header key present, \(summary.applicationKeyGenerations.count) application key generations." + message: L10n.format("prod.keys imported: header key present, %@ application key generations.", [String(describing: (summary.applicationKeyGenerations.count))]) )) } catch { keySummary = try? keyStore.validate(at: keyStore.keyURL) isProdKeysValid = keySummary?.hasHeaderKey == true prodKeysPath = isProdKeysValid ? keyStore.keyURL.path : "" lastError = error.localizedDescription - showSettingsNotice(title: "Keys not changed", message: isProdKeysValid - ? "The key file could not be imported. Your previous copy is kept." - : "The key file could not be imported. Choose a valid prod.keys file.") + showSettingsNotice(title: L10n.text("Keys not changed"), message: isProdKeysValid + ? L10n.text("The key file could not be imported. Your previous copy is kept.") + : L10n.text("The key file could not be imported. Choose a valid prod.keys file.")) logs.append(LogEntry(level: .error, - message: "Could not import prod.keys: \(error.localizedDescription)")) + message: L10n.format("Could not import prod.keys: %@", [String(describing: (error.localizedDescription))]))) } } @@ -721,7 +729,7 @@ public final class AppViewModel: ObservableObject { if let report, !report.ok { let blocked = report.checks.filter { $0.status == .block } .map { "\($0.title): \($0.detail)" }.joined(separator: " | ") - self.logs.append(LogEntry(level: .warning, message: "Toolchain gate: \(blocked)")) + self.logs.append(LogEntry(level: .warning, message: L10n.format("Toolchain gate: %@", [String(describing: (blocked))]))) } } } @@ -754,9 +762,11 @@ public final class AppViewModel: ObservableObject { } public func loadStoreMetadata(for game: PortedGame) { + let locale = L10n.context + let generation = metadataLocaleGeneration guard let titleId = game.titleId, !titleId.isEmpty else { return } if storeMetadataLoading.contains(game.id) { return } - if let existing = storeMetadata[game.id], storeMetadataService.isFresh(existing) { + if let existing = storeMetadata[game.id], storeMetadataService.isFresh(existing, locale: locale) { loadFallbackScreenshots(for: game) return } @@ -767,9 +777,11 @@ public final class AppViewModel: ObservableObject { Task { @MainActor [weak self] in guard let self else { return } do { - let metadata = try await service.metadata(for: game) + let metadata = try await service.metadata(for: game, locale: locale) + guard self.metadataLocaleGeneration == generation else { return } self.setStoreMetadata(metadata, for: game) } catch { + guard self.metadataLocaleGeneration == generation else { return } self.storeMetadataErrors[game.id] = error.localizedDescription } self.storeMetadataLoading.remove(game.id) @@ -777,6 +789,31 @@ public final class AppViewModel: ObservableObject { } } + /// A language change only resets presentation metadata, never the library, personal picks or game settings. + public func refreshLocalizedMetadata() { + metadataLocaleGeneration = UUID() + let generation = metadataLocaleGeneration + localizedMetadataRefresh?.cancel() + storeMetadataErrors.removeAll() + storeMetadataLoading.removeAll() + let games = libraryGames + let locale = L10n.context + let service = storeMetadataService + // Keep the visible description while another language loads. Disk reads do not block the UI. + localizedMetadataRefresh = Task { @MainActor [weak self] in + let entries = await Task.detached(priority: .userInitiated) { + games.compactMap { game -> (PortedGame, NintendoStoreMetadata)? in + guard let titleID = game.titleId, + let cached = service.cached(for: titleID, locale: locale) else { return nil } + return (game, cached) + } + }.value + guard let self, !Task.isCancelled, self.metadataLocaleGeneration == generation else { return } + for (game, metadata) in entries { self.setStoreMetadata(metadata, for: game) } + if let game = self.currentFocusedGame { self.loadStoreMetadata(for: game) } + } + } + /// Fills metadata from the seven-day disk cache without starting a network /// request. The library uses this so a previously viewed game paints its /// title, description and hero immediately when selected. @@ -785,7 +822,7 @@ public final class AppViewModel: ObservableObject { guard storeMetadata[game.id] == nil, let titleId = game.titleId, !titleId.isEmpty, - let metadata = storeMetadataService.cached(for: titleId) else { continue } + let metadata = storeMetadataService.cached(for: titleId, locale: L10n.context) else { continue } setStoreMetadata(metadata, for: game) } } @@ -942,7 +979,7 @@ public final class AppViewModel: ObservableObject { try GameAppDestination.prepareInitialFolder(gameAppsFolder, hasExplicitChoice: UserDefaults.standard.string(forKey: StorageKeys.savedLibraryFolder) != nil) } catch { - logs.append(LogEntry(level: .warning, message: "Could not prepare the default Game apps folder: \(error.localizedDescription)")) + logs.append(LogEntry(level: .warning, message: L10n.format("Could not prepare the default Game apps folder: %@", [String(describing: (error.localizedDescription))]))) } } @@ -950,7 +987,7 @@ public final class AppViewModel: ObservableObject { UserDefaults.standard.set(gameAppsFolder.path, forKey: StorageKeys.savedLibraryFolder) firstRunSetup.finish() showSetupCheck = false - logs.append(LogEntry(level: .success, message: "Setup completed.")) + logs.append(LogEntry(level: .success, message: L10n.text("Setup completed."))) } public func reopenSetup() { @@ -982,55 +1019,87 @@ public final class AppViewModel: ObservableObject { } public func refreshStorageUsage() { - guard !isCalculatingStorage else { return } + guard !isCleaningStorage else { return } + guard !isCalculatingStorage else { storageRefreshRequested = true; return } isCalculatingStorage = true let manager = storageManager let games = libraryGames + let folder = gameAppsFolder DispatchQueue.global(qos: .utility).async { [weak self] in - let usage = manager.usage(games: games) + let snapshot = manager.snapshot(games: games, applicationsFolder: folder) DispatchQueue.main.async { - self?.storageUsage = usage - self?.isCalculatingStorage = false + guard let self else { return } + self.isCalculatingStorage = false + if self.storageRefreshRequested || self.libraryGames != games || self.gameAppsFolder != folder { + self.storageRefreshRequested = false + self.refreshStorageUsage() + } else { + self.storageSnapshot = snapshot + } } } } - public func deleteCaches() { - guard !isBuilding else { - alertMessage = "Finish or cancel the active build before cleaning caches." + public func deleteCaches(for selected: PortedGame? = nil) { + guard canCleanStorage else { + showSettingsNotice(title: L10n.text("Cleanup unavailable"), + message: L10n.text("Wait for the current build, storage operation or app update to finish.")) return } - var removed = 0 - for game in libraryGames { - do { - try storageManager.deleteCache(for: game) - removed += 1 - } catch { - logs.append(LogEntry(level: .error, - message: "Could not delete the cache for \(game.name): \(error.localizedDescription)")) + if let selected { + guard let current = libraryGames.first(where: { $0.id == selected.id }), + current.buildCachePath == selected.buildCachePath, + current.workspacePath == selected.workspacePath else { + showSettingsNotice(title: L10n.text("Cache changed"), message: L10n.text("Recalculate storage and choose the game again. Nothing was deleted.")) + refreshStorageUsage() + return } + cleanStorage(games: [current], caches: true) + } else { + cleanStorage(games: libraryGames, caches: true) } - logs.append(LogEntry(level: .info, message: "Cleaned build caches for \(removed) game\(removed == 1 ? "" : "s").")) - refreshStorageUsage() } public func deleteLogs() { - guard !isBuilding else { - alertMessage = "Finish or cancel the active build before deleting logs." + guard canCleanStorage else { + showSettingsNotice(title: L10n.text("Cleanup unavailable"), + message: L10n.text("Wait for the current build, storage operation or app update to finish.")) return } - var removed = 0 - for game in libraryGames { - do { - try storageManager.deleteLogs(for: game) - removed += 1 - } catch { - logs.append(LogEntry(level: .error, - message: "Could not delete the logs for \(game.name): \(error.localizedDescription)")) + cleanStorage(games: libraryGames, caches: false) + } + + private func cleanStorage(games: [PortedGame], caches: Bool) { + isCleaningStorage = true + storageCleanupMessage = nil + let manager = storageManager + let protectedGames = libraryGames + DispatchQueue.global(qos: .utility).async { [weak self] in + var removed = 0 + var failures: [String] = [] + for game in games { + do { + if caches { + if try manager.deleteCache(for: game, protecting: protectedGames) { removed += 1 } + } else { + if try manager.deleteLogs(for: game) { removed += 1 } + } + } catch { failures.append("\(game.name): \(error.localizedDescription)") } + } + let message = caches + ? (removed == 0 ? L10n.text("No build caches were deleted.") : L10n.count("Deleted %lld build caches. Future rebuilds may take longer.", removed, arguments: [])) + : (removed == 0 ? L10n.text("No logs were deleted.") : L10n.count("Deleted logs for %lld games.", removed, arguments: [])) + DispatchQueue.main.async { + guard let self else { return } + self.isCleaningStorage = false + self.storageCleanupMessage = message + self.logs.append(LogEntry(level: .info, message: message)) + if !failures.isEmpty { + self.showSettingsNotice(title: L10n.text("Some files were not deleted"), message: failures.joined(separator: "\n")) + } + self.refreshStorageUsage() } } - logs.append(LogEntry(level: .info, message: "Deleted logs for \(removed) game\(removed == 1 ? "" : "s").")) - refreshStorageUsage() } @@ -1063,7 +1132,7 @@ public final class AppViewModel: ObservableObject { recoverSavesAtStartup() } catch { lastError = error.localizedDescription - logs.append(LogEntry(level: .error, message: "Could not read the library: \(error.localizedDescription)")) + logs.append(LogEntry(level: .error, message: L10n.format("Could not read the library: %@", [String(describing: (error.localizedDescription))]))) } } @@ -1108,7 +1177,7 @@ public final class AppViewModel: ObservableObject { } catch { self.lastError = error.localizedDescription self.logs.append(LogEntry(level: .error, - message: "Could not update the library: \(error.localizedDescription)")) + message: L10n.format("Could not update the library: %@", [String(describing: (error.localizedDescription))]))) } for index in library.games.indices { let scannedApp = library.games[index].titleId @@ -1205,7 +1274,7 @@ public final class AppViewModel: ObservableObject { ) self.librarySelection.selectedGameID = updated.id self.logs.append(LogEntry(level: .success, - message: "Sources attached to \(game.name); rebuild is now available if the toolchain gate allows it.")) + message: L10n.format("Sources attached to %@; rebuild is now available if the toolchain gate allows it.", [String(describing: (game.name))]))) } catch { self.alertMessage = error.localizedDescription self.logs.append(LogEntry(level: .error, message: error.localizedDescription)) @@ -1261,7 +1330,7 @@ public final class AppViewModel: ObservableObject { guard game.isPlayableLocally else { let reason = GameResourceState.unavailableReason(for: game) let message = reason.map { $0.title + ". " + $0.detail } - ?? "This game is not built yet. Build it first." + ?? L10n.text("This game is not built yet. Build it first.") lastError = message alertMessage = message SoundEffectService.shared.play(.error) @@ -1310,7 +1379,7 @@ public final class AppViewModel: ObservableObject { alertMessage = lastError SoundEffectService.shared.play(.error) logs.append(LogEntry(level: .error, - message: "Could not write the runtime environment: \(error.localizedDescription)")) + message: L10n.format("Could not write the runtime environment: %@", [String(describing: (error.localizedDescription))]))) return } @@ -1318,9 +1387,9 @@ public final class AppViewModel: ObservableObject { do { try cloudSavesPreferences.applyBeforeLaunch(choice, domain: domain) } catch { - logs.append(LogEntry(level: .warning, message: "Cloud saves: \(error.localizedDescription)")) - showSettingsNotice(title: "Cloud unavailable", - message: "Cloud sync could not be updated for this launch. Check Games → Cloud saves.") + logs.append(LogEntry(level: .warning, message: L10n.format("Cloud saves: %@", [String(describing: (error.localizedDescription))]))) + showSettingsNotice(title: L10n.text("Cloud unavailable"), + message: L10n.text("Cloud sync could not be updated for this launch. Check Games → Cloud saves.")) } } @@ -1332,7 +1401,7 @@ public final class AppViewModel: ObservableObject { if success { self.lastError = nil self.playSessionStore?.start(for: game) - self.logs.append(LogEntry(level: .success, message: "Launched game: \(game.name)")) + self.logs.append(LogEntry(level: .success, message: L10n.format("Launched game: %@", [String(describing: (game.name))]))) self.watchLaunch(of: game) DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { if self.launchingGameId == game.id { self.launchingGameId = nil } @@ -1366,7 +1435,7 @@ public final class AppViewModel: ObservableObject { SoundEffectService.shared.play(.gameReady) } else { self.logs.append(LogEntry(level: .warning, - message: "\(game.name) showed no window within 60 s; launch screen closed.")) + message: L10n.format("%@ showed no window within 60 s; launch screen closed.", [String(describing: (game.name))]))) } self.launchOverlay?.isReady = true } @@ -1404,16 +1473,16 @@ public final class AppViewModel: ObservableObject { try cloudSavesPreferences.apply(choice, to: libraryGames.compactMap(\.effectiveBundleId)) } catch { let message = (error as? CloudSavesPreferences.Failure) == .iCloudDriveOff - ? "Turn on iCloud Drive or choose a sync folder." - : "The sync folder is unavailable. Choose another folder." - showSettingsNotice(title: "Cloud unavailable", message: message) + ? L10n.text("Turn on iCloud Drive or choose a sync folder.") + : L10n.text("The sync folder is unavailable. Choose another folder.") + showSettingsNotice(title: L10n.text("Cloud unavailable"), message: message) SoundEffectService.shared.play(.error) logs.append(LogEntry(level: .error, message: error.localizedDescription)) return } settingsStore.settings.cloudSaves = choice if case .folder(let path) = choice { settingsStore.settings.cloudSavesPickedFolder = path } - logs.append(LogEntry(level: .info, message: "Cloud saves: \(choice.folderURL()?.path ?? "off")")) + logs.append(LogEntry(level: .info, message: L10n.format("Cloud saves: %@", [String(describing: (choice.folderURL()?.path ?? "off"))]))) } /// Picks a folder for cloud saves; Cancel keeps the current choice. @@ -1514,7 +1583,7 @@ public final class AppViewModel: ObservableObject { /// A library that could not be written: what the player sees would be gone at the next start, so say so once /// (in the log every time) instead of losing it in silence. func reportLibrarySaveFailure(_ error: Error) { - let message = "NX Library could not save your library, so recent changes may be lost when it quits: " + let message = L10n.text("NX Library could not save your library, so recent changes may be lost when it quits: ") + error.localizedDescription logs.append(LogEntry(level: .error, message: message)) if alertMessage == nil { alertMessage = message } @@ -1568,8 +1637,8 @@ public final class AppViewModel: ObservableObject { } private func settingsNotSaved(_ game: PortedGame, _ error: Error) { - logs.append(LogEntry(level: .error, message: "Could not save settings for \(game.name): \(error.localizedDescription)")) - showSettingsNotice(title: "Options not saved", message: "Your game options could not be saved. Try again.") + logs.append(LogEntry(level: .error, message: L10n.format("Could not save settings for %@: %@", [String(describing: (game.name)), String(describing: (error.localizedDescription))]))) + showSettingsNotice(title: L10n.text("Options not saved"), message: L10n.text("Your game options could not be saved. Try again.")) } public func supportsSetting(_ env: String, for game: PortedGame) -> Bool { @@ -1583,11 +1652,11 @@ public final class AppViewModel: ObservableObject { @discardableResult public func saveRuntimeConfig(_ config: GameRuntimeConfig, for game: PortedGame) -> Bool { do { try config.save(for: game) - logs.append(LogEntry(level: .success, message: "Saved settings for \(game.name)")) + logs.append(LogEntry(level: .success, message: L10n.format("Saved settings for %@", [String(describing: (game.name))]))) return true } catch { - logs.append(LogEntry(level: .error, message: "Could not save settings for \(game.name): \(error.localizedDescription)")) - showSettingsNotice(title: "Options not saved", message: "Your game options could not be saved. Try again.") { [weak self] in + logs.append(LogEntry(level: .error, message: L10n.format("Could not save settings for %@: %@", [String(describing: (game.name)), String(describing: (error.localizedDescription))]))) + showSettingsNotice(title: L10n.text("Options not saved"), message: L10n.text("Your game options could not be saved. Try again.")) { [weak self] in self?.saveRuntimeConfig(config, for: game) } return false @@ -1617,24 +1686,24 @@ public final class AppViewModel: ObservableObject { var parts: [String] = [] if !existingSources.isEmpty { - parts.append("\(existingSources.count) original source file\(existingSources.count == 1 ? "" : "s") (\(Self.formatBytes(sourceBytes)))") + parts.append(L10n.count("%lld original source files (%@)", existingSources.count, arguments: [Self.formatBytes(sourceBytes)])) } if workspaceBytes > 0 { - parts.append("the build cache (\(Self.formatBytes(workspaceBytes)))") + parts.append(L10n.format("Build cache (%@)", [Self.formatBytes(workspaceBytes)])) } - let target = parts.isEmpty ? "the remaining build data" : parts.joined(separator: " and ") - return "This permanently deletes \(target). The installed app, saves and logs stay. Rebuilds will be disabled until you attach the sources again. This cannot be undone." + let target = parts.isEmpty ? L10n.text("Remaining build data") : parts.joined(separator: ", ") + return L10n.format("This permanently deletes: %@. The installed app, saves and logs stay. Rebuilds will be disabled until you attach the sources again. This cannot be undone.", [target]) } public func removeSources(for game: PortedGame) { guard !isBuilding else { - alertMessage = "Wait for the current build to finish before removing sources." + alertMessage = L10n.text("Wait for the current build to finish before removing sources.") return } let fm = FileManager.default guard var library = try? libraryStore.load(), let index = library.games.firstIndex(where: { $0.id == game.id }) else { - alertMessage = "Could not load the library entry for \(game.name)." + alertMessage = L10n.format("Could not load the library entry for %@.", [game.name]) return } @@ -1649,18 +1718,18 @@ public final class AppViewModel: ObservableObject { var isDir: ObjCBool = false guard fm.fileExists(atPath: url.path, isDirectory: &isDir) else { continue } if isDir.boolValue { - failures.append("\(url.lastPathComponent) is a folder") + failures.append(L10n.format("%@ is a folder", [url.lastPathComponent])) continue } if url.path.hasPrefix(appPath + "/") { - failures.append("\(url.lastPathComponent) is inside the app") + failures.append(L10n.format("%@ is inside the app", [url.lastPathComponent])) continue } let shared = otherGames.contains { other in other.sources.contains { $0.url.standardizedFileURL == url } } if shared { - failures.append("\(url.lastPathComponent) is also used by another library entry") + failures.append(L10n.format("%@ is also used by another library entry", [url.lastPathComponent])) continue } do { @@ -1683,7 +1752,7 @@ public final class AppViewModel: ObservableObject { try fm.removeItem(at: workspace) removedWorkspace = true } catch { - failures.append("build cache: \(error.localizedDescription)") + failures.append(L10n.format("Build cache: %@", [error.localizedDescription])) } } } @@ -1708,7 +1777,7 @@ public final class AppViewModel: ObservableObject { do { try libraryStore.save(library) } catch { - alertMessage = "The sources were removed, but the library could not be saved: \(error.localizedDescription)" + alertMessage = L10n.format("The sources were removed, but the library could not be saved: %@", [error.localizedDescription]) logs.append(LogEntry(level: .error, message: alertMessage!)) return } @@ -1716,16 +1785,17 @@ public final class AppViewModel: ObservableObject { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } - let removed = "Removed \(removedCount) source file\(removedCount == 1 ? "" : "s") (\(Self.formatBytes(removedBytes)))\(removedWorkspace ? " and the build cache" : "")." + let removed = L10n.count("Removed %lld source files (%@).", removedCount, arguments: [Self.formatBytes(removedBytes)]) + + (removedWorkspace ? " " + L10n.text("The build cache was also removed.") : "") if failures.isEmpty { logs.append(LogEntry(level: .success, - message: "\(removed) The installed app and saves were kept.")) - alertMessage = "\(removed) The installed app and saves were kept. Rebuilds stay disabled until you attach the sources again." + message: L10n.format("%@ The installed app and saves were kept.", [String(describing: (removed))]))) + alertMessage = L10n.format("%@ The installed app and saves were kept. Rebuilds stay disabled until you attach the sources again.", [removed]) } else { let detail = failures.joined(separator: "; ") logs.append(LogEntry(level: .warning, - message: "\(removed) Some items were kept: \(detail)")) - alertMessage = "\(removed) Some items were kept: \(detail)" + message: L10n.format("%@ Some items were kept: %@", [String(describing: (removed)), String(describing: (detail))]))) + alertMessage = L10n.format("%@ Some items were kept: %@", [removed, detail]) } } @@ -1752,7 +1822,7 @@ public final class AppViewModel: ObservableObject { } private static func formatBytes(_ bytes: Int64) -> String { - ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) + bytes.formatted(.byteCount(style: .file).locale(L10n.context.formatLocale)) } public func deletePortedGame(_ game: PortedGame) { @@ -1782,13 +1852,13 @@ public final class AppViewModel: ObservableObject { } if deleteBundle { logs.append(LogEntry(level: .warning, - message: "Game app deleted: \(game.name)\(removedWorkspace ? "; staged workspace removed" : "")")) + message: L10n.format("Game app deleted: %@%@", [String(describing: (game.name)), String(describing: (removedWorkspace ? "; staged workspace removed" : ""))]))) } else { logs.append(LogEntry(level: .warning, - message: "Removed \(game.name) from the library; its source folders were left in place.")) + message: L10n.format("Removed %@ from the library; its source folders were left in place.", [String(describing: (game.name))]))) } } catch { - let message = "Failed to delete game: \(error.localizedDescription)" + let message = L10n.format("Failed to delete game: %@", [String(describing: (error.localizedDescription))]) alertMessage = message logs.append(LogEntry(level: .error, message: message)) } @@ -1850,7 +1920,7 @@ public final class AppViewModel: ObservableObject { self.logs.append(LogEntry(level: .warning, message: self.scanError!)) } else { self.logs.append(LogEntry(level: .info, - message: "Detected \(games.count) game\(games.count == 1 ? "" : "s") in \(folder.lastPathComponent).")) + message: L10n.count("Detected %lld games in %@.", games.count, arguments: [String(describing: (folder.lastPathComponent))]))) } } } @@ -1867,7 +1937,7 @@ public final class AppViewModel: ObservableObject { let capabilities = EngineSourceCapabilities(repo: repo) if allowExtractedFolder && ExtractedSourceCatalog.recognizes(folder) { guard capabilities.extractedFolders else { - throw NXLibraryError.environmentBlocked("This engine does not support extracted folders.") + throw NXLibraryError.environmentBlocked(L10n.text("This engine does not support extracted folders.")) } let catalog = ExtractedSourceCatalog(profiles: repo.appendingPathComponent("dm/games"), provisional: capabilities.provisionalDumps) @@ -1880,7 +1950,7 @@ public final class AppViewModel: ObservableObject { } if let nup = pickedNUPs.first ?? (allowExtractedFolder ? NUPSourceCatalog.container(in: folder) : nil) { guard capabilities.nupContainers else { - throw NXLibraryError.environmentBlocked("This engine does not support NUP files.") + throw NXLibraryError.environmentBlocked(L10n.text("This engine does not support NUP files.")) } let reader = NUPSourceCatalog(repo: repo, toolchain: toolchain, developerDir: developerDir) return ScannedSources(games: [try reader.scan(nup: nup)]) @@ -2068,6 +2138,7 @@ public final class AppViewModel: ObservableObject { // MARK: - Build Actions public var canStartBuild: Bool { !isBuilding + && !isCleaningStorage && !isUpdatingApp && canAddSelectedGame && supportsSources(selectedSources) @@ -2082,6 +2153,7 @@ public final class AppViewModel: ObservableObject { public func canBuild(_ game: PortedGame) -> Bool { guard !isBuilding, + !isCleaningStorage, !isUpdatingApp, (!SourceSelection.requiresKeys(game.sources) || isProdKeysValid), supportsSources(game.sources), @@ -2220,7 +2292,7 @@ public final class AppViewModel: ObservableObject { progressFraction = 0 etaSeconds = nil handleBuildEvent(.stage(k: 1, n: 8, name: "extract", step: "reading NSP")) - handleBuildEvent(.stageEnd(name: "extract", ok: true, elapsed: 1.2, summary: "done")) + handleBuildEvent(.stageEnd(name: "extract", ok: true, elapsed: 1.2, summary: L10n.text("done"))) handleBuildEvent(.stage(k: 2, n: 8, name: "translate shaders", step: "metal")) handleBuildEvent(.progress(fraction: 0.34, done: 120, total: 350, unit: "MB", eta: 42)) handleBuildEvent(.say("Sample event: this is the live build reducer.")) @@ -2271,7 +2343,7 @@ public final class AppViewModel: ObservableObject { try pendingRuntimeConfig.save(for: game) } librarySelection.selectedGameID = game.id - logs.append(LogEntry(level: .success, message: "Added \(game.name) to the library.")) + logs.append(LogEntry(level: .success, message: L10n.format("Added %@ to the library.", [String(describing: (game.name))]))) if buildNow, canStartBuild { startBuild(for: game) } else { @@ -2340,9 +2412,10 @@ public final class AppViewModel: ObservableObject { private func beginBuild(for game: PortedGame, extraArguments: [String] = []) throws { guard !isBuilding else { throw NXLibraryError.buildAlreadyRunning } - guard !isUpdatingApp else { throw NXLibraryError.environmentBlocked("wait for the app update to finish") } + guard !isCleaningStorage else { throw NXLibraryError.environmentBlocked(L10n.text("wait for storage cleanup to finish")) } + guard !isUpdatingApp else { throw NXLibraryError.environmentBlocked(L10n.text("wait for the app update to finish")) } guard !appIconsInFlight.contains(game.id) else { - throw NXLibraryError.environmentBlocked("wait for the app icon update to finish") + throw NXLibraryError.environmentBlocked(L10n.text("wait for the app icon update to finish")) } var target = libraryGames.first(where: { $0.id == game.id }) ?? game guard let titleId = target.titleId, @@ -2361,7 +2434,7 @@ public final class AppViewModel: ObservableObject { } let requiresKeys = SourceSelection.requiresKeys(target.sources) guard supportsSources(target.sources) else { - throw NXLibraryError.environmentBlocked("These sources need an engine with extracted-folder or multi-title support.") + throw NXLibraryError.environmentBlocked(L10n.text("These sources need an engine with extracted-folder or multi-title support.")) } if requiresKeys { guard isProdKeysValid, FileManager.default.fileExists(atPath: keyStore.keyURL.path) else { @@ -2374,11 +2447,11 @@ public final class AppViewModel: ObservableObject { throw NXLibraryError.keyCheck(reason, game: target.name) } } else if !engineSourceCapabilities.extractedFolders { - throw NXLibraryError.environmentBlocked("This engine does not support extracted folders.") + throw NXLibraryError.environmentBlocked(L10n.text("This engine does not support extracted folders.")) } guard isNxnativeValid else { throw NXLibraryError.engineMissing } guard let environmentStatus else { - throw NXLibraryError.environmentBlocked("the environment check is still running") + throw NXLibraryError.environmentBlocked(L10n.text("the environment check is still running")) } guard environmentStatus.canBuild else { throw NXLibraryError.environmentBlocked(environmentStatus.issues.joined(separator: "; ")) @@ -2471,14 +2544,13 @@ public final class AppViewModel: ObservableObject { for volume in [workspace.directory, scratch.temp, finalApp.deletingLastPathComponent()] { if let device = BuildScratch.Volume.of(volume)?.device, !measured.insert(device).inserted { continue } if let free = freeBytes(at: volume), free < requiredDisk { - let freeText = ByteCountFormatter.string(fromByteCount: free, countStyle: .file) - let requiredText = ByteCountFormatter.string( - fromByteCount: requiredDisk, countStyle: .file) + let freeText = Self.formatBytes(free) + let requiredText = Self.formatBytes(requiredDisk) let why = scratch.appDriveUnsupported - ? " The Game apps folder's drive can't hold build files (it needs APFS or Mac OS Extended), so the build uses this Mac's disk." + ? L10n.text(" The Game apps folder's drive can't hold build files (it needs APFS or Mac OS Extended), so the build uses this Mac's disk.") : "" throw NXLibraryError.environmentBlocked( - "Not enough free space at \(volume.path): \(freeText) available, \(requiredText) needed.\(why)") + L10n.format("Not enough free space at %@: %@ available, %@ needed.%@", [String(describing: (volume.path)), String(describing: (freeText)), String(describing: (requiredText)), String(describing: (why))])) } } @@ -2486,8 +2558,8 @@ public final class AppViewModel: ObservableObject { let toolchain = engineToolchain guard FileManager.default.isExecutableFile(atPath: toolchain.python.path) else { throw NXLibraryError.environmentBlocked(toolchain.isBundled - ? "the bundled Python is missing; reinstall NX Library" - : "the nxnative venv Python is missing") + ? L10n.text("the bundled Python is missing; reinstall NX Library") + : L10n.text("the nxnative venv Python is missing")) } let tempRoot = scratch.temp.appendingPathComponent(UUID().uuidString) @@ -2540,14 +2612,14 @@ public final class AppViewModel: ObservableObject { portingProgress = PortingProgress() frontGameID = target.id logs.append(LogEntry(level: .info, - message: "Build started for \(target.name) (\(titleId)).")) + message: L10n.format("Build started for %@ (%@).", [String(describing: (target.name)), String(describing: (titleId))]))) logs.append(LogEntry(level: .info, - message: "Workspace: \(workspace.directory.path)")) + message: L10n.format("Workspace: %@", [String(describing: (workspace.directory.path))]))) logs.append(LogEntry( level: .info, message: report?.ok == true - ? "Toolchain gate: certified." - : "Toolchain gate: overridden; this build will be recorded as untested." + ? L10n.text("Toolchain gate: certified.") + : L10n.text("Toolchain gate: overridden; this build will be recorded as untested.") )) let service = BuildService() @@ -2577,7 +2649,7 @@ public final class AppViewModel: ObservableObject { stopStageElapsedTimer() isCancellingBuild = true currentStageName = "Cancelling build..." - logs.append(LogEntry(level: .warning, message: "Cancelling build...")) + logs.append(LogEntry(level: .warning, message: L10n.text("Cancelling build..."))) buildService.cancel() } @@ -2667,10 +2739,8 @@ public final class AppViewModel: ObservableObject { let percent = Int((min(1, max(0, done / total)) * 100).rounded(.down)) currentStepName = "\(Self.formatAmount(done)) / \(Self.formatAmount(total)) \(unit) (\(percent)%)" } else { - let doneText = ByteCountFormatter.string( - fromByteCount: Int64(done), countStyle: .file) - let totalText = ByteCountFormatter.string( - fromByteCount: Int64(total), countStyle: .file) + let doneText = Self.formatBytes(Int64(done)) + let totalText = Self.formatBytes(Int64(total)) currentStepName = "\(doneText) / \(totalText)" } } @@ -2808,8 +2878,8 @@ public final class AppViewModel: ObservableObject { buildError = nil lastError = nil logs.append(LogEntry(level: .success, message: result.heldApp == nil - ? "Build succeeded: \(finalApp.path)" - : "Build succeeded while \(game.name) is open: it installs when the game quits.")) + ? L10n.format("Build succeeded: %@", [String(describing: (finalApp.path))]) + : L10n.format("Build succeeded while %@ is open: it installs when the game quits.", [String(describing: (game.name))]))) SoundEffectService.shared.play(.launch) notifyPortingEnded(game, success: true, held: result.heldApp != nil) } @@ -2822,7 +2892,7 @@ public final class AppViewModel: ObservableObject { buildError = nil portingFailureReason = nil lastError = "Build cancelled." - logs.append(LogEntry(level: .warning, message: "Build cancelled.")) + logs.append(LogEntry(level: .warning, message: L10n.text("Build cancelled."))) } else { let reason = PortingFailureClassifier.classify( log: buildLogTail(result.logURL), @@ -2838,7 +2908,7 @@ public final class AppViewModel: ObservableObject { portingFailureReason = reason lastError = message logs.append(LogEntry(level: .error, - message: result.error ?? "Build failed (exit code \(result.exitCode)).")) + message: result.error ?? L10n.format("Build failed (exit code %@).", [String(describing: (result.exitCode))]))) logs.append(LogEntry(level: .error, message: "\(reason.title). \(message) \(reason.next)")) SoundEffectService.shared.play(.error) notifyPortingEnded(game, success: false) @@ -2853,7 +2923,7 @@ public final class AppViewModel: ObservableObject { if game.pendingInstall != nil { presentInstallFailure(for: game, error: error.localizedDescription) } else { - logs.append(LogEntry(level: .error, message: "Could not save the library: \(error.localizedDescription)")) + logs.append(LogEntry(level: .error, message: L10n.format("Could not save the library: %@", [String(describing: (error.localizedDescription))]))) } } libraryGames = library.games.sorted { @@ -2889,7 +2959,7 @@ public final class AppViewModel: ObservableObject { public func revealLog(for game: PortedGame) { guard let url = logURL(for: game) else { - logs.append(LogEntry(level: .warning, message: "No log path is known for \(game.name).")) + logs.append(LogEntry(level: .warning, message: L10n.format("No log path is known for %@.", [String(describing: (game.name))]))) return } let directory = url.deletingLastPathComponent() @@ -2899,7 +2969,7 @@ public final class AppViewModel: ObservableObject { } else { NSWorkspace.shared.activateFileViewerSelecting([directory]) logs.append(LogEntry(level: .info, - message: "No log yet for \(game.name); opened the log folder.")) + message: L10n.format("No log yet for %@; opened the log folder.", [String(describing: (game.name))]))) } } diff --git a/Sources/NXLibrary/ViewModels/ModStoreHost.swift b/Sources/NXLibrary/ViewModels/ModStoreHost.swift index b44cf30..b866e34 100644 --- a/Sources/NXLibrary/ViewModels/ModStoreHost.swift +++ b/Sources/NXLibrary/ViewModels/ModStoreHost.swift @@ -204,8 +204,8 @@ final class ModStoreHost: ObservableObject { func chip(for entry: ModCatalogEntry) -> String? { switch panel.gameBanana.installs[entry.id] { case .downloading(let fraction): return "Downloading \(Int(fraction * 100))%" - case .installing: return "Installing…" - case .failed: return "Didn't install" + case .installing: return L10n.text("Installing…") + case .failed: return L10n.text("Didn't install") case .done, nil: break } guard let record = records[entry.titleID]?[entry.id] else { return nil } diff --git a/Sources/NXLibrary/ViewModels/SetupCheckModel.swift b/Sources/NXLibrary/ViewModels/SetupCheckModel.swift index 2f9ec01..7426cfa 100644 --- a/Sources/NXLibrary/ViewModels/SetupCheckModel.swift +++ b/Sources/NXLibrary/ViewModels/SetupCheckModel.swift @@ -18,14 +18,11 @@ final class SetupCheckModel: ObservableObject { private unowned let viewModel: AppViewModel private let runner = SystemCommandRunner() - private let queue = DispatchQueue(label: "nxlibrary.setup-check", qos: .userInitiated) + private let queue = DispatchQueue(label: L10n.text("nxlibrary.setup-check"), qos: .userInitiated) private var facts: SetupFacts? private var overrides: [SetupItemID: SetupOverride] = [:] private var timer: Timer? private var probing = false - private var dotnetPackage: DotnetPackage? - private var dotnetLookup = false - private var dotnetObservation: NSKeyValueObservation? init(viewModel: AppViewModel) { self.viewModel = viewModel @@ -60,8 +57,7 @@ final class SetupCheckModel: ObservableObject { } private func apply(_ probed: SetupFacts) { - var facts = probed - facts.dotnetLatest = dotnetPackage?.version + let facts = probed self.facts = facts let base = SetupCheck.items(from: facts) // A card that is ready now ends what was waiting on it ("Finish in Installer", @@ -79,7 +75,6 @@ final class SetupCheckModel: ObservableObject { } func startRechecking() { - lookUpDotnet() refresh() timer?.invalidate() timer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { [weak self] _ in self?.refresh() } @@ -90,21 +85,6 @@ final class SetupCheckModel: ObservableObject { timer = nil } - /// Microsoft's newest SDK of the required major, once per session; offline it stays unknown. - private func lookUpDotnet() { - guard dotnetPackage == nil, !dotnetLookup else { return } - dotnetLookup = true - URLSession.shared.dataTask(with: DotnetRelease.metadataURL) { [weak self] data, _, _ in - let package = data.flatMap { try? DotnetRelease.latestSDKPackage(from: $0) } - DispatchQueue.main.async { - self?.dotnetLookup = false - guard let package, package.version.hasPrefix("\(Self.requirements.dotnetMajor).") else { return } - self?.dotnetPackage = package - self?.refresh() - } - }.resume() - } - func rememberStates() { var states = UserDefaults.standard.dictionary(forKey: Self.lastStatesKey) as? [String: String] ?? [:] for item in items where item.state != .working { @@ -142,9 +122,9 @@ final class SetupCheckModel: ObservableObject { // MARK: - Actions - /// What Install all runs, in card order, updates included. + /// What Install all runs, in card order. var missingAutomatic: [SetupItemID] { - SetupCheck.automatic(items, includeUpdates: true) + SetupCheck.automatic(items) } func activate(_ id: SetupItemID) { @@ -158,7 +138,7 @@ final class SetupCheckModel: ObservableObject { let app = viewModel.resolvedDeveloperDir().deletingLastPathComponent().deletingLastPathComponent() NSWorkspace.shared.open(app) } - case .metal, .dotnet, .engine: + case .metal, .engine: guard item.actionTitle != nil || item.state == .needsYou else { return } perform(id) { _ in } case .runtime: @@ -168,7 +148,7 @@ final class SetupCheckModel: ObservableObject { panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = false - panel.prompt = "Import" + panel.prompt = L10n.text("Import") panel.message = "Choose your prod.keys file. It stays on this Mac." if panel.runModal() == .OK, let url = panel.url { viewModel.importKeys(from: url) @@ -219,7 +199,6 @@ final class SetupCheckModel: ObservableObject { private func perform(_ id: SetupItemID, completion: @escaping (Bool) -> Void) { switch id { case .metal: installMetal(completion) - case .dotnet: installDotnet(completion) case .engine: fetchEngine(completion) default: completion(false) } @@ -261,7 +240,7 @@ final class SetupCheckModel: ObservableObject { let allowUntested = facts.allowUntested let pinned = Self.requirements.metalDownload(for: certified) let chip = "DOWNLOADING…" - setOverride(.metal, .working(chip, detail: "Asking Apple for Metal \(certified)…")) + setOverride(.metal, .working(chip, detail: L10n.format("Asking Apple for Metal %@…", [String(describing: (certified))]))) let env = environment queue.async { [weak self] in guard let self else { return } @@ -302,7 +281,7 @@ final class SetupCheckModel: ObservableObject { let commands = SetupActions.gitCommands(for: action, folder: facts.engineFolder, pinned: facts.pinnedCommit) guard !commands.isEmpty else { completion(false); return } let chip = action == .clone ? "CLONING…" : "UPDATING…" - setOverride(.engine, .working(chip, progress: 0, detail: "Connecting to GitHub…")) + setOverride(.engine, .working(chip, progress: 0, detail: L10n.text("Connecting to GitHub…"))) let env = environment queue.async { [weak self] in guard let self else { return } @@ -322,7 +301,7 @@ final class SetupCheckModel: ObservableObject { /// The newest engine's card: A returns to the tested engine, the same action as Settings → Engine. private func backToTestedEngine(_ completion: @escaping (Bool) -> Void) { - setOverride(.engine, .working("SWITCHING…", detail: "Returning to the tested engine…")) + setOverride(.engine, .working("SWITCHING…", detail: L10n.text("Returning to the tested engine…"))) viewModel.useTestedEngine() waitForEngineSwitch(completion) } @@ -341,61 +320,4 @@ final class SetupCheckModel: ObservableObject { } } } - - private func installDotnet(_ completion: @escaping (Bool) -> Void) { - let session = URLSession(configuration: .ephemeral, delegate: DotnetDownloadPolicy(), delegateQueue: nil) - let chip = "DOWNLOADING…" - setOverride(.dotnet, .working(chip, detail: "Asking Microsoft for the newest .NET \(Self.requirements.dotnetMajor) SDK…")) - let fail: (String) -> Void = { [weak self] message in - session.finishTasksAndInvalidate() - DispatchQueue.main.async { - self?.dotnetObservation = nil - self?.viewModel.logs.append(LogEntry(level: .error, message: ".NET SDK: \(message)")) - self?.setOverride(.dotnet, .failed(message)) - completion(false) - } - } - let download: (DotnetPackage) -> Void = { [weak self] package in - let task = session.downloadTask(with: package.url) { location, response, error in - defer { session.finishTasksAndInvalidate() } - guard let location, error == nil, let http = response as? HTTPURLResponse, - http.statusCode == 200, let finalURL = http.url, DotnetRelease.trustedDownloadURL(finalURL) - else { return fail(error?.localizedDescription ?? "The download stopped.") } - let folder = FileManager.default.temporaryDirectory.appendingPathComponent("NXLibrary-dotnet") - let pkg = folder.appendingPathComponent(package.url.lastPathComponent) - do { - try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) - try? FileManager.default.removeItem(at: pkg) - try FileManager.default.moveItem(at: location, to: pkg) - self?.report(.dotnet, "CHECKING…", InstallProgress(fraction: 1, label: "Checking Microsoft's hash…")) - guard try DotnetRelease.verify(file: pkg, sha512: package.sha512) else { - try? FileManager.default.removeItem(at: pkg) - return fail("The download does not match Microsoft's published hash, so it was deleted.") - } - } catch { - return fail(error.localizedDescription) - } - DispatchQueue.main.async { - self?.dotnetObservation = nil - // macOS Installer asks for the password; NX Library waits for the SDK to appear. - NSWorkspace.shared.open(pkg) - self?.viewModel.logs.append(LogEntry(level: .info, message: ".NET \(package.version) SDK installer opened.")) - self?.setOverride(.dotnet, .needsYou("FINISH IN INSTALLER")) - completion(true) - } - } - self?.dotnetObservation = task.progress.observe(\.fractionCompleted) { progress, _ in - self?.report(.dotnet, chip, InstallProgress.bytes(done: progress.completedUnitCount, - total: progress.totalUnitCount)) - } - task.resume() - } - if let package = dotnetPackage { download(package); return } - session.dataTask(with: DotnetRelease.metadataURL) { data, response, error in - guard let data, error == nil, let http = response as? HTTPURLResponse, - http.statusCode == 200, let finalURL = http.url, DotnetRelease.trustedDownloadURL(finalURL) - else { return fail(error?.localizedDescription ?? "No answer from Microsoft.") } - do { download(try DotnetRelease.latestSDKPackage(from: data)) } catch { fail(error.localizedDescription) } - }.resume() - } } diff --git a/Sources/NXLibrary/Views/AddGame/AddGameStepThreeView.swift b/Sources/NXLibrary/Views/AddGame/AddGameStepThreeView.swift index 63b5292..e9ade79 100644 --- a/Sources/NXLibrary/Views/AddGame/AddGameStepThreeView.swift +++ b/Sources/NXLibrary/Views/AddGame/AddGameStepThreeView.swift @@ -58,7 +58,7 @@ struct AddGameStepThreeView: View { VStack(alignment: .leading, spacing: canvas.u(24)) { HStack(alignment: .top, spacing: canvas.u(36)) { gameTile(caption: viewModel.portingETAText - ?? (viewModel.currentStageName.isEmpty ? "Preparing" : viewModel.currentStageName)) + ?? (viewModel.currentStageName.isEmpty ? L10n.text("Preparing") : viewModel.currentStageName)) stageList } PortingLogBox(logs: viewModel.logs, expanded: $viewModel.showLiveLogsInBuild) @@ -109,7 +109,7 @@ struct AddGameStepThreeView: View { private var stageList: some View { VStack(alignment: .leading, spacing: canvas.u(4)) { if viewModel.buildStages.isEmpty { - Text("Starting the porting…") + Text(L10n.text("Starting the porting…")) .seleneFont(17) .foregroundColor(SeleneTheme.ink60) } @@ -181,7 +181,7 @@ struct AddGameStepThreeView: View { .seleneFont(34) .foregroundColor(SeleneTheme.success) VStack(alignment: .leading, spacing: 4) { - Text(heldGameName == nil ? "Ready to play" : PendingInstall.title) + Text(heldGameName == nil ? L10n.text("Ready to play") : PendingInstall.title) .seleneFont(34, .semibold) .foregroundColor(SeleneTheme.ink) Text(path) @@ -194,7 +194,7 @@ struct AddGameStepThreeView: View { } Text(heldGameName.map(PendingInstall.message(gameName:)) - ?? "The app was installed atomically. Your library now points at the new build.") + ?? L10n.text("The app was installed atomically. Your library now points at the new build.")) .seleneFont(14) .foregroundColor(SeleneTheme.ink90) @@ -226,8 +226,8 @@ struct AddGameStepThreeView: View { .seleneFont(19) .foregroundColor(SeleneTheme.ink90) .fixedSize(horizontal: false, vertical: true) - Text(viewModel.canRetryReadyInstall ? "Your build has been kept. Retry installs it without porting again." - : reason?.next ?? "The previous app was left untouched. The log has the failing command and output.") + Text(viewModel.canRetryReadyInstall ? L10n.text("Your build has been kept. Retry installs it without porting again.") + : reason?.next ?? L10n.text("The previous app was left untouched. The log has the failing command and output.")) .seleneFont(17) .foregroundColor(SeleneTheme.ink60) .fixedSize(horizontal: false, vertical: true) @@ -249,10 +249,10 @@ struct AddGameStepThreeView: View { .seleneFont(34) .foregroundColor(SeleneTheme.untested) VStack(alignment: .leading, spacing: 5) { - Text("Porting cancelled") + Text(L10n.text("Porting cancelled")) .seleneFont(34, .semibold) .foregroundColor(SeleneTheme.ink) - Text("The previous app was left untouched. You can retry or go back to the review step.") + Text(L10n.text("The previous app was left untouched. You can retry or go back to the review step.")) .seleneFont(15) .foregroundColor(SeleneTheme.ink90) .fixedSize(horizontal: false, vertical: true) @@ -278,10 +278,10 @@ struct AddGameStepThreeView: View { .seleneFont(34) .foregroundColor(SeleneTheme.ink60) VStack(alignment: .leading, spacing: 4) { - Text("No build is active") + Text(L10n.text("No build is active")) .seleneFont(19, .semibold) .foregroundColor(SeleneTheme.ink) - Text("Go back to the review step and choose Add & Build to start a build.") + Text(L10n.text("Go back to the review step and choose Add & Build to start a build.")) .seleneFont(15) .foregroundColor(SeleneTheme.ink90) } @@ -296,25 +296,25 @@ struct AddGameStepThreeView: View { if viewModel.isBuilding { return [ StepAction(id: "log", - title: viewModel.showLiveLogsInBuild ? "Hide Log" : "Show Log", + title: viewModel.showLiveLogsInBuild ? L10n.text("Hide Log") : L10n.text("Show Log"), systemImage: "terminal", kind: .secondary, isEnabled: true) { viewModel.showLiveLogsInBuild.toggle() }, StepAction(id: "cancel", - title: viewModel.isCancellingBuild ? "Cancelling…" : "Cancel porting", + title: viewModel.isCancellingBuild ? L10n.text("Cancelling…") : L10n.text("Cancel porting"), systemImage: "stop.circle.fill", kind: .destructive, isEnabled: !viewModel.isCancellingBuild) { viewModel.cancelBuild() }, - StepAction(id: "openLog", title: "Open log", + StepAction(id: "openLog", title: L10n.text("Open log"), systemImage: "doc.text.magnifyingglass", kind: .secondary, isEnabled: true) { viewModel.openBuildLog() }, - StepAction(id: "console", title: "Open console", + StepAction(id: "console", title: L10n.text("Open console"), systemImage: nil, kind: .quiet, isEnabled: true) { NotificationCenter.default.post(name: .nxLibraryOpenConsole, object: nil) } @@ -323,28 +323,28 @@ struct AddGameStepThreeView: View { if let path = viewModel.buildSuccessPath { return [ - StepAction(id: "play", title: "Play", + StepAction(id: "play", title: L10n.text("Play"), systemImage: "play.fill", kind: .primary, isEnabled: FileManager.default.fileExists(atPath: path)) { play(path: path) }, - StepAction(id: "library", title: "Show in library", + StepAction(id: "library", title: L10n.text("Show in library"), systemImage: "square.grid.2x2", kind: .secondary, isEnabled: true) { router.showLibrary() }, - StepAction(id: "another", title: "Port another game", + StepAction(id: "another", title: L10n.text("Port another game"), systemImage: "plus", kind: .secondary, isEnabled: true) { withAnimation(reduceMotion ? nil : SeleneTheme.standard) { viewModel.startNewAddGame() } }, - StepAction(id: "openLog", title: "Open log", + StepAction(id: "openLog", title: L10n.text("Open log"), systemImage: "doc.text.magnifyingglass", kind: .secondary, isEnabled: true) { viewModel.openBuildLog() }, - StepAction(id: "reveal", title: "Reveal in Finder", + StepAction(id: "reveal", title: L10n.text("Reveal in Finder"), systemImage: "folder", kind: .secondary, isEnabled: true) { viewModel.openOutputInFinder() } @@ -359,14 +359,14 @@ struct AddGameStepThreeView: View { } let leadsWithAction = !specs.isEmpty specs += [ - StepAction(id: "openLog", title: "Open log", + StepAction(id: "openLog", title: L10n.text("Open log"), systemImage: "doc.text.magnifyingglass", kind: .secondary, isEnabled: true) { viewModel.openBuildLog() }, ] if viewModel.buildError != nil { - specs.append(StepAction(id: "diagnostics", title: didCopyDiagnostics ? "Copied" : "Copy diagnostics", + specs.append(StepAction(id: "diagnostics", title: didCopyDiagnostics ? L10n.text("Copied") : L10n.text("Copy diagnostics"), systemImage: nil, kind: .secondary, isEnabled: true) { viewModel.copyDiagnostics() didCopyDiagnostics = true @@ -374,19 +374,19 @@ struct AddGameStepThreeView: View { }) } specs += [ - StepAction(id: "retry", title: viewModel.canRetryReadyInstall ? "Retry installation" : "Try again", + StepAction(id: "retry", title: viewModel.canRetryReadyInstall ? L10n.text("Retry installation") : L10n.text("Try again"), systemImage: "arrow.clockwise", kind: leadsWithAction ? .secondary : .primary, isEnabled: !viewModel.isBuilding) { viewModel.retryLastBuild() }, - StepAction(id: "options", title: "Back to review", + StepAction(id: "options", title: L10n.text("Back to review"), systemImage: "slider.horizontal.3", kind: .secondary, isEnabled: true) { withAnimation(reduceMotion ? nil : SeleneTheme.standard) { viewModel.goToWizardStep(2) } }, - StepAction(id: "console", title: "Open console", + StepAction(id: "console", title: L10n.text("Open console"), systemImage: nil, kind: .quiet, isEnabled: true) { NotificationCenter.default.post(name: .nxLibraryOpenConsole, object: nil) } @@ -395,7 +395,7 @@ struct AddGameStepThreeView: View { } return [ - StepAction(id: "review", title: "Back to review", + StepAction(id: "review", title: L10n.text("Back to review"), systemImage: "arrow.left", kind: .secondary, isEnabled: true) { withAnimation(reduceMotion ? nil : SeleneTheme.standard) { @@ -410,19 +410,19 @@ struct AddGameStepThreeView: View { let perform = { viewModel.performFailureAction(action, router: router) } switch action { case .portAnyway: - return StepAction(id: "portAnyway", title: "Port anyway", systemImage: nil, kind: .primary, + return StepAction(id: "portAnyway", title: L10n.text("Port anyway"), systemImage: nil, kind: .primary, isEnabled: !viewModel.isBuilding, perform: perform) case .openEngineSettings: - return StepAction(id: "engine", title: "Open Engine settings", systemImage: nil, kind: .primary, + return StepAction(id: "engine", title: L10n.text("Open Engine settings"), systemImage: nil, kind: .primary, isEnabled: true, perform: perform) case .openSetup: - return StepAction(id: "setup", title: "Open Setup", systemImage: nil, kind: .primary, + return StepAction(id: "setup", title: L10n.text("Open Setup"), systemImage: nil, kind: .primary, isEnabled: true, perform: perform) case .importKeys: - return StepAction(id: "keys", title: "Import prod.keys", systemImage: nil, kind: .primary, + return StepAction(id: "keys", title: L10n.text("Import prod.keys"), systemImage: nil, kind: .primary, isEnabled: true, perform: perform) case .openStorage: - return StepAction(id: "storage", title: "Open Storage", systemImage: nil, kind: .primary, + return StepAction(id: "storage", title: L10n.text("Open Storage"), systemImage: nil, kind: .primary, isEnabled: true, perform: perform) case .none: return nil @@ -470,7 +470,7 @@ struct PortingLogBox: View { Image(systemName: "chevron.right") .seleneFont(12, .bold) .rotationEffect(.degrees(expanded ? 90 : 0)) - Text("Log") + Text(L10n.text("Log")) .seleneFont(15, .semibold) Spacer() if let last = logs.last, !expanded { diff --git a/Sources/NXLibrary/Views/AddGame/AddGameView+Rows.swift b/Sources/NXLibrary/Views/AddGame/AddGameView+Rows.swift index daff010..bb9fc52 100644 --- a/Sources/NXLibrary/Views/AddGame/AddGameView+Rows.swift +++ b/Sources/NXLibrary/Views/AddGame/AddGameView+Rows.swift @@ -27,32 +27,32 @@ extension AddGameView { var rows: [AddGameRow] = [] let folder = viewModel.selectedGameFolder rows.append(AddGameRow(id: "chooseFiles", kind: .action { viewModel.selectGameFiles() }, - icon: .add, title: "Choose game files", about: "NSP, XCI, NSZ, XCZ or a Switch 2 NUP.")) + icon: .add, title: L10n.text("Choose game files"), about: L10n.text("NSP, XCI, NSZ, XCZ or a Switch 2 NUP."))) rows.append(AddGameRow(id: "choose", kind: .action { viewModel.selectGameFolder() }, - icon: .folder, title: folder == nil ? "Choose folder" : "Change folder", + icon: .folder, title: folder == nil ? L10n.text("Choose folder") : L10n.text("Change folder"), about: folder.map { ($0.path as NSString).abbreviatingWithTildeInPath } - ?? "Game packages or an extracted game folder. You can also drop them here.")) + ?? L10n.text("Game packages or an extracted game folder. You can also drop them here."))) if let game = viewModel.selectedGame { rows.append(AddGameRow(id: "continue", kind: .action(goToReview), icon: .chevron, - title: "Continue", + title: L10n.text("Continue"), about: "\(game.name) · \(sourceCountText) · \(viewModel.formattedSelectedSourcesSize)", enabled: viewModel.canAddSelectedGame)) } if viewModel.isScanningGameFolder { - rows.append(AddGameRow(id: "scanning", kind: .note, title: "Reading the folder…")) + rows.append(AddGameRow(id: "scanning", kind: .note, title: L10n.text("Reading the folder…"))) } if let error = viewModel.scanError { rows.append(AddGameRow(id: "scanError", kind: .warning(action: nil), - title: viewModel.detectedGames.isEmpty ? "Could not read this folder" : "Some files were skipped", about: error)) + title: viewModel.detectedGames.isEmpty ? L10n.text("Could not read this folder") : L10n.text("Some files were skipped"), about: error)) } if let error = viewModel.dropErrorMessage { rows.append(AddGameRow(id: "dropError", kind: .warning(action: nil), title: error)) } if folder != nil, !viewModel.isScanningGameFolder, viewModel.detectedGames.isEmpty, viewModel.scanError == nil { - rows.append(AddGameRow(id: "empty", kind: .note, title: "No games were found in this folder.")) + rows.append(AddGameRow(id: "empty", kind: .note, title: L10n.text("No games were found in this folder."))) } if viewModel.detectedGames.count > 1 { - rows.append(AddGameRow(id: "gamesHeading", kind: .heading, title: "Choose a game")) + rows.append(AddGameRow(id: "gamesHeading", kind: .heading, title: L10n.text("Choose a game"))) for game in viewModel.detectedGames { rows.append(AddGameRow(id: "game:\(game.id)", kind: .action { viewModel.selectedGame = game }, title: game.name, about: "\(game.summaryText) · \(game.titleId ?? game.id)", compact: true, @@ -60,24 +60,23 @@ extension AddGameView { } } if let game = viewModel.selectedGame { - rows.append(AddGameRow(id: "sourcesHeading", kind: .heading, title: "Sources")) + rows.append(AddGameRow(id: "sourcesHeading", kind: .heading, title: L10n.text("Sources"))) if !game.hasGame { - rows.append(AddGameRow(id: "noBase", kind: .warning(action: nil), title: "Base game required", - about: "This folder has an update or DLC but no base game.")) + rows.append(AddGameRow(id: "noBase", kind: .warning(action: nil), title: L10n.text("Base game required"), + about: L10n.text("This folder has an update or DLC but no base game."))) } else if game.isUpdateOnly { // The engine's NO_BASE note (nxnative 777a5f6c): nothing in the file proves the update is complete. let folder = game.files.contains { $0.layout == .extracted } rows.append(AddGameRow(id: "updateOnlyNUP", kind: .warning(action: nil), - title: folder ? "No base game in this folder" : "No base game in this NUP", - about: "The selected update is played as the whole game. If the game stops on " - + (folder ? "missing files, add the base game's dump (base/exefs and base/romfs)." - : "missing files, use a NUP that includes the base game."))) + title: folder ? L10n.text("No base game in this folder") : L10n.text("No base game in this NUP"), + about: folder ? L10n.text("The selected update is played as the whole game. If the game stops on missing files, add the base game’s dump (base/exefs and base/romfs).") + : L10n.text("The selected update is played as the whole game. If the game stops on missing files, use a NUP that includes the base game."))) } // The same note for an NSP, a NUP or a folder: the engine makes a provisional profile for each. if !viewModel.hasVerifiedEngineProfile(titleId: game.titleId) { - rows.append(AddGameRow(id: "noProfile", kind: .warning(action: nil), title: "Not tested with this engine yet", - about: "The engine has no profile for this game, so it makes a provisional one. " - + "The porting may stop, or the game may not start.")) + rows.append(AddGameRow(id: "noProfile", kind: .warning(action: nil), title: L10n.text("Not tested with this engine yet"), + about: L10n.text("The engine has no profile for this game, so it makes a provisional one. ") + + L10n.text("The porting may stop, or the game may not start."))) } for item in game.files { let locked = SourceSelection.isLocked(item, in: game.files, selected: viewModel.selectedSourceIDs) @@ -94,8 +93,8 @@ extension AddGameView { } rows.append(AddGameRow(id: "sourcesNote", kind: .note, title: game.isUpdateOnly - ? "One update at a time; bundled DLC stay together." - : "The base stays in. One update at a time; bundled DLC stay together.")) + ? L10n.text("One update at a time; bundled DLC stay together.") + : L10n.text("The base stays in. One update at a time; bundled DLC stay together."))) } rows += readinessRows(includeBuildError: false) rows.append(destinationRow) @@ -104,7 +103,7 @@ extension AddGameView { private var sourceCountText: String { let count = Set(viewModel.selectedSources.map { $0.url.resolvingSymlinksInPath().path }).count - return count == 1 ? "1 source" : "\(count) sources" + return L10n.count("%lld sources", count) } private func sourceAbout(_ item: GameFileItem) -> String { @@ -113,8 +112,8 @@ extension AddGameView { parts.append(raw.lowercased().hasPrefix("v") ? raw : "v\(raw)") } let bundled = viewModel.selectedGame?.files.contains { $0.id != item.id && $0.url == item.url } == true - if bundled && item.type != .base { parts.append(item.layout == .container ? "In the NUP file" : "In package") } - else { parts.append(item.layout == .extracted ? "Extracted folder" : item.formattedSize) } + if bundled && item.type != .base { parts.append(item.layout == .container ? L10n.text("In the NUP file") : L10n.text("In package")) } + else { parts.append(item.layout == .extracted ? L10n.text("Extracted folder") : item.formattedSize) } return parts.joined(separator: " · ") } @@ -128,16 +127,16 @@ extension AddGameView { private var reviewRows: [AddGameRow] { var rows: [AddGameRow] = [ AddGameRow(id: "build", kind: .action { addSelectedGame(buildNow: true) }, icon: .build, - title: viewModel.selectedLibraryGame == nil ? "Add and port" : "Rebuild", about: buildHint, enabled: viewModel.canStartBuild), + title: viewModel.selectedLibraryGame == nil ? L10n.text("Add and port") : L10n.text("Rebuild"), about: buildHint, enabled: viewModel.canStartBuild), AddGameRow(id: "library", kind: .action { addSelectedGame(buildNow: false) }, icon: .add, - title: viewModel.selectedLibraryGame == nil ? "Add to library only" : "Save changes", - about: viewModel.selectedLibraryGame == nil ? "Port later" : "Next launch", + title: viewModel.selectedLibraryGame == nil ? L10n.text("Add to library only") : L10n.text("Save changes"), + about: viewModel.selectedLibraryGame == nil ? L10n.text("Port later") : L10n.text("Next launch"), enabled: viewModel.canAddSelectedGame), AddGameRow(id: "sources", kind: .action { withAnimation(reduceMotion ? nil : SeleneTheme.standard) { viewModel.goToWizardStep(1) } - }, icon: .folder, title: "Change sources", + }, icon: .folder, title: L10n.text("Change sources"), about: "\(sourceCountText) · \(viewModel.formattedSelectedSourcesSize)"), - AddGameRow(id: "runtimeHeading", kind: .heading, title: "Next launch"), + AddGameRow(id: "runtimeHeading", kind: .heading, title: L10n.text("Next launch")), toggle("fps60", "60 FPS", "Frame-rate target", $viewModel.pendingRuntimeConfig.enable60FPS), toggle("docked", "Docked mode", "Console profile", @@ -146,19 +145,19 @@ extension AddGameView { $viewModel.pendingRuntimeConfig.enableMetalFXUpscale), toggle("metalHUD", "Metal HUD", "Performance overlay", $viewModel.pendingRuntimeConfig.enableMetalHUD), - AddGameRow(id: "appHeading", kind: .heading, title: "App"), + AddGameRow(id: "appHeading", kind: .heading, title: L10n.text("App")), ] let languages = languageOptions let current = languages.firstIndex { $0.code == viewModel.pendingRuntimeConfig.language } ?? 0 rows.append(AddGameRow(id: "language", kind: .choice(values: languages.map(\.label), index: current) { index in guard languages.indices.contains(index) else { return } viewModel.pendingRuntimeConfig.language = languages[index].code - }, title: "Game language", compact: true)) - rows.append(AddGameRow(id: "appName", kind: .field($viewModel.appName, placeholder: viewModel.selectedGame?.name ?? "Name"), - title: "App name", compact: true)) + }, title: L10n.text("Game language"), compact: true)) + rows.append(AddGameRow(id: "appName", kind: .field($viewModel.appName, placeholder: viewModel.selectedGame?.name ?? L10n.text("Name")), + title: L10n.text("App name"), compact: true)) if viewModel.selectedLibraryGame == nil { rows.append(AddGameRow(id: "bundleID", kind: .field($viewModel.bundleId, placeholder: "org.nxnative.game"), - title: "Bundle identifier", compact: true)) + title: L10n.text("Bundle identifier"), compact: true)) } rows += readinessRows(includeBuildError: true) rows.append(destinationRow) @@ -170,21 +169,21 @@ extension AddGameView { } private var buildHint: String { - if viewModel.canStartBuild { return "Builds the Mac app now; you can keep using the library meanwhile." } - if !viewModel.supportsSources(viewModel.selectedSources) { return "These sources need a newer engine." } - if viewModel.selectedSourcesRequireKeys && !viewModel.isProdKeysValid { return "Import prod.keys first." } + if viewModel.canStartBuild { return L10n.text("Builds the Mac app now; you can keep using the library meanwhile.") } + if !viewModel.supportsSources(viewModel.selectedSources) { return L10n.text("These sources need a newer engine.") } + if viewModel.selectedSourcesRequireKeys && !viewModel.isProdKeysValid { return L10n.text("Import prod.keys first.") } if viewModel.keyCheckBlocks, let readiness = viewModel.keyCheck?.readiness { return "\(readiness.title)." } - if !viewModel.isNxnativeValid { return "The nxnative checkout is missing." } - if viewModel.environmentStatus?.canBuild != true { return "The build environment is incomplete." } - if !viewModel.canPassToolchainGate { return "The toolchain is not certified; enable the override in Settings." } - return "Not available yet." + if !viewModel.isNxnativeValid { return L10n.text("The nxnative checkout is missing.") } + if viewModel.environmentStatus?.canBuild != true { return L10n.text("The build environment is incomplete.") } + if !viewModel.canPassToolchainGate { return L10n.text("The toolchain is not certified; enable the override in Settings.") } + return L10n.text("Not available yet.") } private var languageOptions: [(code: String, label: String)] { var options = viewModel.availableLanguages let current = viewModel.pendingRuntimeConfig.language if !options.contains(where: { $0.code == current }) { - options.append((current, current.isEmpty ? "Game default" : current)) + options.append((current, current.isEmpty ? L10n.text("Game default") : current)) } return options } @@ -203,30 +202,30 @@ extension AddGameView { rows.append(AddGameRow(id: "buildError", kind: .warning(action: { viewModel.buildError = nil viewModel.portingFailureReason = nil - }), title: viewModel.portingFailureReason?.title ?? "The last porting failed", about: error)) + }), title: viewModel.portingFailureReason?.title ?? L10n.text("The last porting failed"), about: error)) } if viewModel.selectedSourcesRequireKeys && !viewModel.isProdKeysValid { rows.append(AddGameRow(id: "keys", kind: .action { viewModel.selectProdKeysFile() }, icon: .key, - title: "Import prod.keys", - about: "Copied once to Application Support, readable only by you.")) + title: L10n.text("Import prod.keys"), + about: L10n.text("Copied once to Application Support, readable only by you."))) } if viewModel.isProdKeysValid, let readiness = viewModel.keyCheck?.readiness { if readiness.warning { rows.append(AddGameRow(id: "keyCheck", kind: .warning(action: nil), title: readiness.title, about: readiness.about)) rows.append(AddGameRow(id: "keys", kind: .action { viewModel.selectProdKeysFile() }, icon: .key, - title: "Import prod.keys", - about: "Copied once to Application Support, readable only by you.")) + title: L10n.text("Import prod.keys"), + about: L10n.text("Copied once to Application Support, readable only by you."))) } else { rows.append(AddGameRow(id: "keyCheck", kind: .note, title: "\(readiness.title). \(readiness.about)")) } } if let environment = viewModel.environmentStatus, environment.xcodeVersion == nil { rows.append(AddGameRow(id: "xcode", kind: .action { viewModel.reopenSetup() }, icon: .checklist, - title: "Open Setup", about: "Xcode is not ready; a porting needs it.")) + title: L10n.text("Open Setup"), about: L10n.text("Xcode is not ready; a porting needs it."))) } if viewModel.isDiskSpaceLow { - rows.append(AddGameRow(id: "disk", kind: .warning(action: nil), title: "Not enough disk space", + rows.append(AddGameRow(id: "disk", kind: .warning(action: nil), title: L10n.text("Not enough disk space"), about: viewModel.diskSpaceWarningMessage)) } return rows @@ -234,10 +233,10 @@ extension AddGameView { private var destinationRow: AddGameRow { if let problem = viewModel.buildDestinationProblem { - return AddGameRow(id: "destination", kind: .warning(action: nil), title: "App folder unavailable", about: problem) + return AddGameRow(id: "destination", kind: .warning(action: nil), title: L10n.text("App folder unavailable"), about: problem) } return AddGameRow(id: "destination", kind: .note, - title: "Installs to \(viewModel.selectedBuildAppURL.path). On a failure or a cancel the previous app stays as it was.") + title: L10n.format("Installs to %@. On a failure or a cancel the previous app stays as it was.", [String(describing: (viewModel.selectedBuildAppURL.path))])) } // MARK: - Porting diff --git a/Sources/NXLibrary/Views/AddGame/AddGameView.swift b/Sources/NXLibrary/Views/AddGame/AddGameView.swift index 1ae134d..a38d6a2 100644 --- a/Sources/NXLibrary/Views/AddGame/AddGameView.swift +++ b/Sources/NXLibrary/Views/AddGame/AddGameView.swift @@ -61,10 +61,10 @@ public struct AddGameView: View { if confirmDiscard { ZStack { SeleneTheme.scrim45.ignoresSafeArea() - SeleneConfirmDialog(title: "Discard selected sources?", - message: "The files on disk are not changed.", - confirmTitle: "Discard", - cancelTitle: "Keep editing", + SeleneConfirmDialog(title: L10n.text("Discard selected sources?"), + message: L10n.text("The files on disk are not changed."), + confirmTitle: L10n.text("Discard"), + cancelTitle: L10n.text("Keep editing"), focus: $confirmFocus, onCancel: dismissDiscard, onConfirm: discardAndClose) @@ -99,7 +99,7 @@ public struct AddGameView: View { private func column(_ rows: [AddGameRow], focused: AddGameRow?) -> some View { VStack(alignment: .leading, spacing: 0) { - Text("ADD A GAME") + Text(L10n.text("ADD A GAME")) .seleneFont(15, .semibold) .tracking(1.2) .foregroundColor(SeleneTheme.ink60) @@ -159,7 +159,7 @@ public struct AddGameView: View { .padding(.bottom, canvas.u(24)) .overlay(alignment: .top) { if dropTargeted { - Text("Drop to add these files") + Text(L10n.text("Drop to add these files")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.onFocus) .padding(.horizontal, canvas.u(18)) @@ -177,7 +177,7 @@ public struct AddGameView: View { private var stepTitle: String { switch viewModel.buildWizardStep { case 1: return "Choose a game" - case 2: return viewModel.selectedGame?.name ?? "Review" + case 2: return viewModel.selectedGame?.name ?? L10n.text("Review") default: if !viewModel.isBuilding, viewModel.buildError != nil, let reason = viewModel.portingFailureReason { return reason.title @@ -191,7 +191,7 @@ public struct AddGameView: View { private var stepLine: some View { let step = viewModel.buildWizardStep return HStack(spacing: canvas.u(10)) { - ForEach(Array(["Choose", "Review", "Porting"].enumerated()), id: \.offset) { index, title in + ForEach(Array(["Choose", L10n.text("Review"), "Porting"].enumerated()), id: \.offset) { index, title in if index > 0 { Circle().fill(SeleneTheme.ink25).frame(width: canvas.u(4), height: canvas.u(4)) } @@ -201,7 +201,7 @@ public struct AddGameView: View { } } .accessibilityElement(children: .ignore) - .accessibilityLabel("Step \(step) of 3") + .accessibilityLabel(L10n.format("Step %@ of 3", [String(describing: (step))])) } // MARK: - Focus @@ -293,7 +293,7 @@ public struct AddGameView: View { switch row.kind { case .toggle: return "Switch" case .choice: return "Change" - case .field: return editing == row.id ? "Done" : "Edit" + case .field: return editing == row.id ? "Done" : L10n.text("Edit") case .action: return row.title default: return "Select" } diff --git a/Sources/NXLibrary/Views/Collection/CollectionView.swift b/Sources/NXLibrary/Views/Collection/CollectionView.swift index 2127524..303ab99 100644 --- a/Sources/NXLibrary/Views/Collection/CollectionView.swift +++ b/Sources/NXLibrary/Views/Collection/CollectionView.swift @@ -135,19 +135,19 @@ public struct CollectionView: View { if viewModel.isScanningLibrary { EmptyView() } else if hasAnyGame { - Text("No games in \(filter.rawValue)") + Text(L10n.format("No games in %@", [L10n.text(filter.rawValue)])) .seleneFont(34) .foregroundColor(SeleneTheme.ink) .shadow(color: .black.opacity(0.45), radius: canvas.u(6)) .allowsHitTesting(false) } else { VStack(spacing: canvas.u(14)) { - Text("Add your first game").seleneFont(34).foregroundColor(SeleneTheme.ink) - Text("Choose game files or find your existing apps.") + Text(L10n.text("Add your first game")).seleneFont(34).foregroundColor(SeleneTheme.ink) + Text(L10n.text("Choose game files or find your existing apps.")) .seleneFont(19).foregroundColor(SeleneTheme.ink60) HStack(spacing: canvas.u(14)) { - SelenePillButton(title: "Add game", focused: true) { router.openAddGame() } - SelenePillButton(title: "Import existing builds", minWidth: 0, focused: false) { + SelenePillButton(title: L10n.text("Add game"), focused: true) { router.openAddGame() } + SelenePillButton(title: L10n.text("Import existing builds"), minWidth: 0, focused: false) { viewModel.importExistingBuilds() } } @@ -162,13 +162,13 @@ public struct CollectionView: View { private func contextHints(_ game: PortedGame?) -> [HintItem] { guard let game else { if viewModel.isScanningLibrary { return [] } - return hasAnyGame ? [HintItem(.rs, "Filter")] : [HintItem(.a, "Add game")] + return hasAnyGame ? [HintItem(.rs, L10n.text("Filter"))] : [HintItem(.a, L10n.text("Add game"))] } - let filterHint = HintItem(.rs, "Filter") + let filterHint = HintItem(.rs, L10n.text("Filter")) if game.isPlayableLocally { - return [HintItem(.a, "Play"), HintItem(.x, "Details"), HintItem(.y, "Game options"), filterHint] + return [HintItem(.a, L10n.text("Play")), HintItem(.x, L10n.text("Details")), HintItem(.y, L10n.text("Game options")), filterHint] } - return [HintItem(.a, "Details"), HintItem(.y, "Game options"), filterHint] + return [HintItem(.a, L10n.text("Details")), HintItem(.y, L10n.text("Game options")), filterHint] } // MARK: - Selection @@ -253,7 +253,7 @@ struct CollectionInfoView: View { .foregroundColor(SeleneTheme.ink60) .padding(.top, canvas.u(6)) } else if !game.isPlayableLocally, game.status != .building, game.portingFailure == nil { - Text("Not built yet") + Text(L10n.text("Not built yet")) .seleneFont(17) .foregroundColor(SeleneTheme.ink75) .padding(.top, canvas.u(20)) diff --git a/Sources/NXLibrary/Views/Components/AppUpdateDialog.swift b/Sources/NXLibrary/Views/Components/AppUpdateDialog.swift index 4c76840..0650173 100644 --- a/Sources/NXLibrary/Views/Components/AppUpdateDialog.swift +++ b/Sources/NXLibrary/Views/Components/AppUpdateDialog.swift @@ -9,55 +9,55 @@ struct AppUpdateDialog: View { private var primaryTitle: String { switch updater.state { - case .available where updater.canInstallRelease: return "Update" - case .failed: return "Retry" + case .available where updater.canInstallRelease: return L10n.text("Update") + case .failed: return L10n.text("Retry") default: return "Close" } } private var title: String { switch updater.state { - case .checking: return "Checking for updates" - case .upToDate: return "You’re up to date" - case .available(let release): return "NX Library \(release.version)" - case .downloading: return "Downloading update" - case .verifying: return "Verifying update" - case .installing: return "Installing update" - case .failed: return "Update unavailable" - default: return "App updates" + case .checking: return L10n.text("Checking for updates") + case .upToDate: return L10n.text("You’re up to date") + case .available(let release): return L10n.format("NX Library %@", [String(describing: (release.version))]) + case .downloading: return L10n.text("Downloading update") + case .verifying: return L10n.text("Verifying update") + case .installing: return L10n.text("Installing update") + case .failed: return L10n.text("Update unavailable") + default: return L10n.text("App updates") } } private var message: String { switch updater.state { - case .checking: return "Looking for a new release…" - case .upToDate: return "You have NX Library \(updater.currentVersion). No newer release is available." + case .checking: return L10n.text("Looking for a new release…") + case .upToDate: return L10n.format("You have NX Library %@. No newer release is available.", [String(describing: (updater.currentVersion))]) case .available: - var lines = ["You have \(updater.currentVersion)."] + var lines = [L10n.format("You have %@.", [String(describing: (updater.currentVersion))])] if updater.installation == .developer { - lines.append("This development copy can check releases. Install a packaged release to use automatic updates.") + lines.append(L10n.text("This development copy can check releases. Install a packaged release to use automatic updates.")) } else if updater.installation == .readOnly { - lines.append("Move NX Library to Applications before updating.") + lines.append(L10n.text("Move NX Library to Applications before updating.")) } else if let reason = blockedReason { - lines.append("The update can install \(reason.prefix(1).lowercased() + reason.dropFirst()).") - } else { lines.append("NX Library restarts to finish.") } + lines.append(L10n.text(reason)) + } else { lines.append(L10n.text("NX Library restarts to finish.")) } return (lines + requirementNotes).joined(separator: "\n\n") - case .downloading(let progress): return "\(Int((progress * 100).rounded()))% · Download continues if you close this dialog." - case .verifying: return "Checking the download before installing." - case .installing: return "NX Library will reopen automatically." + case .downloading(let progress): return L10n.format("%@%% · Download continues if you close this dialog.", [String(describing: (Int((progress * 100).rounded())))]) + case .verifying: return L10n.text("Checking the download before installing.") + case .installing: return L10n.text("NX Library will reopen automatically.") case .failed(let message): return message - case .developer: return "This development copy can check releases but cannot replace itself." - case .readOnly: return "Move NX Library to Applications before updating." - default: return "Updates are currently unavailable." + case .developer: return L10n.text("This development copy can check releases but cannot replace itself.") + case .readOnly: return L10n.text("Move NX Library to Applications before updating.") + default: return L10n.text("Updates are currently unavailable.") } } var body: some View { SeleneConfirmDialog(title: title, message: message, - confirmTitle: primaryTitle, cancelTitle: updater.dialogHasSecondaryButton ? "Close" : nil, - confirmEnabled: primaryTitle != "Update" || blockedReason == nil, + confirmTitle: primaryTitle, cancelTitle: updater.dialogHasSecondaryButton ? L10n.text("Close") : nil, + confirmEnabled: primaryTitle != L10n.text("Update") || blockedReason == nil, focus: $updater.offerFocus, onCancel: updater.later, onConfirm: updater.confirmDialog) { if case .checking = updater.state { ProgressView().padding(.top, 16) } if let release = updater.dialogRelease, !release.notes.isEmpty { - Text("Release notes · \(release.version)") + Text(L10n.format("Release notes · %@", [String(describing: (release.version))])) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.ink60) .padding(.top, 18) diff --git a/Sources/NXLibrary/Views/Components/BuildLiveLogsDrawer.swift b/Sources/NXLibrary/Views/Components/BuildLiveLogsDrawer.swift index 934f7f1..3d3c92a 100644 --- a/Sources/NXLibrary/Views/Components/BuildLiveLogsDrawer.swift +++ b/Sources/NXLibrary/Views/Components/BuildLiveLogsDrawer.swift @@ -1,3 +1,4 @@ +import NXLibraryCore import SwiftUI public struct BuildLiveLogsDrawer: View { @@ -10,11 +11,11 @@ public struct BuildLiveLogsDrawer: View { public var body: some View { VStack(alignment: .leading, spacing: 10) { HStack { - Label("Live output", systemImage: "terminal") + Label(L10n.text("Live output"), systemImage: L10n.text("terminal")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink90) Spacer() - Text("\(viewModel.logs.count) lines") + Text(L10n.format("%@ lines", [String(describing: (viewModel.logs.count))])) .seleneFont(13, .regular, design: .monospaced) .foregroundColor(SeleneTheme.ink60) } @@ -23,7 +24,7 @@ public struct BuildLiveLogsDrawer: View { ScrollView { LazyVStack(alignment: .leading, spacing: 3) { if viewModel.logs.isEmpty { - Text("Waiting for build output…") + Text(L10n.text("Waiting for build output…")) .seleneFont(13, .regular, design: .monospaced) .foregroundColor(SeleneTheme.ink60) } diff --git a/Sources/NXLibrary/Views/Components/BuildStageStepperView.swift b/Sources/NXLibrary/Views/Components/BuildStageStepperView.swift index 80d85bf..1766cb5 100644 --- a/Sources/NXLibrary/Views/Components/BuildStageStepperView.swift +++ b/Sources/NXLibrary/Views/Components/BuildStageStepperView.swift @@ -1,3 +1,4 @@ +import NXLibraryCore import SwiftUI public struct BuildStageStepperView: View { @@ -10,7 +11,7 @@ public struct BuildStageStepperView: View { public var body: some View { VStack(alignment: .leading, spacing: 10) { HStack { - Text("Build stages") + Text(L10n.text("Build stages")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink90) Spacer() @@ -61,7 +62,7 @@ public struct BuildStageStepperView: View { } .frame(width: 16) - Text("Stage \(stage.id): \(stage.name)") + Text(L10n.format("Stage %@: %@", [String(describing: (stage.id)), String(describing: (stage.name))])) .seleneFont(15, stage.isRunning ? .semibold : .regular) .foregroundColor(stage.isRunning ? SeleneTheme.ink : (stage.isCompleted ? SeleneTheme.ink90 : SeleneTheme.ink60)) diff --git a/Sources/NXLibrary/Views/Components/DropZoneView.swift b/Sources/NXLibrary/Views/Components/DropZoneView.swift index bad75e6..b388bb0 100644 --- a/Sources/NXLibrary/Views/Components/DropZoneView.swift +++ b/Sources/NXLibrary/Views/Components/DropZoneView.swift @@ -1,3 +1,4 @@ +import NXLibraryCore import SwiftUI import UniformTypeIdentifiers @@ -28,10 +29,10 @@ public struct DropZoneView: View { } VStack(spacing: 4) { - Text(isTargeted ? "Drop to scan this folder" : "Drop your game files here") + Text(isTargeted ? L10n.text("Drop to scan this folder") : L10n.text("Drop your game files here")) .seleneFont(19, .semibold) .foregroundColor(SeleneTheme.ink) - Text("The base game is required. Updates and DLC are optional.") + Text(L10n.text("The base game is required. Updates and DLC are optional.")) .seleneFont(14) .foregroundColor(SeleneTheme.ink60) .multilineTextAlignment(.center) @@ -43,7 +44,7 @@ public struct DropZoneView: View { HStack(spacing: 7) { Image(systemName: "folder.fill") .seleneFont(13, .semibold) - Text("Choose folder") + Text(L10n.text("Choose folder")) .seleneFont(15, .semibold) } .foregroundColor(SeleneTheme.scrim) @@ -53,9 +54,9 @@ public struct DropZoneView: View { .clipShape(Capsule()) } .buttonStyle(.plain) - .accessibilityLabel("Choose a game folder") + .accessibilityLabel(L10n.text("Choose a game folder")) - Text("NSP · XCI · NSZ · folders") + Text(L10n.text("NSP · XCI · NSZ · folders")) .seleneFont(12, .semibold, design: .monospaced) .tracking(0.4) .foregroundColor(SeleneTheme.ink60.opacity(0.8)) @@ -71,11 +72,11 @@ public struct DropZoneView: View { .lineLimit(1) .truncationMode(.middle) Spacer(minLength: 8) - Button("Change…") { viewModel.selectGameFolder() } + Button(L10n.text("Change…")) { viewModel.selectGameFolder() } .buttonStyle(.plain) .seleneFont(13, .semibold) .foregroundColor(SeleneTheme.glyph) - Button("Clear") { viewModel.selectedGameFolder = nil } + Button(L10n.text("Clear")) { viewModel.selectedGameFolder = nil } .buttonStyle(.plain) .seleneFont(13, .semibold) .foregroundColor(SeleneTheme.ink60) diff --git a/Sources/NXLibrary/Views/Components/ScreenshotViewer.swift b/Sources/NXLibrary/Views/Components/ScreenshotViewer.swift index 9f52046..c4f662b 100644 --- a/Sources/NXLibrary/Views/Components/ScreenshotViewer.swift +++ b/Sources/NXLibrary/Views/Components/ScreenshotViewer.swift @@ -61,7 +61,7 @@ struct ScreenshotViewer: View { } VStack(spacing: 0) { Spacer(minLength: 0) - SeleneHintsBar(left: [HintItem(.b, "Back")], + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"))], right: [HintItem(.lb, "‹"), HintItem(.rb, "›")], bottom: 40, leftInset: 18) } @@ -76,7 +76,7 @@ struct ScreenshotViewer: View { for neighbour in [current + 1, current - 1] { await load(neighbour) } } .accessibilityElement(children: .contain) - .accessibilityLabel("Screenshot \(state.label)") + .accessibilityLabel(L10n.format("Screenshot %@", [String(describing: (state.label))])) } @MainActor diff --git a/Sources/NXLibrary/Views/Components/SeleneConfirmDialog.swift b/Sources/NXLibrary/Views/Components/SeleneConfirmDialog.swift index 17ee615..07da626 100644 --- a/Sources/NXLibrary/Views/Components/SeleneConfirmDialog.swift +++ b/Sources/NXLibrary/Views/Components/SeleneConfirmDialog.swift @@ -26,7 +26,7 @@ struct SeleneConfirmDialog<Detail: View>: View { var body: some View { VStack(spacing: 0) { - Text(title) + Text(L10n.text(title)) .seleneFont(34, .semibold) .foregroundColor(SeleneTheme.ink) .multilineTextAlignment(.center) @@ -151,7 +151,7 @@ struct SeleneSuccessNotice: View { .foregroundColor(SeleneTheme.ink75) .accessibilityHidden(true) VStack(alignment: .leading, spacing: canvas.u(6)) { - Text(title) + Text(L10n.text(title)) .seleneFont(28, .semibold) .foregroundColor(SeleneTheme.ink) if let message { @@ -205,7 +205,7 @@ struct SeleneChoiceDialog: View { var body: some View { VStack(spacing: 0) { - Text(title) + Text(L10n.text(title)) .seleneFont(34, .semibold) .foregroundColor(SeleneTheme.ink) .fixedSize(horizontal: false, vertical: true) diff --git a/Sources/NXLibrary/Views/Components/SeleneHintsBar.swift b/Sources/NXLibrary/Views/Components/SeleneHintsBar.swift index 39b4eb7..6b80adc 100644 --- a/Sources/NXLibrary/Views/Components/SeleneHintsBar.swift +++ b/Sources/NXLibrary/Views/Components/SeleneHintsBar.swift @@ -34,6 +34,7 @@ extension ConnectedPad { } struct SeleneHintsBar: View { + @Environment(\.nxAppLocale) private var localization @Environment(\.seleneCanvas) private var canvas @Environment(\.seleneShowButtonHints) private var showHints @@ -98,8 +99,8 @@ struct SeleneHintsBar: View { } VStack(alignment: .leading, spacing: 0) { ZStack(alignment: .leading) { - ForEach(reserve, id: \.self) { Text($0).seleneFont(19).hidden() } - Text(item.label) + ForEach(reserve, id: \.self) { Text(L10n.text($0, context: localization)).seleneFont(19).hidden() } + Text(L10n.text(item.label, context: localization)) .seleneFont(19) .foregroundColor(SeleneTheme.ink90) } diff --git a/Sources/NXLibrary/Views/Components/SeleneHomeChrome.swift b/Sources/NXLibrary/Views/Components/SeleneHomeChrome.swift index 0fed8ea..ab6d872 100644 --- a/Sources/NXLibrary/Views/Components/SeleneHomeChrome.swift +++ b/Sources/NXLibrary/Views/Components/SeleneHomeChrome.swift @@ -41,8 +41,8 @@ struct SeleneHomeChrome<Backdrop: View, Stage: View>: View { extension HintItem { /// Both home screens' left hints: the other screen (Hold: Search) and the Menu. static func homeGlobals(other: String, onOther: (() -> Void)? = nil, onMenu: (() -> Void)? = nil) -> [HintItem] { - [HintItem(.view, other, glyph: .spotlight, hold: "Hold: Search", action: onOther), - HintItem(.menu, "Menu", glyph: .menu, hold: "Hold: Settings", action: onMenu)] + [HintItem(.view, other, glyph: .spotlight, hold: L10n.text("Hold: Search"), action: onOther), + HintItem(.menu, L10n.text("Menu"), glyph: .menu, hold: L10n.text("Hold: Settings"), action: onMenu)] } } diff --git a/Sources/NXLibrary/Views/Components/SelenePillButton.swift b/Sources/NXLibrary/Views/Components/SelenePillButton.swift index f0144e0..bcde129 100644 --- a/Sources/NXLibrary/Views/Components/SelenePillButton.swift +++ b/Sources/NXLibrary/Views/Components/SelenePillButton.swift @@ -28,7 +28,7 @@ struct SelenePillButton: View { MaterialIconView(icon, size: iconSize) } if let title { - Text(title).seleneFont(21) + Text(L10n.text(title)).seleneFont(21) } if waveDots { WaveDots() diff --git a/Sources/NXLibrary/Views/Components/SeleneSettingsRow.swift b/Sources/NXLibrary/Views/Components/SeleneSettingsRow.swift index b82ea88..727491b 100644 --- a/Sources/NXLibrary/Views/Components/SeleneSettingsRow.swift +++ b/Sources/NXLibrary/Views/Components/SeleneSettingsRow.swift @@ -4,6 +4,7 @@ import NXLibraryCore /// An info button: a click shows `text` beside it. Pass `isShowing` for a screen that also opens it from the /// keyboard or a controller. public struct SeleneInfoTip: View { + @Environment(\.nxAppLocale) private var localization public let text: String private let shown: Binding<Bool>? @State private var shownHere = false @@ -28,7 +29,7 @@ public struct SeleneInfoTip: View { .buttonStyle(.plain) .popover(isPresented: isShowing, arrowEdge: .trailing) { VStack(alignment: .leading, spacing: 6) { - Text(text) + Text(L10n.text(text, context: localization)) .seleneFont(15) .foregroundColor(SeleneTheme.ink.opacity(0.96)) .lineSpacing(2.5) @@ -38,34 +39,37 @@ public struct SeleneInfoTip: View { .frame(width: 290, alignment: .leading) .background(SeleneTheme.glassSolid) } - .help(text) - .accessibilityLabel("Information") - .accessibilityHint(text) + .help(L10n.text(text, context: localization)) + .accessibilityLabel(L10n.text("Information", context: localization)) + .accessibilityHint(L10n.text(text, context: localization)) } } public struct SeleneRow<Trailing: View>: View { + @Environment(\.nxAppLocale) private var localization public let rowID: String public let title: String public let subtitle: String? public let info: String? + public let verbatimTitle: Bool @ViewBuilder public var trailing: () -> Trailing @Environment(\.seleneFocusedRowID) private var focusedRowID - public init(_ title: String, id: String? = nil, subtitle: String? = nil, info: String? = nil, + public init(_ title: String, id: String? = nil, subtitle: String? = nil, info: String? = nil, verbatimTitle: Bool = false, @ViewBuilder trailing: @escaping () -> Trailing) { self.rowID = id ?? title self.title = title self.subtitle = subtitle self.info = info + self.verbatimTitle = verbatimTitle self.trailing = trailing } private var isFocused: Bool { focusedRowID == rowID } public var body: some View { - SeleneOptionRow(icon: nil, title: title, about: subtitle, focused: isFocused) { + SeleneOptionRow(icon: nil, title: verbatimTitle ? title : L10n.text(title, context: localization), about: subtitle.map { L10n.text($0, context: localization) }, focused: isFocused) { HStack(spacing: 10) { if let info { SeleneInfoTip(info) @@ -129,7 +133,7 @@ public struct SeleneValueRow: View { HStack(spacing: 10) { SeleneValueChip(text: value) if let actionTitle, let action { - Button(actionTitle, action: action) + Button(L10n.text(actionTitle), action: action) .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.glyph) @@ -145,21 +149,23 @@ public struct SeleneButtonRow: View { public let subtitle: String? public let info: String? public let buttonTitle: String + public let verbatimTitle: Bool public let action: () -> Void - public init(_ title: String, id: String? = nil, subtitle: String? = nil, info: String? = nil, + public init(_ title: String, id: String? = nil, subtitle: String? = nil, info: String? = nil, verbatimTitle: Bool = false, buttonTitle: String, action: @escaping () -> Void) { self.rowID = id ?? title self.title = title self.subtitle = subtitle self.info = info + self.verbatimTitle = verbatimTitle self.buttonTitle = buttonTitle self.action = action } public var body: some View { - SeleneRow(title, id: rowID, subtitle: subtitle, info: info) { - Button(buttonTitle, action: action) + SeleneRow(title, id: rowID, subtitle: subtitle, info: info, verbatimTitle: verbatimTitle) { + Button(L10n.text(buttonTitle), action: action) .buttonStyle(.plain) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.glyph) @@ -202,6 +208,7 @@ public struct SeleneSegmented<Value: Hashable>: View { } public struct SelenePanel<Content: View>: View { + @Environment(\.nxAppLocale) private var localization public let title: String? public let info: String? @ViewBuilder public var content: () -> Content @@ -216,7 +223,7 @@ public struct SelenePanel<Content: View>: View { VStack(alignment: .leading, spacing: 12) { if let title { HStack(spacing: 6) { - Text(title) + Text(L10n.text(title, context: localization)) .seleneFont(12, .bold) .tracking(1.4) .textCase(.uppercase) @@ -260,7 +267,7 @@ public struct SeleneCycleRow: View { SeleneRow(title, subtitle: subtitle, info: info) { HStack(spacing: 6) { arrow(.chevronLeft, -1) - SeleneValueChip(text: value) + SeleneValueChip(text: L10n.text(value)) arrow(.chevron, 1) } } @@ -273,7 +280,7 @@ public struct SeleneCycleRow: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .accessibilityLabel(delta < 0 ? "Previous \(title)" : "Next \(title)") + .accessibilityLabel(delta < 0 ? L10n.format("Previous %@", [String(describing: (title))]) : L10n.format("Next %@", [String(describing: (title))])) } } diff --git a/Sources/NXLibrary/Views/Components/SeleneStatsRow.swift b/Sources/NXLibrary/Views/Components/SeleneStatsRow.swift index c00c3ff..94a94d2 100644 --- a/Sources/NXLibrary/Views/Components/SeleneStatsRow.swift +++ b/Sources/NXLibrary/Views/Components/SeleneStatsRow.swift @@ -17,7 +17,7 @@ struct SeleneStat: Identifiable { SeleneStat(label: label, value: .text(value)) } - static func platform(_ console: String) -> SeleneStat { SeleneStat(label: "PLATFORM", value: .platform(console)) } + static func platform(_ console: String) -> SeleneStat { SeleneStat(label: L10n.text("PLATFORM"), value: .platform(console)) } } /// Selene's at-a-glance stats line, with a 1u rule between items. Spotlight and @@ -33,12 +33,12 @@ struct SeleneStatsRow: View { /// PLAYTIME · LAST PLAYED · PLATFORM · RELEASED (Spotlight, Collection). init(presentation: LibraryPresentation) { var items = [ - SeleneStat.text("PLAYTIME", presentation.playtime), - SeleneStat.text("LAST PLAYED", presentation.lastPlayed), + SeleneStat.text(L10n.text("PLAYTIME"), presentation.playtime), + SeleneStat.text(L10n.text("LAST PLAYED"), presentation.lastPlayed), SeleneStat.platform(presentation.console) ] if let year = presentation.releaseYear { - items.append(SeleneStat.text("RELEASED", year)) + items.append(SeleneStat.text(L10n.text("RELEASED"), year)) } self.items = items } @@ -63,7 +63,7 @@ struct SeleneStatsRow: View { private func stat(_ item: SeleneStat) -> some View { VStack(alignment: .leading, spacing: canvas.u(2)) { // Line boxes of Selene's Segoe UI 12 / 21 (16 and 28), so the strip lands 34 below. - Text(item.label) + Text(L10n.text(item.label)) .seleneFont(12, .semibold) .foregroundColor(SeleneTheme.ink60) .frame(height: canvas.u(16), alignment: .leading) diff --git a/Sources/NXLibrary/Views/Components/SeleneSwitch.swift b/Sources/NXLibrary/Views/Components/SeleneSwitch.swift index 6535a01..bfe9959 100644 --- a/Sources/NXLibrary/Views/Components/SeleneSwitch.swift +++ b/Sources/NXLibrary/Views/Components/SeleneSwitch.swift @@ -22,7 +22,7 @@ struct SeleneSwitch: View { } .buttonStyle(.plain) .animation(SeleneTheme.quick, value: isOn) - .accessibilityValue(isOn ? "On" : "Off") + .accessibilityValue(isOn ? L10n.text("On") : L10n.text("Off")) .accessibilityAddTraits(.isButton) } } diff --git a/Sources/NXLibrary/Views/Components/SessionCard.swift b/Sources/NXLibrary/Views/Components/SessionCard.swift index 899e202..6e2a1ce 100644 --- a/Sources/NXLibrary/Views/Components/SessionCard.swift +++ b/Sources/NXLibrary/Views/Components/SessionCard.swift @@ -77,7 +77,7 @@ struct SessionCardView: View { } HStack(spacing: canvas.u(16)) { VStack(alignment: .leading, spacing: 0) { - Text("Session ended") + Text(L10n.text("Session ended")) .seleneFont(15) .foregroundColor(SeleneTheme.ink.opacity(0.62)) Text(summary.gameName) @@ -88,11 +88,11 @@ struct SessionCardView: View { .padding(.top, canvas.u(2)) .padding(.bottom, canvas.u(8)) HStack(alignment: .lastTextBaseline, spacing: canvas.u(12)) { - figure(summary.thisSession, "this session") + figure(summary.thisSession, L10n.text("this session")) Text("·") .seleneFont(18) .foregroundColor(SeleneTheme.ink.opacity(0.45)) - figure(summary.total, "total") + figure(summary.total, L10n.text("total")) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -119,7 +119,7 @@ struct SessionCardView: View { .shadow(color: .black.opacity(0.45), radius: canvas.u(18)) .environment(\.seleneMinimumFont, 12) .accessibilityElement(children: .combine) - .accessibilityLabel("Session ended, \(summary.gameName): \(summary.thisSession.replacingOccurrences(of: "<1m", with: "under a minute")) this session, \(summary.total.replacingOccurrences(of: "<1m", with: "under a minute")) total") + .accessibilityLabel(L10n.format("Session ended, %@: %@ this session, %@ total", [String(describing: (summary.gameName)), String(describing: (summary.thisSession.replacingOccurrences(of: "<1m", with: L10n.text("under a minute")))), String(describing: (summary.total.replacingOccurrences(of: "<1m", with: L10n.text("under a minute"))))])) .task(id: summary.id) { await loadArt() } } @@ -128,7 +128,7 @@ struct SessionCardView: View { Text(value) .seleneFont(20, .semibold) .foregroundColor(SeleneTheme.ink) - Text(label) + Text(L10n.text(label)) .seleneFont(15) .foregroundColor(SeleneTheme.ink.opacity(0.6)) } diff --git a/Sources/NXLibrary/Views/Components/SteamGridDBKeyDialog.swift b/Sources/NXLibrary/Views/Components/SteamGridDBKeyDialog.swift index 38fc8d4..2d60dd9 100644 --- a/Sources/NXLibrary/Views/Components/SteamGridDBKeyDialog.swift +++ b/Sources/NXLibrary/Views/Components/SteamGridDBKeyDialog.swift @@ -23,13 +23,13 @@ struct SteamGridDBKeyDialog: View { ZStack { SeleneTheme.scrim45.ignoresSafeArea().contentShape(Rectangle()) .onTapGesture { close(saved: false) } - SeleneConfirmDialog(title: "SteamGridDB key", - message: "Paste the API key from Preferences → API in your SteamGridDB profile. It stays on this Mac.", - confirmTitle: checking ? "Checking…" : "Save", + SeleneConfirmDialog(title: L10n.text("SteamGridDB key"), + message: L10n.text("Paste the API key from Preferences → API in your SteamGridDB profile. It stays on this Mac."), + confirmTitle: checking ? "Checking…" : L10n.text("Save"), confirmEnabled: !checking && SteamGridDBKey.normalized(draft) != nil, focus: $focus, onCancel: { close(saved: false) }, onConfirm: save) { VStack(alignment: .leading, spacing: canvas.u(12)) { - TextField("API key", text: $draft) + TextField(L10n.text("API key"), text: $draft) .textFieldStyle(.plain) .font(.system(size: max(13, canvas.u(17)), design: .monospaced)) .focused($fieldFocused) @@ -37,11 +37,11 @@ struct SteamGridDBKeyDialog: View { .onExitCommand { close(saved: false) } .disabled(checking) HStack(spacing: canvas.u(6)) { - Text(message ?? "Free with a SteamGridDB account.") + Text(message ?? L10n.text("Free with a SteamGridDB account.")) .foregroundColor(message == nil ? SeleneTheme.ink60 : SeleneTheme.danger) .fixedSize(horizontal: false, vertical: true) Spacer(minLength: 0) - Button("Get a key") { NSWorkspace.shared.open(SteamGridDBArt.keyPage) } + Button(L10n.text("Get a key")) { NSWorkspace.shared.open(SteamGridDBArt.keyPage) } .buttonStyle(.plain) .foregroundColor(SeleneTheme.focus) } diff --git a/Sources/NXLibrary/Views/Console/ConsoleView.swift b/Sources/NXLibrary/Views/Console/ConsoleView.swift index f521c84..7539ed8 100644 --- a/Sources/NXLibrary/Views/Console/ConsoleView.swift +++ b/Sources/NXLibrary/Views/Console/ConsoleView.swift @@ -12,10 +12,10 @@ enum ConsoleFilter: String, CaseIterable, Identifiable { var title: String { switch self { - case .all: return "All" - case .errors: return "Errors" + case .all: return L10n.text("All") + case .errors: return L10n.text("Errors") case .build: return "Build" - case .launch: return "Launch" + case .launch: return L10n.text("Launch") } } } @@ -103,7 +103,7 @@ public struct ConsoleView: View { VStack(spacing: 0) { column(logs) if showsHints { - SeleneHintsBar(left: [HintItem(.b, searching ? "Done" : "Back")], + SeleneHintsBar(left: [HintItem(.b, searching ? L10n.text("Done") : L10n.text("Back"))], right: [HintItem(.a, aLabel)], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) .opacity(confirmClear ? 0 : 1) @@ -113,10 +113,10 @@ public struct ConsoleView: View { if confirmClear { ZStack { SeleneTheme.scrim45.ignoresSafeArea() - SeleneConfirmDialog(title: "Clear the console?", - message: "The log file on disk is kept; only the lines shown here go.", + SeleneConfirmDialog(title: L10n.text("Clear the console?"), + message: L10n.text("The log file on disk is kept; only the lines shown here go."), confirmTitle: "Clear", - cancelTitle: "Keep", + cancelTitle: L10n.text("Keep"), focus: $confirmFocus, onCancel: dismissClear, onConfirm: clear) @@ -137,13 +137,13 @@ public struct ConsoleView: View { private func column(_ logs: [LogEntry]) -> some View { VStack(alignment: .leading, spacing: 0) { - Text("CONSOLE") + Text(L10n.text("CONSOLE")) .seleneFont(15, .semibold) .tracking(1.2) .foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(8)) HStack(alignment: .firstTextBaseline) { - Text("Build and launch output") + Text(L10n.text("Build and launch output")) .seleneFont(40) .foregroundColor(SeleneTheme.ink) .lineLimit(1) @@ -170,7 +170,7 @@ public struct ConsoleView: View { private func countLine(_ logs: [LogEntry]) -> String { let errors = viewModel.logs.filter { $0.level == .error }.count - let lines = logs.count == viewModel.logs.count ? "\(logs.count) lines" : "\(logs.count) of \(viewModel.logs.count) lines" + let lines = logs.count == viewModel.logs.count ? L10n.count("%lld lines", logs.count) : L10n.format("Visible lines: %@ / %@", [String(describing: logs.count), String(describing: viewModel.logs.count)]) return errors == 0 ? lines : "\(lines) · \(errors) \(errors == 1 ? "error" : "errors")" } @@ -178,7 +178,7 @@ public struct ConsoleView: View { HStack(spacing: canvas.u(6)) { ForEach(ConsoleFilter.allCases) { item in pill(.filter(item)) { - Text(item.title) + Text(L10n.text(item.title)) .seleneFont(15, filter == item ? .semibold : .regular) .foregroundColor(filter == item ? SeleneTheme.ink : SeleneTheme.ink60) } @@ -190,14 +190,14 @@ public struct ConsoleView: View { MaterialIconView(.search, size: 16) .foregroundColor(SeleneTheme.ink60) if searching { - TextField("Search", text: $searchText) + TextField(L10n.text("Search"), text: $searchText) .textFieldStyle(.plain) .seleneFont(15) .foregroundColor(SeleneTheme.ink) .focused($searchFocused) .onSubmit(endSearch) } else { - Text(searchText.isEmpty ? "Search" : searchText) + Text(searchText.isEmpty ? L10n.text("Search") : searchText) .seleneFont(15) .foregroundColor(searchText.isEmpty ? SeleneTheme.ink60 : SeleneTheme.ink) .lineLimit(1) @@ -212,28 +212,28 @@ public struct ConsoleView: View { Circle() .fill(follow ? PortingLogBox.softGreen : SeleneTheme.ink25) .frame(width: canvas.u(7), height: canvas.u(7)) - Text("Follow") + Text(L10n.text("Follow")) .seleneFont(15) .foregroundColor(follow ? SeleneTheme.ink : SeleneTheme.ink60) } } pill(.copy) { - Text(didCopy ? "Copied" : "Copy log") + Text(didCopy ? L10n.text("Copied") : L10n.text("Copy log")) .seleneFont(15) .foregroundColor(SeleneTheme.ink) } pill(.diagnostics) { - Text(didCopyDiagnostics ? "Copied" : "Copy diagnostics") + Text(didCopyDiagnostics ? L10n.text("Copied") : L10n.text("Copy diagnostics")) .seleneFont(15) .foregroundColor(SeleneTheme.ink) } pill(.reveal) { - Text("Show in Finder") + Text(L10n.text("Show in Finder")) .seleneFont(15) .foregroundColor(SeleneTheme.ink) } pill(.clear) { - Text("Clear") + Text(L10n.text("Clear")) .seleneFont(15) .foregroundColor(PortingPillCopy.softRed) } @@ -323,12 +323,12 @@ public struct ConsoleView: View { VStack(spacing: canvas.u(8)) { MaterialIconView(.terminal, size: 30) .foregroundColor(SeleneTheme.ink25) - Text(viewModel.logs.isEmpty ? "Nothing yet" : "No lines match") + Text(viewModel.logs.isEmpty ? L10n.text("Nothing yet") : L10n.text("No lines match")) .seleneFont(19, .medium) .foregroundColor(SeleneTheme.ink82) Text(viewModel.logs.isEmpty - ? "Portings and launches write here." - : "Try another filter or search.") + ? L10n.text("Portings and launches write here.") + : L10n.text("Try another filter or search.")) .seleneFont(14) .foregroundColor(SeleneTheme.ink60) } @@ -397,7 +397,7 @@ public struct ConsoleView: View { switch focus { case .filter(let item): return item.title case .search: return searching ? "Done" : "Search" - case .follow: return follow ? "Stop following" : "Follow" + case .follow: return follow ? L10n.text("Stop following") : "Follow" case .copy: return "Copy log" case .diagnostics: return "Copy diagnostics" case .reveal: return "Show in Finder" @@ -478,7 +478,7 @@ public struct ConsoleView: View { if let url = viewModel.buildLogURL, let build = try? String(contentsOf: url, encoding: .utf8) { let tail = build.split(separator: "\n", omittingEmptySubsequences: false).suffix(400) - text += "\n\n--- Build log: \(url.lastPathComponent) ---\n" + tail.joined(separator: "\n") + text += L10n.format("\n\n--- Build log: %@ ---\n", [String(describing: (url.lastPathComponent))]) + tail.joined(separator: "\n") } guard !text.isEmpty else { return SoundEffectService.shared.play(.error) } let pasteboard = NSPasteboard.general diff --git a/Sources/NXLibrary/Views/ControlsView.swift b/Sources/NXLibrary/Views/ControlsView.swift index 5734603..cb08250 100644 --- a/Sources/NXLibrary/Views/ControlsView.swift +++ b/Sources/NXLibrary/Views/ControlsView.swift @@ -26,29 +26,29 @@ public struct ControlsView: View { if inGame { // nxnative dm/host/front_mac.m: key_bit, on_key and push. return [ - ("Left stick", "W · A · S · D"), - ("D-pad", "Arrow keys"), + (L10n.text("Left stick"), "W · A · S · D"), + (L10n.text("D-pad"), L10n.text("Arrow keys")), ("A · B · X · Y", "J · K · U · I"), ("L · R", "Q · E"), ("ZL · ZR", "Z · C"), - ("Press left / right stick", "F · G"), - ("Plus · Minus", "Return · Tab"), - ("Shake", "Hold Space"), - ("Right stick", "Shift + Arrow keys · Controller"), - ("Touchscreen", "Click and drag · Handheld mode"), - ("Gamepad", "USB or Bluetooth · Button positions follow the engine's layout"), - ("Rumble", "Depends on the game, build and controller"), + (L10n.text("Press left / right stick"), "F · G"), + (L10n.text("Plus · Minus"), L10n.text("Return · Tab")), + (L10n.text("Shake"), L10n.text("Hold Space")), + (L10n.text("Right stick"), L10n.text("Shift + Arrow keys · Controller")), + (L10n.text("Touchscreen"), L10n.text("Click and drag · Handheld mode")), + ("Gamepad", L10n.text("USB or Bluetooth · Button positions follow the engine's layout")), + ("Rumble", L10n.text("Depends on the game, build and controller")), // nxnative 48888df5 (home_quit.h): held for 2 s, a prompt; A quits as ⌘Q does, B or Home goes back. - ("Quit the game", "⌘Q · Hold Home for 2 s, then A, in games built with the current engine") + (L10n.text("Quit the game"), "⌘Q · Hold Home for 2 s, then A, in games built with the current engine") ] } return [ - ("Move", "Arrows · D-pad or left stick"), - ("Select", "Return / Space · " + controller.activeBrand.confirmButtonName), - ("Back", "Escape · " + controller.activeBrand.cancelButtonName), + ("Move", L10n.text("Arrows · D-pad or left stick")), + ("Select", L10n.text("Return / Space · ") + controller.activeBrand.confirmButtonName), + ("Back", L10n.text("Escape · ") + controller.activeBrand.cancelButtonName), ("Menu", "⌘K · Menu / Start"), ("Find a game", "⌘F · Hold View"), - ("Move a tile", "Hold Confirm on a Spotlight tile") + (L10n.text("Move a tile"), L10n.text("Hold Confirm on a Spotlight tile")) ] } @@ -57,19 +57,19 @@ public struct ControlsView: View { ScrollViewReader { proxy in ScrollView { VStack(alignment: .leading, spacing: canvas.u(12)) { - Text("Controls") + Text(L10n.text("Controls")) .seleneFont(40) .foregroundColor(SeleneTheme.ink) - Text(inGame ? "Default game inputs. Older builds may differ." : "Navigate NX Library.") + Text(inGame ? L10n.text("Default game inputs. Older builds may differ.") : L10n.text("Navigate NX Library.")) .seleneFont(19) .foregroundColor(SeleneTheme.ink60) .padding(.bottom, canvas.u(18)) SeleneSegmented(selection: $inGame, options: [(false, "Library"), (true, "In game")]) .padding(.bottom, canvas.u(18)) - SeleneOptionRow(icon: .gamepad, title: "Controller", - about: controller.isGamepadConnected ? controller.activeControllerName : "Not connected", + SeleneOptionRow(icon: .gamepad, title: L10n.text("Controller"), + about: controller.isGamepadConnected ? controller.activeControllerName : L10n.text("Not connected"), focused: focus == 0) { - Button("Detect") { controller.refreshControllers() } + Button(L10n.text("Detect")) { controller.refreshControllers() } .buttonStyle(.plain) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.glyph) @@ -93,8 +93,8 @@ public struct ControlsView: View { } .onChange(of: inGame) { _ in focus = 0; proxy.scrollTo(0, anchor: .top) } } - SeleneHintsBar(left: [HintItem(.b, "Back", action: onClose)], - right: [HintItem(.lb, "Library") { inGame = false }, HintItem(.rb, "In game") { inGame = true }]) + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"), action: onClose)], + right: [HintItem(.lb, L10n.text("Library")) { inGame = false }, HintItem(.rb, L10n.text("In game")) { inGame = true }]) } .background(SeleneBackdrop()) .environment(\.seleneMinimumFont, 12) diff --git a/Sources/NXLibrary/Views/Detail/BuildSourcesPanel.swift b/Sources/NXLibrary/Views/Detail/BuildSourcesPanel.swift index 5322121..7f0d9ac 100644 --- a/Sources/NXLibrary/Views/Detail/BuildSourcesPanel.swift +++ b/Sources/NXLibrary/Views/Detail/BuildSourcesPanel.swift @@ -46,8 +46,8 @@ struct BuildSourcesPanel: View { VStack(alignment: .leading, spacing: 0) { header status - Text(wide ? "Version \(presentation.version) · \(presentation.size) · \(appFileName)" - : "Version \(presentation.version) · \(presentation.size)") + Text(wide ? L10n.format("Version %@ · %@ · %@", [String(describing: (presentation.version)), String(describing: (presentation.size)), String(describing: (appFileName))]) + : L10n.format("Version %@ · %@", [String(describing: (presentation.version)), String(describing: (presentation.size))])) .seleneFont(15) .foregroundColor(SeleneTheme.ink60) .lineLimit(1) @@ -64,8 +64,8 @@ struct BuildSourcesPanel: View { } if presentation.sourcesDetached { Text(presentation.canPlay - ? "Game files are missing or not attached. Play is available; attach them to rebuild." - : "Game files are missing or not attached. Attach them to build.") + ? L10n.text("Game files are missing or not attached. Play is available; attach them to rebuild.") + : L10n.text("Game files are missing or not attached. Attach them to build.")) .seleneFont(15) .foregroundColor(SeleneTheme.ink60) .padding(.top, canvas.u(4)) @@ -79,13 +79,13 @@ struct BuildSourcesPanel: View { private var header: some View { HStack(spacing: canvas.u(8)) { - Text("Build & Sources") + Text(L10n.text("Build & Sources")) .seleneFont(21, .semibold) .foregroundColor(SeleneTheme.ink) - SeleneInfoTip("NX Library uses symlinks to your game ROM files to build the macOS native binary. Rebuilding uses your current engine pin and settings, and preserves your game saves. " + SeleneInfoTip(L10n.text("NX Library uses symlinks to your game ROM files to build the macOS native binary. Rebuilding uses your current engine pin and settings, and preserves your game saves. ") + (presentation.sourcesDetached - ? "The sources are detached: attach them again to rebuild." - : "The original sources are attached. Rebuild writes a new app and keeps the previous one if it fails.")) + ? L10n.text("The sources are detached: attach them again to rebuild.") + : L10n.text("The original sources are attached. Rebuild writes a new app and keeps the previous one if it fails."))) } .padding(.top, canvas.u(2)) .padding(.bottom, canvas.u(14)) @@ -115,7 +115,7 @@ struct BuildSourcesPanel: View { Circle() .fill(presentation.sourcesDetached ? SeleneTheme.untested : SeleneTheme.success) .frame(width: canvas.u(9), height: canvas.u(9)) - Text(presentation.sourcesDetached ? "Sources detached" : "Sources attached") + Text(presentation.sourcesDetached ? L10n.text("Sources detached") : L10n.text("Sources attached")) .seleneFont(15, .medium) .foregroundColor(presentation.sourcesDetached ? SeleneTheme.untested.opacity(0.9) @@ -125,11 +125,11 @@ struct BuildSourcesPanel: View { /// The engine and the toolchain. @ViewBuilder private var engineStatus: some View { - Text("Engine \(presentation.engineCommitShort)") + Text(L10n.format("Engine %@", [String(describing: (presentation.engineCommitShort))])) .seleneFont(15, .semibold) .monospacedDigit() .foregroundColor(SeleneTheme.ink82) - Text(presentation.toolchainVerified ? "Certified" : "Untested toolchain") + Text(presentation.toolchainVerified ? L10n.text("Certified") : L10n.text("Untested toolchain")) .seleneFont(15, .medium) .foregroundColor(presentation.toolchainVerified ? SeleneTheme.success.opacity(0.85) @@ -138,7 +138,7 @@ struct BuildSourcesPanel: View { @ViewBuilder private var blockedStatus: some View { if buildBlocked { - Text(blockedHint.flatMap { $0.buildHint.isEmpty ? nil : $0.buildHint } ?? "Build blocked — Settings → Engine") + Text(blockedHint.flatMap { $0.buildHint.isEmpty ? nil : $0.buildHint } ?? L10n.text("Build blocked — Settings → Engine")) .seleneFont(15, .semibold) .foregroundColor(blockedHint?.tone == .info ? SeleneTheme.ink82 : SeleneTheme.untested) } @@ -187,30 +187,30 @@ extension BuildSourcesRow { static func rows(for game: PortedGame, presentation p: GameDetailPresentation, buildBlocked: Bool, viewModel: AppViewModel, rebuild: @escaping () -> Void) -> [BuildSourcesRow] { var rows: [BuildSourcesRow] = [ - BuildSourcesRow(id: "attach", title: "Attach sources…") { + BuildSourcesRow(id: "attach", title: L10n.text("Attach sources…")) { viewModel.locateSources(for: game) }, - BuildSourcesRow(id: "rebuild", title: "Rebuild", enabled: p.canRebuild && !buildBlocked) { + BuildSourcesRow(id: "rebuild", title: L10n.text("Rebuild"), enabled: p.canRebuild && !buildBlocked) { rebuild() }, - BuildSourcesRow(id: "log", title: "Open log") { + BuildSourcesRow(id: "log", title: L10n.text("Open log")) { viewModel.revealLog(for: game) }, - BuildSourcesRow(id: "save", title: "Open save folder") { + BuildSourcesRow(id: "save", title: L10n.text("Open save folder")) { viewModel.revealSaveFolder(URL(fileURLWithPath: p.savePath)) }, - BuildSourcesRow(id: "app", title: "Reveal app in Finder") { + BuildSourcesRow(id: "app", title: L10n.text("Reveal app in Finder")) { viewModel.revealInFinder(url: game.url) } ] if let store = p.storeURL { - rows.append(BuildSourcesRow(id: "store", title: "Store page") { viewModel.openURL(store) }) + rows.append(BuildSourcesRow(id: "store", title: L10n.text("Store page")) { viewModel.openURL(store) }) } if let hltb = p.hltbURL { - rows.append(BuildSourcesRow(id: "hltb", title: "HowLongToBeat") { viewModel.openURL(hltb) }) + rows.append(BuildSourcesRow(id: "hltb", title: L10n.text("HowLongToBeat")) { viewModel.openURL(hltb) }) } if p.description.isEmpty, let search = viewModel.nintendoStoreSearchURL(for: game) { - rows.append(BuildSourcesRow(id: "search", title: "Search Nintendo Store") { + rows.append(BuildSourcesRow(id: "search", title: L10n.text("Search Nintendo Store")) { viewModel.openURL(search) }) } diff --git a/Sources/NXLibrary/Views/Detail/DetailsBands.swift b/Sources/NXLibrary/Views/Detail/DetailsBands.swift index b6f64ec..26df916 100644 --- a/Sources/NXLibrary/Views/Detail/DetailsBands.swift +++ b/Sources/NXLibrary/Views/Detail/DetailsBands.swift @@ -11,7 +11,7 @@ struct DetailsBandTitle: View { var size: CGFloat = 21 var body: some View { - Text(text) + Text(L10n.text(text)) .seleneFont(size, .semibold) .foregroundColor(SeleneTheme.ink) .padding(.top, canvas.u(2)) @@ -38,7 +38,7 @@ struct DetailsScreenshotsBand: View { var body: some View { VStack(alignment: .leading, spacing: 0) { if !urls.isEmpty || known { - DetailsBandTitle(text: source.map { "Screenshots · \($0)" } ?? "Screenshots") + DetailsBandTitle(text: source.map { L10n.format("Screenshots · %@", [String(describing: ($0))]) } ?? L10n.text("Screenshots")) } SpotlightScreenshotStrip(titleID: titleID, urls: urls, known: known, focusedIndex: focusedIndex, artwork: artwork, @@ -60,16 +60,16 @@ struct DetailsActivityColumn: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - DetailsBandTitle(text: "Your activity") + DetailsBandTitle(text: L10n.text("Your activity")) VStack(alignment: .leading, spacing: canvas.u(14)) { HStack(alignment: .top, spacing: canvas.u(24)) { - cell("PLAY TIME", presentation.playtime) + cell(L10n.text("PLAY TIME"), presentation.playtime) cell("LAST PLAYED", presentation.lastPlayed) } HStack(alignment: .top, spacing: canvas.u(24)) { - cell("THIS WEEK", presentation.thisWeek) + cell(L10n.text("THIS WEEK"), presentation.thisWeek) if let rank = presentation.rank { - cell("RANK", rank) + cell(L10n.text("RANK"), rank) } } } @@ -79,7 +79,7 @@ struct DetailsActivityColumn: View { private func cell(_ label: String, _ value: String) -> some View { VStack(alignment: .leading, spacing: canvas.u(2)) { - Text(label) + Text(L10n.text(label)) .seleneFont(12, .semibold) .foregroundColor(SeleneTheme.ink60) Text(value) @@ -115,7 +115,7 @@ struct DetailsMoreBand: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - DetailsBandTitle(text: "More like this") + DetailsBandTitle(text: L10n.text("More like this")) HStack(alignment: .top, spacing: canvas.u(Self.gap)) { ForEach(Array(games.enumerated()), id: \.element.id) { index, game in tile(game, index: index, focused: focusedIndex == index) @@ -203,7 +203,7 @@ struct DetailsModsBand: View { } Spacer(minLength: 0) if gameBanana.phase != .idle { - Text("Mod data provided by GameBanana") + Text(L10n.text("Mod data provided by GameBanana")) .seleneFont(14) .foregroundColor(SeleneTheme.ink60) } @@ -233,10 +233,10 @@ struct DetailsModsBand: View { private var title: String { let on = mods.filter(\.isOn).count - return on == 0 ? "Mods" : "Mods · \(on) on" + return on == 0 ? L10n.text("Mods") : L10n.format("Mods · %@ on", [String(describing: (on))]) } - static func state(_ mod: LocalMod) -> String { mod.romfs == nil ? "Not a mod" : mod.isOn ? "On" : "Off" } + static func state(_ mod: LocalMod) -> String { mod.romfs == nil ? L10n.text("Not a mod") : mod.isOn ? "On" : "Off" } @ViewBuilder private func tileView(_ kind: ModsBandTile, index: Int, focused: Bool) -> some View { @@ -247,11 +247,11 @@ struct DetailsModsBand: View { .accessibilityLabel("\(mod.folderName), \(Self.state(mod))") case .manage: tile(index: index, focused: focused) { manageFace } - .accessibilityLabel(mods.isEmpty ? "Add mods" : "Manage mods") + .accessibilityLabel(mods.isEmpty ? L10n.text("Add mods") : L10n.text("Manage mods")) case .available(let id): if let mod = gameBanana.available.first(where: { $0.id == id }) { tile(index: index, focused: focused, bleed: true) { availableFace(mod) } - .accessibilityLabel("\(mod.name), by \(mod.author)\(chip(for: mod).map { ", \($0)" } ?? "")") + .accessibilityLabel(L10n.format("%@, by %@%@", [String(describing: (mod.name)), String(describing: (mod.author)), String(describing: (chip(for: mod).map { ", \($0)" } ?? ""))])) } case .status(let action): tile(index: index, focused: focused) { statusFace(action) } @@ -279,10 +279,10 @@ struct DetailsModsBand: View { MaterialIconView(mods.isEmpty ? .add : .settings, size: 26) .foregroundColor(SeleneTheme.ink75) Spacer(minLength: 0) - Text(mods.isEmpty ? "Add mods" : "Manage mods") + Text(mods.isEmpty ? L10n.text("Add mods") : L10n.text("Manage mods")) .seleneFont(19, .semibold) .foregroundColor(SeleneTheme.ink) - Text(mods.isEmpty ? "File mods, no rebuild" : mods.count > ModsBandLayout.maxInstalled ? "All \(mods.count) mods" : "Order, folder, Remove") + Text(mods.isEmpty ? L10n.text("File mods, no rebuild") : mods.count > ModsBandLayout.maxInstalled ? L10n.format("All %@ mods", [String(describing: (mods.count))]) : L10n.text("Order, folder, Remove")) .seleneFont(15, .medium) .foregroundColor(SeleneTheme.ink60) } @@ -291,9 +291,9 @@ struct DetailsModsBand: View { /// Installed / Update / the install in progress, for an available mod. private func chip(for mod: AvailableMod) -> String? { switch gameBanana.installs[mod.id] { - case .downloading(let fraction): return "Downloading \(Int(fraction * 100))%" - case .installing: return "Installing…" - case .failed: return "Didn't install" + case .downloading(let fraction): return L10n.format("Downloading %@%%", [String(describing: (Int(fraction * 100)))]) + case .installing: return L10n.text("Installing…") + case .failed: return L10n.text("Didn't install") case .done, nil: break } guard let record = records[mod.id] else { return nil } @@ -318,7 +318,7 @@ struct DetailsModsBand: View { .seleneFont(18, .semibold) .foregroundColor(SeleneTheme.ink) .lineLimit(1) - Text("by \(mod.author)") + Text(L10n.format("by %@", [String(describing: (mod.author))])) .seleneFont(14, .medium) .foregroundColor(SeleneTheme.ink75) .lineLimit(1) @@ -329,13 +329,13 @@ struct DetailsModsBand: View { private func statusText(_ action: ModsBandAction) -> (icon: MaterialIcon, title: String, about: String) { switch action { - case .retry: return (.sync, "Retry", "Ask GameBanana again") - case .openPage: return (.link, "Open on GameBanana", "See every mod for this game") + case .retry: return (.sync, L10n.text("Retry"), L10n.text("Ask GameBanana again")) + case .openPage: return (.link, L10n.text("Open on GameBanana"), L10n.text("See every mod for this game")) case .candidate(let id): - return (.store, gameBanana.candidate(id)?.name ?? "Game", "This is the game") - case .noneOfThese: return (.close, "None of these", "Pick the game by name") - case .findGame: return (.search, "Find the game", "Search GameBanana by name") - case .wrongGame: return (.search, "Wrong game?", gameBanana.matchedName.map { "GameBanana: \($0)" } ?? "Pick another game") + return (.store, gameBanana.candidate(id)?.name ?? "Game", L10n.text("This is the game")) + case .noneOfThese: return (.close, L10n.text("None of these"), L10n.text("Pick the game by name")) + case .findGame: return (.search, L10n.text("Find the game"), L10n.text("Search GameBanana by name")) + case .wrongGame: return (.search, L10n.text("Wrong game?"), gameBanana.matchedName.map { L10n.format("GameBanana: %@", [String(describing: ($0))]) } ?? L10n.text("Pick another game")) } } diff --git a/Sources/NXLibrary/Views/Detail/DetailsTopBlock.swift b/Sources/NXLibrary/Views/Detail/DetailsTopBlock.swift index 14a959f..d0ab664 100644 --- a/Sources/NXLibrary/Views/Detail/DetailsTopBlock.swift +++ b/Sources/NXLibrary/Views/Detail/DetailsTopBlock.swift @@ -46,7 +46,7 @@ struct DetailsTopBlock: View { .padding(.top, canvas.u(16)) } if case .failed(let failure) = pillSet { - Text(failure == .cancelled ? "Porting cancelled" : "Porting failed") + Text(failure == .cancelled ? L10n.text("Porting cancelled") : L10n.text("Porting failed")) .seleneFont(19, .semibold) .foregroundColor(PortingPillCopy.softRed) .padding(.leading, canvas.u(4)) @@ -125,9 +125,9 @@ struct DetailsTopBlock: View { Text(reason.detail).seleneFont(15).foregroundColor(SeleneTheme.ink60) case .normal: if !game.sources.isEmpty, !GameResourceState.sourcesAvailable(for: game) { - Text("Game files not found · Rebuild unavailable").seleneFont(15).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("Game files not found · Rebuild unavailable")).seleneFont(15).foregroundColor(SeleneTheme.ink60) } else if !game.isPlayableLocally { - Text("Not built yet").seleneFont(15).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("Not built yet")).seleneFont(15).foregroundColor(SeleneTheme.ink60) } } } @@ -135,10 +135,10 @@ struct DetailsTopBlock: View { @ViewBuilder private var pills: some View { switch pillSet { case .porting: - SelenePillButton(title: "Porting", minWidth: 200, height: 52, horizontalPadding: 36, + SelenePillButton(title: L10n.text("Porting"), minWidth: 200, height: 52, horizontalPadding: 36, focused: focusedPill == 0, waveDots: true) { onPill(0) } .onHover { if $0 { onHoverPill(0) } } - .accessibilityLabel("Porting, show progress") + .accessibilityLabel(L10n.text("Porting, show progress")) case .failed: HStack(spacing: canvas.u(14)) { ForEach(Array(PortingPillCopy.failedTitles.enumerated()), id: \.offset) { index, title in @@ -158,7 +158,7 @@ struct DetailsTopBlock: View { horizontalPadding: 36, focused: focusedPill == 0, enabled: isRunning || (primaryEnabled && pillSet.unavailableReason == nil), action: onPrimary) .onHover { if $0 { onHoverPill(0) } } - SelenePillButton(title: isRunning ? "Close Game" : "Options", minWidth: 200, height: 52, + SelenePillButton(title: isRunning ? L10n.text("Close Game") : L10n.text("Options"), minWidth: 200, height: 52, horizontalPadding: 36, focused: focusedPill == 1, action: onSecondary) .onHover { if $0 { onHoverPill(1) } } @@ -166,14 +166,14 @@ struct DetailsTopBlock: View { iconSize: 20, minWidth: 52, height: 52, horizontalPadding: 0, focused: focusedPill == 2, action: onFavorite) .onHover { if $0 { onHoverPill(2) } } - .accessibilityLabel(game.isFavorite ? "Remove from favorites" : "Add to favorites") + .accessibilityLabel(game.isFavorite ? L10n.text("Remove from favorites") : L10n.text("Add to favorites")) } } private var primaryTitle: String { - if isRunning { return "Resume" } - if pillSet.unavailableReason != nil { return "Unavailable" } - return presentation.canPlay ? "Play" : "Build" + if isRunning { return L10n.text("Resume") } + if pillSet.unavailableReason != nil { return L10n.text("Unavailable") } + return presentation.canPlay ? L10n.text("Play") : "Build" } // MARK: - Stats @@ -181,14 +181,14 @@ struct DetailsTopBlock: View { /// PLAYTIME · LAST PLAYED · RELEASED · CHEATS · PLATFORM, 28 either side of each rule. private var stats: some View { var items = [ - SeleneStat.text("PLAYTIME", presentation.playtime), - SeleneStat.text("LAST PLAYED", presentation.lastPlayed) + SeleneStat.text(L10n.text("PLAYTIME"), presentation.playtime), + SeleneStat.text(L10n.text("LAST PLAYED"), presentation.lastPlayed) ] if let released = presentation.released { - items.append(SeleneStat.text("RELEASED", released)) + items.append(SeleneStat.text(L10n.text("RELEASED"), released)) } if cheatsOn { - items.append(SeleneStat.text("CHEATS", "On")) + items.append(SeleneStat.text(L10n.text("CHEATS"), L10n.text("On"))) } items.append(SeleneStat.platform(presentation.console)) return SeleneStatsRow(items: items, gap: 28) @@ -221,10 +221,13 @@ struct DetailsTopBlock: View { let text = presentation.description VStack(alignment: .leading, spacing: canvas.u(6)) { if text.isEmpty { - Text("No description for this game yet.") + Text(L10n.text("No description for this game yet.")) .seleneFont(17) .foregroundColor(SeleneTheme.ink60) } else { + if let note = presentation.descriptionLanguageNote { + Text(note).seleneFont(14).foregroundColor(SeleneTheme.ink60) + } if expanded { ScrollView(.vertical, showsIndicators: false) { descriptionText(text) @@ -238,7 +241,7 @@ struct DetailsTopBlock: View { .clipped() } if text.count > Self.readMoreThreshold { - Button(expanded ? "Show less" : "Read more") { expanded.toggle() } + Button(expanded ? L10n.text("Show less") : L10n.text("Read more")) { expanded.toggle() } .buttonStyle(.plain) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.glyph) diff --git a/Sources/NXLibrary/Views/Detail/GameBananaFindPanel.swift b/Sources/NXLibrary/Views/Detail/GameBananaFindPanel.swift index 9a9d662..3e8d3a7 100644 --- a/Sources/NXLibrary/Views/Detail/GameBananaFindPanel.swift +++ b/Sources/NXLibrary/Views/Detail/GameBananaFindPanel.swift @@ -17,16 +17,16 @@ struct GameBananaFindPanel: View { .contentShape(Rectangle()) .onTapGesture { close() } VStack(alignment: .leading, spacing: canvas.u(14)) { - Text("GAMEBANANA") + Text(L10n.text("GAMEBANANA")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) - Text("Which game is \(gameName)?") + Text(L10n.format("Which game is %@?", [String(describing: (gameName))])) .seleneFont(32) .foregroundColor(SeleneTheme.ink) .lineLimit(2) HStack(spacing: canvas.u(10)) { MaterialIconView(.search, size: 22).foregroundColor(SeleneTheme.ink60) - TextField("", text: $model.query, prompt: Text("Game name").foregroundColor(SeleneTheme.ink60)) + TextField("", text: $model.query, prompt: Text(L10n.text("Game name")).foregroundColor(SeleneTheme.ink60)) .textFieldStyle(.plain) .seleneFont(18) .foregroundColor(SeleneTheme.ink) @@ -45,9 +45,9 @@ struct GameBananaFindPanel: View { .onChange(of: fieldFocused) { if model.editing != $0 { model.editing = $0 } } if model.searching { - Text("Searching GameBanana…").seleneFont(16).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("Searching GameBanana…")).seleneFont(16).foregroundColor(SeleneTheme.ink60) } else if model.searched && model.results.isEmpty { - Text("No games found. Try another name.").seleneFont(16).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("No games found. Try another name.")).seleneFont(16).foregroundColor(SeleneTheme.ink60) } VStack(alignment: .leading, spacing: canvas.u(8)) { ForEach(Array(model.results.enumerated()), id: \.element.id) { index, game in @@ -56,7 +56,7 @@ struct GameBananaFindPanel: View { row(index: model.results.count, title: "None of these", icon: nil) .opacity(model.searched && !model.searching ? 1 : 0.5) } - Text("Mod data provided by GameBanana") + Text(L10n.text("Mod data provided by GameBanana")) .seleneFont(14) .foregroundColor(SeleneTheme.ink60) } diff --git a/Sources/NXLibrary/Views/Detail/GameDetailView.swift b/Sources/NXLibrary/Views/Detail/GameDetailView.swift index 0a3eea4..d5d1a2b 100644 --- a/Sources/NXLibrary/Views/Detail/GameDetailView.swift +++ b/Sources/NXLibrary/Views/Detail/GameDetailView.swift @@ -190,8 +190,8 @@ struct GameDetailView: View { VStack(spacing: 0) { Spacer(minLength: 0) - SeleneHintsBar(left: [HintItem(.b, "Back", action: { router.back() })], - right: [HintItem(.y, "Options"), HintItem(.a, "Select")], + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"), action: { router.back() })], + right: [HintItem(.y, L10n.text("Options")), HintItem(.a, L10n.text("Select"))], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } .seleneOverlaidHints(router.overlayHasHints(viewModel)) diff --git a/Sources/NXLibrary/Views/Detail/ModDetailPanel.swift b/Sources/NXLibrary/Views/Detail/ModDetailPanel.swift index b660fc2..15a235b 100644 --- a/Sources/NXLibrary/Views/Detail/ModDetailPanel.swift +++ b/Sources/NXLibrary/Views/Detail/ModDetailPanel.swift @@ -112,7 +112,7 @@ struct ModDetailPanel: View { let rows = Self.rows(for: mod) return VStack(alignment: .leading, spacing: canvas.u(12)) { Group { - Text("GAMEBANANA MOD") + Text(L10n.text("GAMEBANANA MOD")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) Text(mod.name) @@ -130,7 +130,7 @@ struct ModDetailPanel: View { .lineLimit(3) .fixedSize(horizontal: false, vertical: true) } - Text("\(mod.likes) likes · \(mod.downloads) downloads") + Text(L10n.format("%@ likes · %@ downloads", [String(describing: (mod.likes)), String(describing: (mod.downloads))])) .seleneFont(15) .foregroundColor(SeleneTheme.ink60) } @@ -144,7 +144,7 @@ struct ModDetailPanel: View { .padding(.top, canvas.u(6)) } VStack(alignment: .leading, spacing: canvas.u(8)) { - Text(mod.files.count == 1 ? "FILE" : "FILES") + Text(mod.files.count == 1 ? L10n.text("FILE") : L10n.text("FILES")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(14)) @@ -154,7 +154,7 @@ struct ModDetailPanel: View { } } .padding(.top, canvas.u(22)) - Text("Mod data provided by GameBanana") + Text(L10n.text("Mod data provided by GameBanana")) .seleneFont(14) .foregroundColor(SeleneTheme.ink60) .padding(.top, canvas.u(6)) @@ -214,7 +214,7 @@ struct ModDetailPanel: View { ) .background(alignment: .topLeading) { // One line, to measure. - Text("Ag").seleneFont(16).hidden() + Text(L10n.text("Ag")).seleneFont(16).hidden() .background(GeometryReader { proxy in Color.clear.preference(key: LineHeightKey.self, value: proxy.size.height) }) @@ -278,24 +278,24 @@ struct ModDetailPanel: View { .frame(width: canvas.u(28), height: canvas.u(28)) .clipShape(Circle()) } - Text("by \(mod.author)") + Text(L10n.format("by %@", [String(describing: (mod.author))])) .seleneFont(18, .medium) .foregroundColor(SeleneTheme.ink) } } private var versionLine: String { - let date = mod.updated.formatted(date: .abbreviated, time: .omitted) - var parts = [mod.version.isEmpty ? "Updated \(date)" : "Version \(mod.version) · Updated \(date)"] - if let version = game.versionString, !version.isEmpty { parts.append("Your game: \(version)") } + let date = mod.updated.formatted(.dateTime.day().month(.abbreviated).year().locale(L10n.context.formatLocale)) + var parts = [mod.version.isEmpty ? L10n.format("Updated %@", [String(describing: (date))]) : L10n.format("Version %@ · Updated %@", [String(describing: (mod.version)), String(describing: (date))])] + if let version = game.versionString, !version.isEmpty { parts.append(L10n.format("Your game: %@", [String(describing: (version))])) } return parts.joined(separator: " ") } /// The author's requirements (game versions, other mods), so a mismatch shows before the download. private var requirementsLine: String? { guard !mod.requirements.isEmpty else { return nil } - let names = mod.requirements.map { $0.isOptional ? "\($0.name) (recommended)" : $0.name } - return "Requires: " + names.joined(separator: " · ") + let names = mod.requirements.map { $0.isOptional ? L10n.format("%@ (recommended)", [String(describing: ($0.name))]) : $0.name } + return L10n.text("Requires: ") + names.joined(separator: " · ") } // MARK: - Rows @@ -316,14 +316,14 @@ struct ModDetailPanel: View { private func describe(_ row: Row) -> RowText { switch row { case .readMore: - return RowText(icon: .chevron, title: expanded ? "Show less" : "Read more", action: nil, + return RowText(icon: .chevron, title: expanded ? L10n.text("Show less") : L10n.text("Read more"), action: nil, compact: true, iconTurn: expanded ? -90 : 90) case .openPage: - return RowText(icon: .link, title: "Open on GameBanana", detail: "Every file, comments and credits") + return RowText(icon: .link, title: L10n.text("Open on GameBanana"), detail: L10n.text("Every file, comments and credits")) case .file(let id): guard let file = mod.files.first(where: { $0.id == id }) else { return RowText(icon: .puzzle, title: "") } - let size = ByteCountFormatter.string(fromByteCount: file.size, countStyle: .file) - let date = file.dateAdded.map { " · " + $0.formatted(date: .abbreviated, time: .omitted) } ?? "" + let size = file.size.formatted(.byteCount(style: .file).locale(L10n.context.formatLocale)) + let date = file.dateAdded.map { " · " + $0.formatted(.dateTime.day().month(.abbreviated).year().locale(L10n.context.formatLocale)) } ?? "" var text = RowText(icon: .puzzle, title: file.name, detail: size + date) // Only the file being installed shows the install; the others wait for it to end. let state = model.installState(mod: mod.id, file: file.id) @@ -333,7 +333,7 @@ struct ModDetailPanel: View { text.progress = fraction return text case .installing: - text.action = "Installing…" + text.action = L10n.text("Installing…") text.enabled = false return text case .failed(let message): diff --git a/Sources/NXLibrary/Views/GameOptions/ChangeArtworkView.swift b/Sources/NXLibrary/Views/GameOptions/ChangeArtworkView.swift index fc8c99e..d37e4af 100644 --- a/Sources/NXLibrary/Views/GameOptions/ChangeArtworkView.swift +++ b/Sources/NXLibrary/Views/GameOptions/ChangeArtworkView.swift @@ -119,7 +119,7 @@ struct ChangeArtworkView: View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { - Text(browsing.map { "\($0.title.uppercased()) · BROWSE ONLINE" } ?? "CHANGE ARTWORK") + Text(browsing.map { L10n.format("%@ · BROWSE ONLINE", [String(describing: ($0.title.uppercased()))]) } ?? L10n.text("CHANGE ARTWORK")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(8)) @@ -143,7 +143,7 @@ struct ChangeArtworkView: View { .padding(.bottom, canvas.u(20)) .contentShape(Rectangle()) - SeleneHintsBar(left: [HintItem(.b, "Back", action: back)], right: rightHints, + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"), action: back)], right: rightHints, bottom: SeleneBottomScrim.hintsBottom, leftInset: 18, rightReserve: [["Open page"], ["Previous"], ["Next"], ["Choose", "Search"]]) } @@ -173,10 +173,10 @@ struct ChangeArtworkView: View { if askAppIcon { ZStack { SeleneTheme.scrim45.ignoresSafeArea() - SeleneConfirmDialog(title: "Where should the icon appear?", + SeleneConfirmDialog(title: L10n.text("Where should the icon appear?"), message: appIconMessage, - confirmTitle: "Use on the Mac", - cancelTitle: "Only here", + confirmTitle: L10n.text("Use on the Mac"), + cancelTitle: L10n.text("Only here"), focus: $appIconFocus, onCancel: { answerAppIcon(false) }, onConfirm: { answerAppIcon(true) }) @@ -189,8 +189,8 @@ struct ChangeArtworkView: View { private var appIconMessage: String { let running = RunningGameMonitor.runningApplication(for: game) != nil - return "Only here changes the image inside NX Library. Use on the Mac also shows it in Finder and the Dock. Automatic uses the game's own icon, then an online image if available. Changing the image keeps this location choice." - + (running ? " It changes when the game quits." : " A large game takes about a minute.") + return L10n.text("Only here changes the image inside NX Library. Use on the Mac also shows it in Finder and the Dock. Automatic uses the game's own icon, then an online image if available. Changing the image keeps this location choice.") + + (running ? L10n.text(" It changes when the game quits.") : L10n.text(" A large game takes about a minute.")) } private func answerAppIcon(_ use: Bool) { @@ -208,11 +208,11 @@ struct ChangeArtworkView: View { } private var rightHints: [HintItem] { - let move = [HintItem(.lb, "Previous"), HintItem(.rb, "Next")] - guard browsing != nil else { return move + [HintItem(.a, "Choose")] } - let page = browseResult?.page.map { url in HintItem(.x, "Open page", action: { NSWorkspace.shared.open(url) }) } - let accept = focus < 0 ? HintItem(.a, "Search", action: { fieldFocused = true }) - : HintItem(.a, "Choose", action: chooseFocused) + let move = [HintItem(.lb, L10n.text("Previous")), HintItem(.rb, L10n.text("Next"))] + guard browsing != nil else { return move + [HintItem(.a, L10n.text("Choose"))] } + let page = browseResult?.page.map { url in HintItem(.x, L10n.text("Open page"), action: { NSWorkspace.shared.open(url) }) } + let accept = focus < 0 ? HintItem(.a, L10n.text("Search"), action: { fieldFocused = true }) + : HintItem(.a, L10n.text("Choose"), action: chooseFocused) return (page.map { [$0] } ?? []) + move + (downloading ? [] : [accept]) } @@ -238,7 +238,7 @@ struct ChangeArtworkView: View { HStack(spacing: canvas.u(10)) { MaterialIconView(.search, size: 22).foregroundColor(SeleneTheme.ink60) TextField("", text: $browseDraft, - prompt: Text("Search \(browsing?.title ?? "")").foregroundColor(SeleneTheme.ink60)) + prompt: Text(L10n.format("Search %@", [String(describing: (browsing?.title ?? ""))])).foregroundColor(SeleneTheme.ink60)) .textFieldStyle(.plain) .seleneFont(18) .foregroundColor(SeleneTheme.ink) @@ -270,7 +270,7 @@ struct ChangeArtworkView: View { private var mainStatus: String { if let appIcon = appIconStatus { return appIcon } - return searching.contains(slot) ? "Searching…" : (found[slot]?.isEmpty ?? false) ? "No images found" : artworkLocation + return searching.contains(slot) ? L10n.text("Searching…") : (found[slot]?.isEmpty ?? false) ? L10n.text("No images found") : artworkLocation } /// On the Icon tab, how the game app's own icon change stands: signing a large app takes about a minute. @@ -291,26 +291,26 @@ struct ChangeArtworkView: View { } private func browseStatus(_ source: ArtworkBrowseSource) -> String { - if downloading { return "Downloading the full image from \(source.title)…" } - guard let result = browseResult else { return "Searching \(source.title) for “\(browseQuery)”…" } + if downloading { return L10n.format("Downloading the full image from %@…", [String(describing: (source.title))]) } + guard let result = browseResult else { return L10n.format("Searching %@ for “%@”…", [String(describing: (source.title)), String(describing: (browseQuery))]) } guard result.page != nil else { - return "\(source.title) has no \(source == .launchBox ? "Switch game" : "game") called “\(browseQuery)”" + return L10n.format("%@ has no %@ called “%@”", [String(describing: (source.title)), String(describing: (source == .launchBox ? L10n.text("Switch game") : "game")), String(describing: (browseQuery))]) } - if browseLoading.contains(slot) && (browseItems[slot] ?? []).isEmpty { return "Loading…" } + if browseLoading.contains(slot) && (browseItems[slot] ?? []).isEmpty { return L10n.text("Loading…") } if (browseItems[slot] ?? []).isEmpty { - return "\(source.title) has no \(slot.title.lowercased()) for “\(browseQuery)”" + return L10n.format("%@ has no %@ for “%@”", [String(describing: (source.title)), String(describing: (slot.title.lowercased())), String(describing: (browseQuery))]) } - return "\(source.title) · “\(browseQuery)” · full size downloads when you choose" + return L10n.format("%@ · “%@” · full size downloads when you choose", [String(describing: (source.title)), String(describing: (browseQuery))]) } private var artworkLocation: String { switch slot { - case .cover: return "Collection · Suggestions" - case .icon: return "Game tiles" - case .logo: return "Library · Launch" + case .cover: return L10n.text("Collection · Suggestions") + case .icon: return L10n.text("Game tiles") + case .logo: return L10n.text("Library · Launch") case .background: return viewModel.settingsStore?.settings.keepWallpaperOnGames == true - ? "Details · Launch · Home: wallpaper" : "Library · Launch" + ? L10n.text("Details · Launch · Home: wallpaper") : L10n.text("Library · Launch") } } @@ -432,22 +432,22 @@ struct ChangeArtworkView: View { switch tile { case .automatic: switch slot { - case .cover: return ("Automatic", "GameTDB, or a composed cover") - case .background: return ("Automatic", "The game's own art, then Title DB") - case .icon: return ("Automatic", "The game's own icon, then Title DB") - case .logo: return ("Automatic", "The store logo, then Steam") + case .cover: return (L10n.text("Automatic"), L10n.text("GameTDB, or a composed cover")) + case .background: return (L10n.text("Automatic"), L10n.text("The game's own art, then Title DB")) + case .icon: return (L10n.text("Automatic"), L10n.text("The game's own icon, then Title DB")) + case .logo: return (L10n.text("Automatic"), L10n.text("The store logo, then Steam")) } case .yourPick: - return ("Your pick", "Saved on this Mac") + return (L10n.text("Your pick"), L10n.text("Saved on this Mac")) case .iconLocation: - return ("Icon location", viewModel.usesIconOnMac(for: game) ? "Use on the Mac" : "Only here") + return (L10n.text("Icon location"), viewModel.usesIconOnMac(for: game) ? L10n.text("Use on the Mac") : "Only here") case .art(let item): return (item.art.candidate.source, item.art.resolution) case .browse(let source): let keyless = source.needsKey && !viewModel.hasSteamGridDBKey - return (source.title, keyless ? "Browse online · add your key" : "Browse online") + return (source.title, keyless ? L10n.text("Browse online · add your key") : "Browse online") case .file: - return ("From file…", "PNG or JPEG") + return (L10n.text("From file…"), L10n.text("PNG or JPEG")) } } @@ -625,7 +625,7 @@ struct ChangeArtworkView: View { downloading = false guard browsing == source else { return } guard let data else { - message = "The full image could not be downloaded from \(source.title). Try again." + message = L10n.format("The full image could not be downloaded from %@. Try again.", [String(describing: (source.title))]) SoundEffectService.shared.play(.error) return } @@ -658,10 +658,10 @@ struct ChangeArtworkView: View { panel.allowedContentTypes = [.png, .jpeg, .heic, .webP, .image] panel.allowsMultipleSelection = false panel.canChooseDirectories = false - panel.message = "Choose a \(slot.title.lowercased()) for \(game.name)." + panel.message = L10n.format("Choose a %@ for %@.", [String(describing: (slot.title.lowercased())), String(describing: (game.name))]) guard panel.runModal() == .OK, let url = panel.url else { return } guard let data = try? Data(contentsOf: url) else { - message = "That file could not be read." + message = L10n.text("That file could not be read.") SoundEffectService.shared.play(.error) return } diff --git a/Sources/NXLibrary/Views/GameOptions/ChangeMetadataView.swift b/Sources/NXLibrary/Views/GameOptions/ChangeMetadataView.swift index 991d360..1f2f5fb 100644 --- a/Sources/NXLibrary/Views/GameOptions/ChangeMetadataView.swift +++ b/Sources/NXLibrary/Views/GameOptions/ChangeMetadataView.swift @@ -65,7 +65,7 @@ struct ChangeMetadataView: View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { - Text("CHANGE METADATA") + Text(L10n.text("CHANGE METADATA")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(8)) @@ -76,7 +76,7 @@ struct ChangeMetadataView: View { .padding(.leading, canvas.u(6)) .padding(.bottom, canvas.u(14)) tabs - Text(searching ? "Looking on Title DB and GameTDB…" : " ") + Text(searching ? L10n.text("Looking on Title DB and GameTDB…") : " ") .seleneFont(14) .foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(10)) @@ -89,8 +89,8 @@ struct ChangeMetadataView: View { .padding(.bottom, canvas.u(20)) .contentShape(Rectangle()) - SeleneHintsBar(left: [HintItem(.b, "Back", action: close)], - right: [HintItem(.lb, "Previous"), HintItem(.rb, "Next"), HintItem(.a, "Choose")], + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"), action: close)], + right: [HintItem(.lb, L10n.text("Previous")), HintItem(.rb, L10n.text("Next")), HintItem(.a, L10n.text("Choose"))], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -172,16 +172,20 @@ struct ChangeMetadataView: View { switch row { case .automatic: let value = MetadataOptions.list(for: field, store: store, titleDB: nil, gameTDB: []).first?.value - return ("Automatic", value.map { "Nintendo store: \($0)" } ?? "Nothing from the store yet.") + return (L10n.text("Automatic"), value.map { L10n.format("Nintendo store: %@", [displayValue($0)]) } ?? L10n.text("Nothing from the store yet.")) case .yourPick(let value): - return ("Your own", value) + return (L10n.text("Your own"), displayValue(value)) case .option(let option): - return (option.source, option.value) + return (option.source, displayValue(option.value)) case .write: - return ("Write your own…", "Type the \(field.title.lowercased()) yourself.") + return (L10n.text("Write your own…"), L10n.format("Type the %@ yourself.", [String(describing: (field.title.lowercased()))])) } } + private func displayValue(_ value: String) -> String { + field == .genres ? GameGenre.list(value.components(separatedBy: ",")).joined(separator: ", ") : value + } + /// Long descriptions show in full only on the focused row. private func excerpt(_ value: String?, full: Bool) -> String? { guard let value, !full, value.count > 240 else { return value } @@ -231,7 +235,7 @@ struct ChangeMetadataView: View { ZStack { SeleneTheme.scrim45.ignoresSafeArea().contentShape(Rectangle()) .onTapGesture { editing = false } - SeleneConfirmDialog(title: field.title, message: game.name, confirmTitle: "Save", + SeleneConfirmDialog(title: field.title, message: game.name, confirmTitle: L10n.text("Save"), focus: $editorFocus, onCancel: { editing = false }, onConfirm: saveDraft) { VStack(alignment: .leading, spacing: canvas.u(12)) { if field == .description { @@ -245,9 +249,9 @@ struct ChangeMetadataView: View { .focused($editorHasFocus) .onSubmit(saveDraft) } - Text(field == .genres ? "Separate with commas." - : field == .releaseDate ? "YYYY-MM-DD" - : field == .description ? "⌘Return to save · Empty for Automatic" : "Empty for Automatic") + Text(field == .genres ? L10n.text("Separate with commas.") + : field == .releaseDate ? L10n.text("YYYY-MM-DD") + : field == .description ? L10n.text("⌘Return to save · Empty for Automatic") : L10n.text("Empty for Automatic")) .seleneFont(14) .foregroundColor(SeleneTheme.ink60) } diff --git a/Sources/NXLibrary/Views/GameOptions/CheatsPanel.swift b/Sources/NXLibrary/Views/GameOptions/CheatsPanel.swift index 136c9c1..8f5ef8f 100644 --- a/Sources/NXLibrary/Views/GameOptions/CheatsPanel.swift +++ b/Sources/NXLibrary/Views/GameOptions/CheatsPanel.swift @@ -73,16 +73,16 @@ struct CheatsPanel: View { if let next = entry.section, next != section { lines.append(.heading(next)) } section = entry.section if let broken = entry.broken { - lines.append(.text(title: entry.name, about: "Not supported: \(broken).", chip: nil, dim: true)) + lines.append(.text(title: entry.name, about: L10n.format("Not supported: %@.", [String(describing: (broken))]), chip: nil, dim: true)) continue } switch entry.kind { case .label: lines.append(.text(title: entry.name, about: entry.note, chip: nil, dim: true)) case .master: - lines.append(.text(title: entry.name, about: entry.note, chip: "Always on", dim: false)) + lines.append(.text(title: entry.name, about: entry.note, chip: L10n.text("Always on"), dim: false)) case .cheat where entry.once: - lines.append(.text(title: entry.name, about: "Runs once: use it in the game's Cheats panel (⌘⇧C).", + lines.append(.text(title: entry.name, about: L10n.text("Runs once: use it in the game's Cheats panel (⌘⇧C)."), chip: nil, dim: true)) case .cheat: lines.append(.row(.cheat(entry.name))) @@ -94,7 +94,7 @@ struct CheatsPanel: View { lines += [.row(.add), .row(.showInFinder)] } if !state.otherBuilds.isEmpty { - lines.append(.heading("Other versions")) + lines.append(.heading(L10n.text("Other versions"))) lines += state.otherBuilds.map { .row(.otherBuild($0)) } } return lines @@ -106,7 +106,7 @@ struct CheatsPanel: View { private var notices: [String] { var notices: [String] = [] - if case .needsRebuild(let why) = context.status { notices.append(why + " Rebuild to use cheats.") } + if case .needsRebuild(let why) = context.status { notices.append(why + L10n.text(" Rebuild to use cheats.")) } if running { notices.append(CheatCopy.running(gameName: game.name)) } else if cheatState?.safeModePending == true { notices.append(CheatCopy.safeMode) } return notices @@ -129,7 +129,7 @@ struct CheatsPanel: View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { - Text("CHEATS") + Text(L10n.text("CHEATS")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(8)) @@ -158,7 +158,7 @@ struct CheatsPanel: View { .padding(.bottom, canvas.u(20)) .contentShape(Rectangle()) - SeleneHintsBar(left: [HintItem(.b, "Back", action: close)], right: [HintItem(.a, "Select")], + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"), action: close)], right: [HintItem(.a, L10n.text("Select"))], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -279,25 +279,25 @@ struct CheatsPanel: View { let writable = !running switch row { case .rebuild: - return (.build, "Rebuild", "Build a new app with this engine; the old app stays if the build fails.", nil, false, true) + return (.build, "Rebuild", L10n.text("Build a new app with this engine; the old app stays if the build fails."), nil, false, true) case .master: - return (.casino, "Cheats", "Off keeps every cheat from running, whatever is switched on.", + return (.casino, L10n.text("Cheats"), L10n.text("Off keeps every cheat from running, whatever is switched on."), cheatState?.masterOn ?? true, false, writable) case .cheat(let key): let entry = entries[key] return (nil, entry.map(CheatCopy.title(of:)) ?? key, entry?.note, cheatState?.enabled.contains(key) ?? false, false, writable) case .add: - return (.add, "Add cheats…", "A .txt cheat file, or a folder in SD-card layout. You can also drop them here.", + return (.add, L10n.text("Add cheats…"), L10n.text("A .txt cheat file, or a folder in SD-card layout. You can also drop them here."), nil, false, writable) case .turnAllOff: - return (.power, "Turn all off", "Every cheat off; the Cheats switch stays as it is.", nil, false, writable) + return (.power, L10n.text("Turn all off"), L10n.text("Every cheat off; the Cheats switch stays as it is."), nil, false, writable) case .showInFinder: - return (.folder, "Show in Finder", "The folder the game reads its cheats from.", nil, false, true) + return (.folder, "Show in Finder", L10n.text("The folder the game reads its cheats from."), nil, false, true) case .remove: - return (.delete, "Remove cheats", "This version's cheat files go to the Trash.", nil, true, writable) + return (.delete, L10n.text("Remove cheats"), L10n.text("This version's cheat files go to the Trash."), nil, true, writable) case .otherBuild(let build): - return (.history, CheatCopy.otherBuild(build), "Select to move these files to the Trash.", nil, false, writable) + return (.history, CheatCopy.otherBuild(build), L10n.text("Select to move these files to the Trash."), nil, false, writable) } } @@ -381,10 +381,10 @@ struct CheatsPanel: View { running = true SoundEffectService.shared.play(.error) } catch CheatStoreError.writeFailed(let reason) { - message = "The cheat files were not changed: \(reason)." + message = L10n.format("The cheat files were not changed: %@.", [String(describing: (reason))]) SoundEffectService.shared.play(.error) } catch { - message = "The cheat files were not changed: \(error.localizedDescription)." + message = L10n.format("The cheat files were not changed: %@.", [String(describing: (error.localizedDescription))]) SoundEffectService.shared.play(.error) } return false @@ -404,8 +404,8 @@ struct CheatsPanel: View { private func addFromOpenPanel() { let panel = NSOpenPanel() - panel.title = "Add cheats for \(game.name)" - panel.prompt = "Add" + panel.title = L10n.format("Add cheats for %@", [String(describing: (game.name))]) + panel.prompt = L10n.text("Add") panel.canChooseFiles = true panel.canChooseDirectories = true panel.allowsMultipleSelection = true @@ -452,7 +452,7 @@ struct CheatsPanel: View { default: return } guard let data = try? Data(contentsOf: found[index].url) else { - refuse("NX Library could not read the file.") + refuse(L10n.text("NX Library could not read the file.")) return } askConfirm(.install(data: data, file: file, unnamed: unnamed, replacing: cheatState?.file != nil)) diff --git a/Sources/NXLibrary/Views/GameOptions/GameOptionsView.swift b/Sources/NXLibrary/Views/GameOptions/GameOptionsView.swift index 1f7e878..1f1e0bf 100644 --- a/Sources/NXLibrary/Views/GameOptions/GameOptionsView.swift +++ b/Sources/NXLibrary/Views/GameOptions/GameOptionsView.swift @@ -91,7 +91,7 @@ struct GameOptionsView: View { sourcesDetached: !GameResourceState.sourcesAvailable(for: game), // Mirrors GameDetailView.toolchainBlocked: a known report that does not pass, without the override. toolchainBlocked: viewModel.isCheckingEnvironment || (viewModel.compatibilityReport != nil && !viewModel.canPassToolchainGate), - toolchainHint: viewModel.isCheckingEnvironment ? "Checking environment" : viewModel.toolchainGateSummary?.buildHint, + toolchainHint: viewModel.isCheckingEnvironment ? L10n.text("Checking environment") : viewModel.toolchainGateSummary?.buildHint, hasStorePage: viewModel.storeMetadata[game.id]?.storeURL != nil, gameName: game.name, appPath: game.url.path, @@ -127,7 +127,7 @@ struct GameOptionsView: View { VStack(spacing: 0) { column(p) // Where the screen underneath had its hints (which hide while this is open). - SeleneHintsBar(left: [HintItem(.b, "Back")], right: rightHints(p), + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"))], right: rightHints(p), bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -211,7 +211,7 @@ struct GameOptionsView: View { let entries = p.entries(level: level, origin: origin) let focused = focusedRow return VStack(alignment: .leading, spacing: 0) { - Text("GAME OPTIONS") + Text(L10n.text("GAME OPTIONS")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(8)) @@ -334,8 +334,8 @@ struct GameOptionsView: View { /// Y opens the focused row's explanation, when it has one. private func rightHints(_ p: GameOptionsPresentation) -> [HintItem] { - guard confirm == nil, let row = focusedRow, p.info(row) != nil else { return [HintItem(.a, "Select")] } - return [HintItem(.y, infoRow == row ? "Hide info" : "Info"), HintItem(.a, "Select")] + guard confirm == nil, let row = focusedRow, p.info(row) != nil else { return [HintItem(.a, L10n.text("Select"))] } + return [HintItem(.y, infoRow == row ? L10n.text("Hide info") : L10n.text("Info")), HintItem(.a, L10n.text("Select"))] } /// Y: the focused row's explanation, open or closed. @@ -391,7 +391,7 @@ struct GameOptionsView: View { } private func choiceText(_ row: GameOptionsRow) -> String { - if let (setting, value) = settingValue(row) { return GameSettingValues.label(setting, value) } + if let (setting, value) = settingValue(row) { return L10n.text(GameSettingValues.label(setting, value)) } switch row { case .consoleMode: return GameOptionsValues.consoleModeLabel(docked: config.isDockedMode) @@ -431,7 +431,7 @@ struct GameOptionsView: View { return (copy.title, copy.message, copy.confirmTitle) case .removeSources: let info = presentation.info(.removeSources) ?? "" - return ("Remove sources and build cache?", + return (L10n.text("Remove sources and build cache?"), info + "\n\n" + viewModel.sourceRemovalSummary(for: current), "Remove") } diff --git a/Sources/NXLibrary/Views/GameOptions/ModsPanel.swift b/Sources/NXLibrary/Views/GameOptions/ModsPanel.swift index d7f4591..f2fb40b 100644 --- a/Sources/NXLibrary/Views/GameOptions/ModsPanel.swift +++ b/Sources/NXLibrary/Views/GameOptions/ModsPanel.swift @@ -60,7 +60,7 @@ struct ModsPanel: View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { - Text("MODS") + Text(L10n.text("MODS")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(8)) @@ -83,7 +83,7 @@ struct ModsPanel: View { .padding(.bottom, canvas.u(20)) .contentShape(Rectangle()) - SeleneHintsBar(left: [HintItem(.b, "Back", action: close)], right: hints, + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"), action: close)], right: hints, bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -111,9 +111,9 @@ struct ModsPanel: View { } private var hints: [HintItem] { - guard let mod = focusedMod else { return [HintItem(.a, "Select")] } - var items = [HintItem(nil, "◂ ▸ Move"), HintItem(.y, "Remove", action: askRemove)] - items.append(HintItem(.a, mod.romfs == nil ? "Select" : mod.isOn ? "Turn off" : "Turn on")) + guard let mod = focusedMod else { return [HintItem(.a, L10n.text("Select"))] } + var items = [HintItem(nil, L10n.text("◂ ▸ Move")), HintItem(.y, L10n.text("Remove"), action: askRemove)] + items.append(HintItem(.a, mod.romfs == nil ? L10n.text("Select") : mod.isOn ? L10n.text("Turn off") : L10n.text("Turn on"))) return items } @@ -148,7 +148,7 @@ struct ModsPanel: View { ScrollView { VStack(alignment: .leading, spacing: canvas.u(8)) { if mods.isEmpty { - SeleneOptionRow(icon: nil, title: "No mods yet", about: ModsCopy.empty, focused: false, enabled: false) {} + SeleneOptionRow(icon: nil, title: L10n.text("No mods yet"), about: ModsCopy.empty, focused: false, enabled: false) {} } ForEach(Array(rows.enumerated()), id: \.element) { index, row in rowView(row, focused: index == focus) @@ -203,7 +203,7 @@ struct ModsPanel: View { if name.contains(":") { return (.puzzle, name, ModsCopy.colon, nil, false) } return (.puzzle, name, nil, mod.isOn, !running) case .openFolder: - return (.folder, "Open mods folder", "Put each mod in its own folder here.", nil, true) + return (.folder, L10n.text("Open mods folder"), L10n.text("Put each mod in its own folder here."), nil, true) } } @@ -213,9 +213,9 @@ struct ModsPanel: View { .ignoresSafeArea() .contentShape(Rectangle()) .onTapGesture { dismissConfirm() } - SeleneConfirmDialog(title: "Move \(name) to the Trash?", - message: "The game stops loading it. You can put it back from the Trash.", - confirmTitle: "Move to Trash", focus: $confirmFocus, + SeleneConfirmDialog(title: L10n.format("Move %@ to the Trash?", [String(describing: (name))]), + message: L10n.text("The game stops loading it. You can put it back from the Trash."), + confirmTitle: L10n.text("Move to Trash"), focus: $confirmFocus, onCancel: dismissConfirm, onConfirm: runRemove) } } @@ -384,18 +384,28 @@ struct ModsPanel: View { enum ModsCopy { static func header(on: Int, total: Int) -> String { switch (on, total) { - case (_, 0): return "No mods in this game's folder" - case (0, _): return total == 1 ? "1 mod, off" : "\(total) mods, all off" - default: return "\(on) of \(total) on · the upper mod wins when two change the same file" + case (_, 0): return L10n.text("No mods in this game's folder") + case (0, _): return total == 1 ? "1 mod, off" : L10n.format("%@ mods, all off", [String(describing: (total))]) + default: return L10n.format("%@ of %@ on · the upper mod wins when two change the same file", [String(describing: (on)), String(describing: (total))]) } } - static func running(gameName: String) -> String { "\(gameName) is running. Quit it to change its mods." } + static func running(gameName: String) -> String { L10n.format("%@ is running. Quit it to change its mods.", [String(describing: (gameName))]) } - static let empty = "Open the mods folder and put each mod in its own folder, with a romfs folder inside." - static let notAMod = "Not a mod: needs a romfs folder." - static let notAFolder = "Not a mod: unzip it into its own folder first." - static let colon = "Rename this folder: its name can't contain ':'." - static let help = "Mods change the game's files only: mods that patch code (exefs, such as 60 fps patches) don't work. " - + "Changes apply at the next launch. If a mod changes nothing, rebuild the game." + static var empty: String { + L10n.text("Open the mods folder and put each mod in its own folder, with a romfs folder inside.") + } + static var notAMod: String { + L10n.text("Not a mod: needs a romfs folder.") + } + static var notAFolder: String { + L10n.text("Not a mod: unzip it into its own folder first.") + } + static var colon: String { + L10n.text("Rename this folder: its name can't contain ':'.") + } + static var help: String { + L10n.text("Mods change the game's files only: mods that patch code (exefs, such as 60 fps patches) don't work. ") + + L10n.text("Changes apply at the next launch. If a mod changes nothing, rebuild the game.") + } } diff --git a/Sources/NXLibrary/Views/GameOptions/MoreSettingsPanel.swift b/Sources/NXLibrary/Views/GameOptions/MoreSettingsPanel.swift index f15611e..e3c480e 100644 --- a/Sources/NXLibrary/Views/GameOptions/MoreSettingsPanel.swift +++ b/Sources/NXLibrary/Views/GameOptions/MoreSettingsPanel.swift @@ -32,7 +32,7 @@ struct MoreSettingsPanel: View { SeleneTheme.scrim65.ignoresSafeArea().contentShape(Rectangle()).onTapGesture { onClose() } VStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { - Text("MORE SETTINGS").seleneFont(15, .semibold).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("MORE SETTINGS")).seleneFont(15, .semibold).foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(8)) Text(game.name).seleneFont(40).foregroundColor(SeleneTheme.ink).lineLimit(1) .padding(.leading, canvas.u(6)) @@ -42,7 +42,7 @@ struct MoreSettingsPanel: View { .frame(width: canvas.u(1040), alignment: .leading) .frame(maxHeight: .infinity, alignment: .top) .padding(.top, canvas.u(90)).padding(.bottom, canvas.u(20)) - SeleneHintsBar(left: [HintItem(.b, "Back", action: onClose)], right: [HintItem(.a, "Select")], + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"), action: onClose)], right: [HintItem(.a, L10n.text("Select"))], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -87,15 +87,15 @@ struct MoreSettingsPanel: View { } private func about(_ setting: LauncherSetting) -> String { - if isFixed(setting) { return "Set in env.txt" } - if let note = setting.note, !note.isEmpty { return note } - return setting.section + if isFixed(setting) { return L10n.text("Set in env.txt") } + if let note = setting.note, !note.isEmpty { return L10n.text(note) } + return L10n.text(setting.section) } private func rowView(_ setting: LauncherSetting, focused: Bool) -> some View { let current = value(setting) - let label = GameSettingValues.label(setting, current) - return SeleneOptionRow(icon: nil, title: setting.label, about: about(setting), focused: focused, + let label = L10n.text(GameSettingValues.label(setting, current)) + return SeleneOptionRow(icon: nil, title: L10n.text(setting.label), about: about(setting), focused: focused, enabled: !isFixed(setting)) { if setting.kind == "toggle" { SeleneSwitch(isOn: .constant(current == setting.on)).allowsHitTesting(false) @@ -104,7 +104,7 @@ struct MoreSettingsPanel: View { } } .accessibilityElement(children: .ignore) - .accessibilityLabel(setting.label) + .accessibilityLabel(L10n.text(setting.label)) .accessibilityHint(about(setting)) .accessibilityValue(label) .accessibilityAddTraits(.isButton) diff --git a/Sources/NXLibrary/Views/GameOptions/SaveImportDetails.swift b/Sources/NXLibrary/Views/GameOptions/SaveImportDetails.swift index b83859a..4cc9024 100644 --- a/Sources/NXLibrary/Views/GameOptions/SaveImportDetails.swift +++ b/Sources/NXLibrary/Views/GameOptions/SaveImportDetails.swift @@ -19,7 +19,7 @@ struct SaveImportDetails: View { private var content: some View { VStack(spacing: canvas.u(18)) { VStack(spacing: canvas.u(13)) { - endpoint("From", folder: source) + endpoint(L10n.text("From"), folder: source) SaveTransferArrow() endpoint("To", folder: destination) } @@ -30,7 +30,7 @@ struct SaveImportDetails: View { .fill(SeleneTheme.glassLight)) VStack(spacing: canvas.u(8)) { - Text("Your current save is backed up before replacement.") + Text(L10n.text("Your current save is backed up before replacement.")) .seleneFont(17, .medium) .foregroundColor(SeleneTheme.ink) Text(SavePresentation.importWarning) diff --git a/Sources/NXLibrary/Views/GameOptions/SavesPanel.swift b/Sources/NXLibrary/Views/GameOptions/SavesPanel.swift index 009c1a7..5e8438e 100644 --- a/Sources/NXLibrary/Views/GameOptions/SavesPanel.swift +++ b/Sources/NXLibrary/Views/GameOptions/SavesPanel.swift @@ -40,7 +40,7 @@ struct SavesPanel: View { SeleneTheme.scrim65.ignoresSafeArea().contentShape(Rectangle()).onTapGesture { close() } VStack(spacing: 0) { VStack(alignment: .leading, spacing: 0) { - Text("SAVES").seleneFont(15, .semibold).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("SAVES")).seleneFont(15, .semibold).foregroundColor(SeleneTheme.ink60) .padding(.leading, canvas.u(8)) Text(game.name).seleneFont(40).foregroundColor(SeleneTheme.ink).lineLimit(1) .padding(.leading, canvas.u(6)) @@ -51,7 +51,7 @@ struct SavesPanel: View { .frame(width: canvas.u(1040), alignment: .leading) .frame(maxHeight: .infinity, alignment: .top) .padding(.top, canvas.u(90)).padding(.bottom, canvas.u(20)) - SeleneHintsBar(left: [HintItem(.b, "Back", action: close)], right: [HintItem(.a, "Select")], + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"), action: close)], right: [HintItem(.a, L10n.text("Select"))], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) }.frame(maxWidth: .infinity, maxHeight: .infinity) .allowsHitTesting(!confirming) @@ -66,7 +66,7 @@ struct SavesPanel: View { } ZStack { if model.importSuccess != nil && !confirming { - SeleneSuccessNotice(title: "Imported successfully") + SeleneSuccessNotice(title: L10n.text("Imported successfully")) .transition(.opacity) } } @@ -93,7 +93,7 @@ struct SavesPanel: View { } private var summary: some View { HStack(spacing: canvas.u(12)) { - Text([game.versionString.map { "v" + $0 }, model.running ? "Game running" : SavePresentation.size(model.currentBytes)].compactMap { $0 }.joined(separator: " · ")) + Text([game.versionString.map { "v" + $0 }, model.running ? L10n.text("Game running") : SavePresentation.size(model.currentBytes)].compactMap { $0 }.joined(separator: " · ")) .seleneFont(15).foregroundColor(SeleneTheme.ink60).lineLimit(1) Spacer() // Always occupies the same space: refresh never changes header/list geometry. @@ -101,7 +101,7 @@ struct SavesPanel: View { .frame(width: canvas.u(20), height: canvas.u(20)) .opacity(model.busy ? 1 : 0) .accessibilityHidden(!model.busy) - .accessibilityLabel("Working") + .accessibilityLabel(L10n.text("Working")) } .padding(.horizontal, canvas.u(8)) .padding(.top, canvas.u(6)).padding(.bottom, canvas.u(14)) @@ -132,27 +132,27 @@ struct SavesPanel: View { } private var importValue: String? { if model.running { return "Game running" } - if model.recoveryRequired { return "Recovery needed" } - return model.context?.cloudReason == nil ? nil : "Cloud on" + if model.recoveryRequired { return L10n.text("Recovery needed") } + return model.context?.cloudReason == nil ? nil : L10n.text("Cloud on") } private func description(_ row: Row) -> (title: String, value: String?, enabled: Bool) { switch row { - case .open: return ("Open folder", nil, model.context != nil && !model.busy) - case .backup: return ("Back up now", model.running ? "Game running" : nil, model.context != nil && !model.running && !model.recoveryRequired && !model.busy) - case .target: return ("Save type", SavePresentation.slotName(target), model.context != nil && !model.busy && !model.running && !model.recoveryRequired) - case .add: return ("Import save…", importValue, model.importAction != .unavailable) - case .refresh: return (model.recoveryRequired ? "Retry recovery" : "Refresh", nil, !model.busy) - case .backups: return ("Backup folder", model.latestBackupDate.map(SavePresentation.date) ?? "No backups", !model.busy) + case .open: return (L10n.text("Open folder"), nil, model.context != nil && !model.busy) + case .backup: return (L10n.text("Back up now"), model.running ? L10n.text("Game running") : nil, model.context != nil && !model.running && !model.recoveryRequired && !model.busy) + case .target: return (L10n.text("Save type"), SavePresentation.slotName(target), model.context != nil && !model.busy && !model.running && !model.recoveryRequired) + case .add: return (L10n.text("Import save…"), importValue, model.importAction != .unavailable) + case .refresh: return (model.recoveryRequired ? L10n.text("Retry recovery") : L10n.text("Refresh"), nil, !model.busy) + case .backups: return (L10n.text("Backup folder"), model.latestBackupDate.map(SavePresentation.date) ?? L10n.text("No backups"), !model.busy) } } private func subtitle(_ row: Row) -> String { switch row { - case .open: return "Open the current save in Finder." - case .backup: return "Keep a copy of your current progress." - case .target: return "Destination for imports only." - case .add: return "Choose a save folder to import." - case .refresh: return "Check saves and backups again." - case .backups: return "Open this game's backups in Finder." + case .open: return L10n.text("Open the current save in Finder.") + case .backup: return L10n.text("Keep a copy of your current progress.") + case .target: return L10n.text("Destination for imports only.") + case .add: return L10n.text("Choose a save folder to import.") + case .refresh: return L10n.text("Check saves and backups again.") + case .backups: return L10n.text("Open this game's backups in Finder.") } } private func rowView(_ row: Row, focused: Bool) -> some View { @@ -178,33 +178,33 @@ struct SavesPanel: View { @ViewBuilder private var confirmation: some View { if let issue = model.issue { - SeleneChoiceDialog(title: issue.title, message: issue.message, choices: ["Back"], + SeleneChoiceDialog(title: issue.title, message: issue.message, choices: [L10n.text("Back")], focus: .constant(0), onChoose: { _ in model.dismissIssue() }) } else if let preview = model.preview, let context = model.context { - SeleneConfirmDialog(title: "Import this save?", + SeleneConfirmDialog(title: L10n.text("Import this save?"), message: SavePresentation.importSummary(preview, version: game.versionString), - confirmTitle: "Import", focus: $confirmFocus, + confirmTitle: L10n.text("Import"), focus: $confirmFocus, onCancel: { if !model.busy { model.cancelPreview() } }, onConfirm: { if !model.busy { model.confirmImport() } }) { SaveImportDetails(source: preview.source, destination: context.location.directory(preview.slot)) } .disabled(model.busy) } else if let snapshot = model.restorePreview?.snapshot { - SeleneConfirmDialog(title: "Restore this backup?", message: SavePresentation.restoreMessage(snapshot, installedVersion: game.versionString), - confirmTitle: "Restore", focus: $confirmFocus, + SeleneConfirmDialog(title: L10n.text("Restore this backup?"), message: SavePresentation.restoreMessage(snapshot, installedVersion: game.versionString), + confirmTitle: L10n.text("Restore"), focus: $confirmFocus, onCancel: { if !model.busy { model.cancelPreview() } }, onConfirm: { if !model.busy { model.confirmRestore() } }).disabled(model.busy) } else if cloudPrompt { - SeleneConfirmDialog(title: "Cloud saves on", message: cloudHelp.message, confirmTitle: cloudHelp.button, + SeleneConfirmDialog(title: L10n.text("Cloud saves on"), message: cloudHelp.message, confirmTitle: cloudHelp.button, focus: $confirmFocus, onCancel: { cloudPrompt = false }, onConfirm: resolveCloud) } } private var cloudHelp: (message: String, button: String) { switch model.context?.cloudReason { - case "NX Library → Settings → Games": return ("Turn off Cloud saves in Settings → Games to import or restore.", "Settings") - case "the game's env.txt", "the app's env.txt": return ("Turn off cloud sync in this configuration file first.", "Show file") - case "the game's Cloud saves settings": return ("Turn off Cloud saves in the game's own settings first.", "OK") - default: return ("Turn off cloud sync in the launch environment first.", "OK") + case "NX Library → Settings → Games": return (L10n.text("Turn off Cloud saves in Settings → Games to import or restore."), "Settings") + case "the game's env.txt", "the app's env.txt": return (L10n.text("Turn off cloud sync in this configuration file first."), L10n.text("Show file")) + case "the game's Cloud saves settings": return (L10n.text("Turn off Cloud saves in the game's own settings first."), "OK") + default: return (L10n.text("Turn off cloud sync in the launch environment first."), "OK") } } private func resolveCloud() { diff --git a/Sources/NXLibrary/Views/Launch/IntroView.swift b/Sources/NXLibrary/Views/Launch/IntroView.swift index 59075fd..a1be8b3 100644 --- a/Sources/NXLibrary/Views/Launch/IntroView.swift +++ b/Sources/NXLibrary/Views/Launch/IntroView.swift @@ -23,7 +23,7 @@ struct IntroView: View { SeleneBackdrop() } .accessibilityElement() - .accessibilityLabel("NX Library") + .accessibilityLabel(L10n.text("NX Library")) .opacity(leaving ? 0 : 1) .ignoresSafeArea() .contentShape(Rectangle()) diff --git a/Sources/NXLibrary/Views/Launch/LaunchScreenView.swift b/Sources/NXLibrary/Views/Launch/LaunchScreenView.swift index ea117bf..4de871f 100644 --- a/Sources/NXLibrary/Views/Launch/LaunchScreenView.swift +++ b/Sources/NXLibrary/Views/Launch/LaunchScreenView.swift @@ -59,7 +59,7 @@ struct LaunchScreenView: View { .contentShape(Rectangle()) .onTapGesture { finish() } .accessibilityElement(children: .ignore) - .accessibilityLabel("Starting \(game.name)") + .accessibilityLabel(L10n.format("Starting %@", [String(describing: (game.name))])) .onAppear(perform: appear) .onChange(of: state.isReady) { ready in if ready { finish() } diff --git a/Sources/NXLibrary/Views/MainView.swift b/Sources/NXLibrary/Views/MainView.swift index fa521d2..4b285b9 100644 --- a/Sources/NXLibrary/Views/MainView.swift +++ b/Sources/NXLibrary/Views/MainView.swift @@ -1,6 +1,12 @@ import SwiftUI +import NXLibraryCore public struct MainView: View { - public init() {} - public var body: some View { AppShellView() } + private let settingsStore: AppSettingsStore + + public init(settingsStore: AppSettingsStore) { + self.settingsStore = settingsStore + } + + public var body: some View { AppShellView(settingsStore: settingsStore) } } diff --git a/Sources/NXLibrary/Views/Search/FindGameView.swift b/Sources/NXLibrary/Views/Search/FindGameView.swift index 0128b06..f1723b3 100644 --- a/Sources/NXLibrary/Views/Search/FindGameView.swift +++ b/Sources/NXLibrary/Views/Search/FindGameView.swift @@ -49,8 +49,8 @@ struct FindGameView: View { VStack { Spacer() SeleneHintsBar(right: [ - HintItem(.b, "Close") { close() }, - HintItem(.a, "Select") + HintItem(.b, L10n.text("Close")) { close() }, + HintItem(.a, L10n.text("Select")) ], bottom: SeleneBottomScrim.hintsBottom) } } @@ -70,7 +70,7 @@ struct FindGameView: View { HStack(spacing: canvas.u(14)) { MaterialIconView(.search, size: 24) .foregroundColor(SeleneTheme.ink60) - TextField("Find a game", text: $query) + TextField(L10n.text("Find a game"), text: $query) .textFieldStyle(.plain) .seleneFont(19) .foregroundColor(SeleneTheme.ink) @@ -90,7 +90,7 @@ struct FindGameView: View { @ViewBuilder private var grid: some View { let games = results if !query.isEmpty && games.isEmpty { - Text("No games match.") + Text(L10n.text("No games match.")) .seleneFont(19) .foregroundColor(SeleneTheme.ink60) } else { diff --git a/Sources/NXLibrary/Views/Settings/LocalePicker.swift b/Sources/NXLibrary/Views/Settings/LocalePicker.swift new file mode 100644 index 0000000..19e405a --- /dev/null +++ b/Sources/NXLibrary/Views/Settings/LocalePicker.swift @@ -0,0 +1,157 @@ +import SwiftUI +import AppKit +import NXLibraryCore + +enum LocalePickerMode { case language, region } + +/// Direct language selection and searchable regional formats share the same input behavior. +struct LocalePicker: View { + let mode: LocalePickerMode + @ObservedObject var settings: AppSettingsStore + let canHandleInput: () -> Bool + let onClose: () -> Void + @ObservedObject private var localization = AppLocalization.shared + @Environment(\.seleneCanvas) private var canvas + @Environment(\.seleneReduceMotion) private var reduceMotion + @State private var query = "" + @State private var focus = 0 + @State private var keyMonitor: Any? + @FocusState private var searchFocused: Bool + + private struct Choice { let id: String; let name: String } + private var choices: [Choice] { + if mode == .language { + return AppLanguage.allCases.map { Choice(id: $0.rawValue, name: $0.nativeName) } + } + return (query.isEmpty ? [Choice(id: "auto", name: L10n.text("Automatic (Mac)"))] : []) + + AppRegionOption.matching(query, context: localization.context).map { Choice(id: $0.id, name: $0.name) } + } + private var selectedID: String { + mode == .language ? settings.settings.appLanguage.rawValue : settings.settings.appRegion ?? "auto" + } + private var formatPreview: String { + let rows = choices + let id = rows.indices.contains(focus) ? rows[focus].id : selectedID + let locale = AppLocale(language: localization.context.language, region: id == "auto" ? nil : id).formatLocale + return L10n.format("Example: %@ · %@ · %@", [ + Date().formatted(.dateTime.day().month(.twoDigits).year().locale(locale)), + 12_345.67.formatted(.number.locale(locale)), + Int64(1_500_000_000).formatted(.byteCount(style: .file).locale(locale)) + ]) + } + + var body: some View { + let rows = choices + ZStack { + SeleneTheme.scrim65.ignoresSafeArea().onTapGesture(perform: onClose) + VStack(alignment: .leading, spacing: canvas.u(16)) { + Text(L10n.text(mode == .language ? "Choose app language" : "Regional formats")).seleneFont(32, .semibold).foregroundColor(SeleneTheme.ink) + Text(L10n.text(mode == .language ? "Menus, genres and available game descriptions. The games’ own language stays separate." : "Dates, numbers and sizes only. Game descriptions follow the app language.")) + .seleneFont(16).foregroundColor(SeleneTheme.ink60) + if mode == .region { + Text(formatPreview).seleneFont(16).foregroundColor(SeleneTheme.ink) + } + if mode == .region { + TextField(L10n.text("Find a region"), text: $query) + .textFieldStyle(.plain).seleneFont(20).focused($searchFocused) + .onSubmit { choose() } + .padding(canvas.u(14)).background(SeleneTheme.glass) + .clipShape(RoundedRectangle(cornerRadius: canvas.u(12))) + } + ScrollViewReader { proxy in + ScrollView { + LazyVStack(spacing: canvas.u(5)) { + if rows.isEmpty { + Text(L10n.text("No matching regions.")).seleneFont(18).foregroundColor(SeleneTheme.ink60) + } + ForEach(rows.indices, id: \.self) { index in + Button { focus = index; choose() } label: { + HStack { + Text(rows[index].name).seleneFont(20) + Spacer() + if rows[index].id == selectedID { + MaterialIconView(.check, size: 22) + } + if mode == .region, rows[index].id != "auto" { Text(rows[index].id).seleneFont(15).foregroundColor(SeleneTheme.ink60) } + } + .foregroundColor(SeleneTheme.ink) + .padding(canvas.u(14)) + .background(index == focus ? SeleneTheme.glass : Color.clear) + .seleneRowFocus(index == focus, radius: canvas.u(12)) + .contentShape(Rectangle()) + } + .buttonStyle(.plain).id(index) + .accessibilityAddTraits(rows[index].id == selectedID ? .isSelected : []) + } + } + .padding(canvas.u(4)) + } + .scrollIndicators(.hidden) + .onChange(of: focus) { index in + withAnimation(reduceMotion ? nil : SeleneTheme.quick) { proxy.scrollTo(index, anchor: .center) } + } + .onAppear { + focus = rows.firstIndex(where: { $0.id == selectedID }) ?? 0 + proxy.scrollTo(focus, anchor: .center) + } + } + .frame(height: canvas.u(420)) + HStack { + Spacer() + Button(L10n.text("Cancel"), action: onClose).buttonStyle(.plain).seleneFont(19, .semibold) + .foregroundColor(SeleneTheme.glyph).padding(canvas.u(8)) + } + } + .padding(canvas.u(32)) + .frame(width: canvas.u(800)) + .background(SeleneTheme.glassSolid) + .clipShape(RoundedRectangle(cornerRadius: canvas.u(24))) + .overlay(RoundedRectangle(cornerRadius: canvas.u(24)).strokeBorder(SeleneTheme.ink15, lineWidth: 1)) + } + .onChange(of: query) { _ in focus = 0 } + .onAppear(perform: installKeys) + .onDisappear { if let keyMonitor { NSEvent.removeMonitor(keyMonitor) } } + .onReceive(NotificationCenter.default.publisher(for: .nxLocalePickerGamepadAction)) { note in + guard canHandleInput(), let action = note.object as? GamepadAction else { return } + switch action { + case .navigateUp: move(-1) + case .navigateDown: move(1) + case .confirm: choose() + case .cancel: onClose() + default: break + } + } + } + + private func move(_ delta: Int) { + searchFocused = false + focus = min(max(0, focus + delta), max(0, choices.count - 1)) + } + + private func choose() { + let rows = choices + guard rows.indices.contains(focus) else { return } + let id = rows[focus].id + if mode == .language, let language = AppLanguage(rawValue: id) { + settings.settings.appLanguage = language + } else if mode == .region { + settings.settings.appRegion = id == "auto" ? nil : id + } + onClose() + } + + private func installKeys() { + keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + guard canHandleInput() else { return event } + switch event.keyCode { + case 53: onClose() + case 125: move(1) + case 126: move(-1) + case 36, 76: choose() + case 49 where !searchFocused: choose() + default: return event + } + return nil + } + } +} diff --git a/Sources/NXLibrary/Views/Settings/SettingsAboutSection.swift b/Sources/NXLibrary/Views/Settings/SettingsAboutSection.swift index c52d69a..8613a66 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsAboutSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsAboutSection.swift @@ -12,29 +12,29 @@ struct SettingsAboutSection: View { private let credits: [(name: String, role: String, url: String)] = [ ("seedds", "nxnative engine, NVN/Metal host and ARM64 rewriter", "https://github.com/seedds/nxnative"), - ("d2r-mobile", "Foundational native execution architecture", "https://github.com/jhaizhou-ops/d2r-mobile"), - ("Ryujinx Team", "Maxwell shader translator and SPIR-V generator", "https://github.com/Ryujinx/Ryujinx"), - ("Atmosphère NX", "Mesosphère kernel synchronization semantics", "https://github.com/Atmosphere-NX/Atmosphere"), - ("LibHac", "NSP reading and PFS/HFS layered filesystem", "https://github.com/Thealexbarney/LibHac"), - ("Khronos Group", "SPIRV-Cross Metal Shading Language compiler", "https://github.com/KhronosGroup/SPIRV-Cross"), - ("GameTDB", "Switch box covers", "https://www.gametdb.com"), - ("Title DB", "Game names, icons and banners", "https://github.com/blawar/titledb"), - ("LaunchBox Games Database", "Box covers, logos, backgrounds and square art in Change artwork", "https://gamesdb.launchbox-app.com"), - ("SteamGridDB", "Community covers, backgrounds, logos and icons, with your API key", "https://www.steamgriddb.com"), - ("NX2Apple", "Key check, failure explanations and diagnostics, adapted (MIT)", "https://github.com/LNPANDY/NX2Apple") + ("d2r-mobile", L10n.text("Foundational native execution architecture"), "https://github.com/jhaizhou-ops/d2r-mobile"), + ("Ryujinx Team", L10n.text("Maxwell shader translator and SPIR-V generator"), "https://github.com/Ryujinx/Ryujinx"), + ("Atmosphère NX", L10n.text("Mesosphère kernel synchronization semantics"), "https://github.com/Atmosphere-NX/Atmosphere"), + ("LibHac", L10n.text("NSP reading and PFS/HFS layered filesystem"), "https://github.com/Thealexbarney/LibHac"), + ("Khronos Group", L10n.text("SPIRV-Cross Metal Shading Language compiler"), "https://github.com/KhronosGroup/SPIRV-Cross"), + ("GameTDB", L10n.text("Switch box covers"), "https://www.gametdb.com"), + ("Title DB", L10n.text("Game names, icons and banners"), "https://github.com/blawar/titledb"), + ("LaunchBox Games Database", L10n.text("Box covers, logos, backgrounds and square art in Change artwork"), "https://gamesdb.launchbox-app.com"), + ("SteamGridDB", L10n.text("Community covers, backgrounds, logos and icons, with your API key"), "https://www.steamgriddb.com"), + ("NX2Apple", L10n.text("Key check, failure explanations and diagnostics, adapted (MIT)"), "https://github.com/LNPANDY/NX2Apple") ] var body: some View { VStack(spacing: 12) { - SelenePanel("NX Library") { - Text("A personal macOS front-end for building and launching your own Switch games with nxnative.") + SelenePanel(L10n.text("NX Library")) { + Text(L10n.text("A personal macOS front-end for building and launching your own Switch games with nxnative.")) .seleneFont(16) .foregroundColor(SeleneTheme.ink90) .fixedSize(horizontal: false, vertical: true) - Text("Version \(version) · Build \(buildDate) · Engine \(seleneShort(viewModel.pinnedEngineCommit))") + Text(L10n.format("Version %@ · Build %@ · Engine %@", [String(describing: (version)), String(describing: (buildDate)), String(describing: (seleneShort(viewModel.pinnedEngineCommit)))])) .seleneFont(13, .regular, design: .monospaced) .foregroundColor(SeleneTheme.ink60) - Text("Play time is tracked only for launches from NX Library. Not affiliated with Nintendo.") + Text(L10n.text("Play time is tracked only for launches from NX Library. Not affiliated with Nintendo.")) .seleneFont(13) .foregroundColor(SeleneTheme.ink60) updateRow @@ -42,7 +42,7 @@ struct SettingsAboutSection: View { copyDiagnostics() } label: { HStack(spacing: 4) { - Text(didCopyDiagnostics ? "Copied" : "Copy diagnostics") + Text(didCopyDiagnostics ? L10n.text("Copied") : L10n.text("Copy diagnostics")) Image(systemName: didCopyDiagnostics ? "checkmark" : "doc.on.doc") } .seleneFont(14, .semibold) @@ -52,10 +52,10 @@ struct SettingsAboutSection: View { .padding(6) .seleneRowFocus(focusedRowTitle == "Copy diagnostics") .id("selene-row-Copy diagnostics") - .accessibilityHint("A report for an issue: versions, setup and the last porting. No keys.") + .accessibilityHint(L10n.text("A report for an issue: versions, setup and the last porting. No keys.")) } - SelenePanel("Credits") { + SelenePanel(L10n.text("Credits")) { LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 8) { ForEach(credits, id: \.name) { credit in Button { @@ -88,8 +88,8 @@ struct SettingsAboutSection: View { } } - SelenePanel("Visual direction") { - Text("Inspired by Selene (MIT) by Matthew Silva.") + SelenePanel(L10n.text("Visual direction")) { + Text(L10n.text("Inspired by Selene (MIT) by Matthew Silva.")) .seleneFont(16, .semibold) .foregroundColor(SeleneTheme.ink) Button { @@ -108,24 +108,24 @@ struct SettingsAboutSection: View { .padding(6) .seleneRowFocus(focusedRowTitle == "Selene") .id("selene-row-Selene") - Text("Selene is a Playnite theme for Windows. No Selene name, logo, wallpaper, avatar or sound is used as NX Library branding; this is a visual adaptation in SwiftUI.") + Text(L10n.text("Selene is a Playnite theme for Windows. No Selene name, logo, wallpaper, avatar or sound is used as NX Library branding; this is a visual adaptation in SwiftUI.")) .seleneFont(13) .foregroundColor(SeleneTheme.ink60) .fixedSize(horizontal: false, vertical: true) } - SelenePanel("Nintendo Store data") { - Text("Descriptions, genres and artwork are fetched from the public Nintendo Store catalog for personal use, cached locally for seven days, and never committed or redistributed.") + SelenePanel(L10n.text("Nintendo Store data")) { + Text(L10n.text("Descriptions, genres and artwork are fetched from the public Nintendo Store catalog for personal use, cached locally for seven days, and never committed or redistributed.")) .seleneFont(15) .foregroundColor(SeleneTheme.ink90) .fixedSize(horizontal: false, vertical: true) - Text("The app ships no keys, ROMs, game art or Nintendo UI assets.") + Text(L10n.text("The app ships no keys, ROMs, game art or Nintendo UI assets.")) .seleneFont(13) .foregroundColor(SeleneTheme.ink60) } - SelenePanel("Licenses") { - Text("NX Library is personal software released under the MIT license. nxnative, Ryujinx, SPIR-V Cross, LibHac and the other dependencies retain their own licenses through the engine.") + SelenePanel(L10n.text("Licenses")) { + Text(L10n.text("NX Library is personal software released under the MIT license. nxnative, Ryujinx, SPIR-V Cross, LibHac and the other dependencies retain their own licenses through the engine.")) .seleneFont(15) .foregroundColor(SeleneTheme.ink90) .fixedSize(horizontal: false, vertical: true) @@ -134,7 +134,7 @@ struct SettingsAboutSection: View { openProjectDocs() } label: { HStack(spacing: 4) { - Text("Open project docs") + Text(L10n.text("Open project docs")) Image(systemName: "folder") } .seleneFont(14, .semibold) @@ -145,7 +145,7 @@ struct SettingsAboutSection: View { .seleneRowFocus(focusedRowTitle == "Open project docs") .id("selene-row-Open project docs") - Text("This app is not affiliated with Nintendo.") + Text(L10n.text("This app is not affiliated with Nintendo.")) .seleneFont(13, .semibold) .foregroundColor(SeleneTheme.ink60) } @@ -153,6 +153,7 @@ struct SettingsAboutSection: View { .onAppear(perform: publishActions) .onChange(of: updater.state) { _ in publishActions() } .onChange(of: viewModel.isBuilding) { _ in publishActions() } + .onChange(of: viewModel.isCleaningStorage) { _ in publishActions() } .onChange(of: viewModel.hasActiveSaveOperations) { _ in publishActions() } .onChange(of: viewModel.isUpdatingApp) { _ in publishActions() } .onChange(of: viewModel.runningGameCount) { _ in publishActions() } @@ -215,35 +216,35 @@ struct SettingsAboutSection: View { private var updateText: String { switch updater.state { case .hidden: return "" - case .developer: return "Development copy · manual checks only" - case .readOnly: return "Move NX Library to Applications to update" - case .idle: return "Updates not checked yet" + case .developer: return L10n.text("Development copy · manual checks only") + case .readOnly: return L10n.text("Move NX Library to Applications to update") + case .idle: return L10n.text("Updates not checked yet") case .checking: return "Checking for updates…" - case .upToDate: return "NX Library is up to date" + case .upToDate: return L10n.text("NX Library is up to date") case .available(let release): let xcode = AppUpdate.neededXcode(release.requirements, installedXcode: viewModel.environmentStatus?.xcodeVersion) - .map { " · new portings need Xcode \($0)" } ?? "" - return (updater.blockedReason().map { "\(release.version) is available · \($0)" } ?? "\(release.version) is available") + xcode - case .downloading(let fraction): return "Downloading \(Int((fraction * 100).rounded()))%" - case .verifying: return "Checking download…" - case .installing: return "Installing…" + .map { L10n.format(" · new portings need Xcode %@", [String(describing: ($0))]) } ?? "" + return (updater.blockedReason().map { L10n.format("%@ is available · %@", [String(describing: (release.version)), String(describing: ($0))]) } ?? L10n.format("%@ is available", [String(describing: (release.version))])) + xcode + case .downloading(let fraction): return L10n.format("Downloading %@%%", [String(describing: (Int((fraction * 100).rounded())))]) + case .verifying: return L10n.text("Checking download…") + case .installing: return L10n.text("Installing…") case .failed(let message): return message } } private var updateActions: [UpdateAction] { - let checkNow = UpdateAction(title: "Check now") { updater.checkNow() } + let checkNow = UpdateAction(title: L10n.text("Check now")) { updater.checkNow() } switch updater.state { case .hidden, .checking, .downloading, .verifying, .installing: return [] case .readOnly: - return [checkNow, UpdateAction(title: "Reveal in Finder") { + return [checkNow, UpdateAction(title: L10n.text("Reveal in Finder")) { NSWorkspace.shared.activateFileViewerSelecting([Bundle.main.bundleURL]) }] case .developer, .idle, .upToDate: return [checkNow] case .available: - return [UpdateAction(title: "View update") { updater.showDetails() }, checkNow] + return [UpdateAction(title: L10n.text("View update")) { updater.showDetails() }, checkNow] case .failed: return [checkNow] } @@ -256,7 +257,7 @@ struct SettingsAboutSection: View { private var buildDate: String { let url = Bundle.main.executableURL ?? Bundle.main.bundleURL if let date = try? url.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate { - return date.formatted(date: .abbreviated, time: .omitted) + return date.formatted(.dateTime.day().month(.abbreviated).year().locale(L10n.context.formatLocale)) } return "local" } @@ -267,7 +268,7 @@ struct SettingsAboutSection: View { if FileManager.default.fileExists(atPath: docs.path) { NSWorkspace.shared.open(docs) } else { - viewModel.showSettingsNotice(title: "Docs unavailable", message: "Project documentation is not installed on this Mac.") + viewModel.showSettingsNotice(title: L10n.text("Docs unavailable"), message: L10n.text("Project documentation is not installed on this Mac.")) } } } diff --git a/Sources/NXLibrary/Views/Settings/SettingsControllersSection.swift b/Sources/NXLibrary/Views/Settings/SettingsControllersSection.swift index 2087645..6df7612 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsControllersSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsControllersSection.swift @@ -10,16 +10,16 @@ struct SettingsControllersSection: View { var body: some View { VStack(spacing: 9) { - SettingsSubheading("Library controls") + SettingsSubheading(L10n.text("Library controls")) SeleneRow("Gamepad", subtitle: controller.isGamepadConnected ? controller.activeControllerName : "Connect a controller and press Detect.") { HStack(spacing: 10) { - Text(controller.isGamepadConnected ? "Connected" : "Not connected") + Text(controller.isGamepadConnected ? L10n.text("Connected") : L10n.text("Not connected")) .seleneFont(13, .semibold) .foregroundColor(controller.isGamepadConnected ? SeleneTheme.success : SeleneTheme.ink60) - Button("Detect") { controller.refreshControllers() } + Button(L10n.text("Detect")) { controller.refreshControllers() } .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.glyph) @@ -48,7 +48,7 @@ struct SettingsControllersSection: View { } } - SettingsSubheading("New games") + SettingsSubheading(L10n.text("New games")) SeleneToggleRow("Rumble", subtitle: "Controller vibration", diff --git a/Sources/NXLibrary/Views/Settings/SettingsEngineSection.swift b/Sources/NXLibrary/Views/Settings/SettingsEngineSection.swift index 4267de1..9aa5762 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsEngineSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsEngineSection.swift @@ -14,18 +14,18 @@ struct SettingsEngineSection: View { SeleneRow("Engine repository", subtitle: "The upstream nxnative checkout used for builds. Updates stay manual.") { HStack(spacing: 10) { - Text(viewModel.nxnativePath.isEmpty ? "Not selected" : viewModel.nxnativePath) + Text(viewModel.nxnativePath.isEmpty ? L10n.text("Not selected") : viewModel.nxnativePath) .seleneFont(15, .medium) .foregroundColor(SeleneTheme.ink.opacity(0.62)) .lineLimit(1) .truncationMode(.middle) .frame(maxWidth: 300, alignment: .trailing) - Button("Change") { viewModel.selectNxnativeFolder() } + Button(L10n.text("Change")) { viewModel.selectNxnativeFolder() } .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.glyph) .disabled(configurationLocked) - Button("Reveal") { + Button(L10n.text("Reveal")) { viewModel.revealInFinder(url: URL(fileURLWithPath: viewModel.nxnativePath)) } .buttonStyle(.plain) @@ -38,8 +38,8 @@ struct SettingsEngineSection: View { SeleneValueRow("Pinned commit", value: seleneShort(viewModel.pinnedEngineCommit)) SeleneValueRow("Current commit", value: seleneShort(viewModel.currentEngineCommit)) - SelenePanel("Compatibility", - info: "Audits the local engine commit, working tree cleanliness, installed Metal compiler, and required CLI flags against the certified toolchain lock.") { + SelenePanel(L10n.text("Compatibility"), + info: L10n.text("Audits the local engine commit, working tree cleanliness, installed Metal compiler, and required CLI flags against the certified toolchain lock.")) { HStack(spacing: 8) { Circle() .fill(statusColor) @@ -48,7 +48,7 @@ struct SettingsEngineSection: View { .seleneFont(15, .semibold) .foregroundColor(statusColor) Spacer() - Button("Re-check") { viewModel.detectEnvironment() } + Button(L10n.text("Re-check")) { viewModel.detectEnvironment() } .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.glyph) @@ -70,7 +70,7 @@ struct SettingsEngineSection: View { } else { HStack(spacing: 8) { ProgressView().controlSize(.small) - Text("Running the compatibility check...") + Text(L10n.text("Running the compatibility check...")) .seleneFont(15) .foregroundColor(SeleneTheme.ink60) } @@ -79,14 +79,11 @@ struct SettingsEngineSection: View { if let environment = viewModel.environmentStatus { Divider().overlay(SeleneTheme.ink15) buildRow("Xcode", ok: environment.xcodeVersion != nil, - detail: environment.xcodeVersion ?? "not found") + detail: environment.xcodeVersion ?? L10n.text("not found")) buildRow("Metal compiler", ok: environment.metalVersion != nil, - detail: environment.metalVersion ?? "not found") - buildRow("Python and build tools", ok: environment.pythonURL != nil, + detail: environment.metalVersion ?? L10n.text("not found")) + buildRow(L10n.text("Python and build tools"), ok: environment.pythonURL != nil, detail: runtimeDetail(environment)) - buildRow(".NET 10 runtime", ok: !environment.issues.contains { - $0.lowercased().contains(".net") - }, detail: "checked by the build command") ForEach(environment.tools) { tool in buildRow(tool.name, ok: tool.ok, detail: tool.detail) } @@ -108,7 +105,7 @@ struct SettingsEngineSection: View { .labelsHidden() .toggleStyle(SeleneToggleStyle()) .disabled(!canChangeOverride) - .accessibilityValue(viewModel.allowUntestedToolchain ? "On" : "Off") + .accessibilityValue(viewModel.allowUntestedToolchain ? L10n.text("On") : L10n.text("Off")) } SeleneButtonRow("Upstream", @@ -133,18 +130,18 @@ struct SettingsEngineSection: View { } } - SelenePanel("prod.keys", - info: "Nintendo Switch cryptographic keys required to decrypt game packages (NSP/XCI). NX Library stores a private copy with mode 0600 permissions; key values are never displayed or logged.") { + SelenePanel(L10n.text("prod.keys"), + info: L10n.text("Nintendo Switch cryptographic keys required to decrypt game packages (NSP/XCI). NX Library stores a private copy with mode 0600 permissions; key values are never displayed or logged.")) { HStack(spacing: 8) { Image(systemName: viewModel.isProdKeysValid ? "checkmark.circle.fill" : "xmark.octagon.fill") .seleneFont(15, .bold) .foregroundColor(viewModel.isProdKeysValid ? SeleneTheme.success : SeleneTheme.danger) - Text(viewModel.isProdKeysValid ? "Keys imported" : "No prod.keys imported") + Text(viewModel.isProdKeysValid ? L10n.text("Keys imported") : L10n.text("No prod.keys imported")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink) Spacer() Text(viewModel.isProdKeysValid && !viewModel.prodKeysPath.isEmpty - ? viewModel.prodKeysPath : "Choose your file once") + ? viewModel.prodKeysPath : L10n.text("Choose your file once")) .seleneFont(13, .regular, design: .monospaced) .foregroundColor(SeleneTheme.ink60) .lineLimit(1) @@ -152,20 +149,20 @@ struct SettingsEngineSection: View { } if let summary = viewModel.keySummary { - Text("header_key present · \(summary.applicationKeyGenerations.count) application key generations") + Text(L10n.format("header_key present · %@ application key generations", [String(describing: (summary.applicationKeyGenerations.count))])) .seleneFont(13, .regular, design: .monospaced) .foregroundColor(SeleneTheme.success) } HStack(spacing: 14) { - Button(viewModel.isProdKeysValid ? "Replace Key..." : "Import Key...") { + Button(viewModel.isProdKeysValid ? L10n.text("Replace Key...") : L10n.text("Import Key...")) { viewModel.selectProdKeysFile() } .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.glyph) .disabled(configurationLocked) - Button("Detect Ryujinx...") { viewModel.selectRyujinxFolder() } + Button(L10n.text("Detect Ryujinx...")) { viewModel.selectRyujinxFolder() } .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.glyph) @@ -173,7 +170,7 @@ struct SettingsEngineSection: View { Spacer() } - Text("The original file is untouched. The stored copy is mode 0600 and key values are never shown or logged.") + Text(L10n.text("The original file is untouched. The stored copy is mode 0600 and key values are never shown or logged.")) .seleneFont(13) .foregroundColor(SeleneTheme.ink60) .fixedSize(horizontal: false, vertical: true) @@ -195,9 +192,9 @@ struct SettingsEngineSection: View { } private func confirmOverride() { - viewModel.showSettingsNotice(title: "Enable untested toolchain?", - message: "The engine and compiler do not match the certified combination. Builds may fail or show incorrect graphics and will be marked as untested.", - actionTitle: "Enable", cancelTitle: "Cancel") { + viewModel.showSettingsNotice(title: L10n.text("Enable untested toolchain?"), + message: L10n.text("The engine and compiler do not match the certified combination. Builds may fail or show incorrect graphics and will be marked as untested."), + actionTitle: L10n.text("Enable"), cancelTitle: L10n.text("Cancel")) { guard !configurationLocked else { return } viewModel.allowUntestedToolchain = true } @@ -223,21 +220,21 @@ struct SettingsEngineSection: View { } private var overrideSubtitle: String { - if viewModel.usesNewestEngine { return "On while you use the newest engine." } + if viewModel.usesNewestEngine { return L10n.text("On while you use the newest engine.") } guard let report = viewModel.compatibilityReport else { - return "Available when the compatibility gate blocks a build." + return L10n.text("Available when the compatibility gate blocks a build.") } if report.ok { - return "Not needed: the current toolchain is certified." + return L10n.text("Not needed: the current toolchain is certified.") } if viewModel.toolchainGateSummary?.tone == .info { - return "Recommended for your newer Metal compiler: builds are recorded as untested." + return L10n.text("Recommended for your newer Metal compiler: builds are recorded as untested.") } - return "Build anyway, recorded as untested. Corrupted graphics are a known risk." + return L10n.text("Build anyway, recorded as untested. Corrupted graphics are a known risk.") } private var statusText: String { - viewModel.toolchainGateSummary?.label ?? "Checking" + viewModel.toolchainGateSummary?.label ?? L10n.text("Checking") } private var statusColor: Color { @@ -253,9 +250,9 @@ struct SettingsEngineSection: View { private var upstreamSubtitle: String { switch viewModel.upstreamState { case .idle: - return "See how this NX Library's engine compares with seedds/nxnative main." + return L10n.text("See how this NX Library's engine compares with seedds/nxnative main.") case .checking: - return "Asking GitHub for the latest commits..." + return L10n.text("Asking GitHub for the latest commits...") case .loaded(let status): return UpstreamCopy.subtitle(upToDate: status.isUpToDate, newCommits: status.newCommitsCount) case .failed(let message): @@ -275,12 +272,12 @@ struct SettingsEngineSection: View { .fill(SeleneTheme.untested) .frame(width: 8, height: 8) .accessibilityHidden(true) - Text("In use") + Text(L10n.text("In use")) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.untested) } if viewModel.isSwitchingEngine { - Text("Switching…") + Text(L10n.text("Switching…")) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink60) } else { @@ -314,10 +311,10 @@ struct SettingsEngineSection: View { } private var newestSubtitle: String { - if viewModel.isSwitchingEngine { return "Switching the engine…" } + if viewModel.isSwitchingEngine { return L10n.text("Switching the engine…") } if let choice = viewModel.newestEngineChoice { - var base = "\(NewestEngineCopy.note) In use: \(choice.shortCommit), " - + "\(NewestEngineCopy.ahead(choice.aheadBy)) of the tested \(seleneShort(viewModel.pinnedEngineCommit))." + var base = L10n.format("%@ In use: %@, ", [String(describing: (NewestEngineCopy.note)), String(describing: (choice.shortCommit))]) + + L10n.format("%@ of the tested %@.", [String(describing: (NewestEngineCopy.ahead(choice.aheadBy))), String(describing: (seleneShort(viewModel.pinnedEngineCommit)))]) if viewModel.newestEngineActions.contains(.update), let offer = viewModel.newestEngineOffer { base += " " + NewestEngineCopy.update(choice: choice, main: offer.upstreamCommit, aheadBy: offer.newCommitsCount) @@ -334,7 +331,7 @@ struct SettingsEngineSection: View { private func newestTitle(_ action: NewestEngineAction) -> String { switch action { - case .useNewest: return "Use newest" + case .useNewest: return L10n.text("Use newest") case .update: return "Update" case .backToTested: return "Back to tested" } @@ -349,8 +346,8 @@ struct SettingsEngineSection: View { } private var upstreamButtonTitle: String { - if case .checking = viewModel.upstreamState { return "Checking..." } - return "Check" + if case .checking = viewModel.upstreamState { return L10n.text("Checking...") } + return L10n.text("Check") } /// Ten commits, or down to the one in use when it is further down, so its tag is always on screen. @@ -362,7 +359,7 @@ struct SettingsEngineSection: View { /// In use (green) on the commit the newest engine is at, New (orange) on the commits above it. private func commitMark(_ mark: UpstreamCommitMark) -> some View { let color = mark == .inUse ? SeleneTheme.success : SeleneTheme.untested - return Text(mark == .inUse ? "In use" : "New") + return Text(mark == .inUse ? "In use" : L10n.text("New")) .seleneFont(12, .bold) .foregroundColor(color) .padding(.horizontal, 7) @@ -377,7 +374,7 @@ struct SettingsEngineSection: View { if !status.commits.isEmpty { let marks = NewestEngine.marks(status.commits, inUse: viewModel.newestEngineChoice?.commit) let shown = shownCommitCount(status, marks: marks) - SelenePanel("Upstream Commits (\(status.newCommitsCount) newer)") { + SelenePanel(L10n.format("Upstream Commits (%@ newer)", [String(describing: (status.newCommitsCount))])) { VStack(alignment: .leading, spacing: 10) { ForEach(status.commits.prefix(shown)) { commit in HStack(alignment: .top, spacing: 10) { @@ -423,11 +420,11 @@ struct SettingsEngineSection: View { if status.commits.count > shown { HStack { - Text("+ \(status.commits.count - shown) more commits upstream") + Text(L10n.format("+ %@ more commits upstream", [String(describing: (status.commits.count - shown))])) .seleneFont(14, .medium) .foregroundColor(SeleneTheme.ink60) Spacer() - Button("View all on GitHub") { + Button(L10n.text("View all on GitHub")) { viewModel.openUpstreamCompare() } .buttonStyle(.plain) @@ -454,7 +451,7 @@ struct SettingsEngineSection: View { if check.id == "metal" { let installed = viewModel.installedMetalVersion ?? "unknown" let required = viewModel.requiredMetalVersion ?? "unknown" - return "installed \(installed) · required \(required)" + return L10n.format("installed %@ · required %@", [String(describing: (installed)), String(describing: (required))]) } return check.detail } @@ -485,8 +482,8 @@ struct SettingsEngineSection: View { private func rowLook(status: CheckStatus, metal: MetalStanding?) -> (icon: String, color: Color, badge: String) { switch metal { - case .newer: return ("info.circle.fill", SeleneTheme.glyph, "NEWER") - case .older: return ("exclamationmark.triangle.fill", SeleneTheme.untested, "OLDER") + case .newer: return ("info.circle.fill", SeleneTheme.glyph, L10n.text("NEWER")) + case .older: return ("exclamationmark.triangle.fill", SeleneTheme.untested, L10n.text("OLDER")) default: return (icon(for: status), color(for: status), status.rawValue.uppercased()) } } @@ -494,9 +491,9 @@ struct SettingsEngineSection: View { private func runtimeDetail(_ environment: EnvironmentStatus) -> String { guard let python = environment.pythonURL else { return "missing" } if viewModel.engineToolchain.isBundled { - return BundledRuntime.locate()?.versionSummary() ?? "Included in NX Library" + return BundledRuntime.locate()?.versionSummary() ?? L10n.text("Included in NX Library") } - return "Developer build: \(python.path)" + return L10n.format("Developer build: %@", [String(describing: (python.path))]) } private func buildRow(_ title: String, ok: Bool, detail: String) -> some View { diff --git a/Sources/NXLibrary/Views/Settings/SettingsGamesSection.swift b/Sources/NXLibrary/Views/Settings/SettingsGamesSection.swift index 47fe8e7..25632ee 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsGamesSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsGamesSection.swift @@ -38,7 +38,7 @@ struct SettingsGamesSection: View { private var pickedFolderSubtitle: String { let picked = settings.settings.cloudSavesPickedFolder - return picked.isEmpty ? "None chosen yet." : (picked as NSString).abbreviatingWithTildeInPath + return picked.isEmpty ? L10n.text("None chosen yet.") : (picked as NSString).abbreviatingWithTildeInPath } private func stepCloudSaves(_ step: Int) { @@ -47,7 +47,7 @@ struct SettingsGamesSection: View { var body: some View { VStack(spacing: canvas.u(8)) { - SettingsSubheading("All games") + SettingsSubheading(L10n.text("All games")) SeleneCycleRow("Launch screen style", subtitle: "Artwork layout", @@ -76,7 +76,7 @@ struct SettingsGamesSection: View { settings.settings.surprisePool = settings.settings.surprisePool.stepped(step) } - SettingsSubheading("Cloud saves") + SettingsSubheading(L10n.text("Cloud saves")) SeleneCycleRow("Cloud saves", subtitle: "All games · Next launch", @@ -87,10 +87,10 @@ struct SettingsGamesSection: View { subtitle: pickedFolderSubtitle, buttonTitle: "Choose…") { viewModel.chooseCloudSavesFolder() } - SettingsSubheading("Hidden games") + SettingsSubheading(L10n.text("Hidden games")) if hiddenGames.isEmpty { - Text("No hidden games. Hide one from its Game options; it comes back here.") + Text(L10n.text("No hidden games. Hide one from its Game options; it comes back here.")) .seleneFont(15) .foregroundColor(SeleneTheme.ink60) .frame(maxWidth: .infinity, alignment: .leading) @@ -98,12 +98,12 @@ struct SettingsGamesSection: View { } else { ForEach(hiddenGames) { game in SeleneButtonRow(game.name, id: "hidden-\(game.id)", - subtitle: "Out of Spotlight, Collection, Surprise me and search.", + subtitle: "Out of Spotlight, Collection, Surprise me and search.", verbatimTitle: true, buttonTitle: "Show") { unhide(game) } } } - SettingsSubheading("New games") + SettingsSubheading(L10n.text("New games")) SeleneToggleRow("Fullscreen", subtitle: "Launch mode", @@ -179,13 +179,13 @@ struct SettingsGamesSection: View { .value("launchStyle", "Launch screen style") { step in settings.settings.launchStyle = settings.settings.launchStyle.stepped(step) }, - .init(id: "launchMotion", title: "Launch screen motion") { + .init(id: "launchMotion", title: L10n.text("Launch screen motion"), rowID: "Launch screen motion") { settings.settings.launchMotion.toggle() }, .value("launchShade", "Launch screen shade") { step in settings.settings.launchShade = settings.settings.launchShade.stepped(step) }, - .init(id: "showsSessionCard", title: "Session card") { + .init(id: "showsSessionCard", title: L10n.text("Session card"), rowID: "Session card") { settings.settings.showsSessionCard.toggle() }, .value("surprisePool", "Surprise me pool") { step in @@ -196,19 +196,19 @@ struct SettingsGamesSection: View { private var defaultActions: [SettingsRowAction] { [ - .init(id: "fullscreen", title: "Fullscreen") { + .init(id: "fullscreen", title: L10n.text("Fullscreen"), rowID: "Fullscreen") { settings.settings.gameDefaults.fullscreen.toggle() }, - .init(id: "fps", title: "60 FPS") { + .init(id: "fps", title: L10n.text("60 FPS"), rowID: "60 FPS") { settings.settings.gameDefaults.enable60FPS.toggle() }, - .init(id: "docked", title: "Docked mode") { + .init(id: "docked", title: L10n.text("Docked mode"), rowID: "Docked mode") { settings.settings.gameDefaults.isDockedMode.toggle() }, - .init(id: "metalfx", title: "MetalFX upscaling") { + .init(id: "metalfx", title: L10n.text("MetalFX upscaling"), rowID: "MetalFX upscaling") { settings.settings.gameDefaults.enableMetalFXUpscale.toggle() }, - .init(id: "hud", title: "Metal HUD") { + .init(id: "hud", title: L10n.text("Metal HUD"), rowID: "Metal HUD") { settings.settings.gameDefaults.enableMetalHUD.toggle() }, .value("language", "Game language") { step in diff --git a/Sources/NXLibrary/Views/Settings/SettingsLibrarySection.swift b/Sources/NXLibrary/Views/Settings/SettingsLibrarySection.swift index d1bcffb..c2a239e 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsLibrarySection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsLibrarySection.swift @@ -16,17 +16,17 @@ struct SettingsLibrarySection: View { subtitle: GameFoldersCopy.appsContents, info: "Choose where to keep your game apps. Put any existing nxnative games (.app) here. Changing folders does not move apps; rebuilds update them where they are.") { HStack(spacing: 10) { - Text(viewModel.libraryFolder?.path ?? "Not selected") + Text(viewModel.libraryFolder?.path ?? L10n.text("Not selected")) .seleneFont(15, .medium) .foregroundColor(SeleneTheme.ink.opacity(0.62)) .lineLimit(1) .truncationMode(.middle) .frame(maxWidth: 320, alignment: .trailing) - Button("Change") { viewModel.selectLibraryFolder() } + Button(L10n.text("Change")) { viewModel.selectLibraryFolder() } .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.glyph) - Button("Reset") { + Button(L10n.text("Reset")) { viewModel.libraryFolder = URL(fileURLWithPath: viewModel.defaultInstallPath) .appendingPathComponent("Applications") viewModel.loadLibrary() @@ -41,18 +41,18 @@ struct SettingsLibrarySection: View { subtitle: GameFoldersCopy.sourcesAbout(extractedFolders: viewModel.engineSourceCapabilities.extractedFolders), info: "Existing game apps play without their sources. Reset group keeps this folder. Clear removes the selection, keeping your files.") { HStack(spacing: 10) { - Text(viewModel.gameFilesFolder?.path ?? "Not selected · Optional") + Text(viewModel.gameFilesFolder?.path ?? L10n.text("Not selected · Optional")) .seleneFont(15, .medium) .foregroundColor(SeleneTheme.ink.opacity(0.62)) .lineLimit(1) .truncationMode(.middle) .frame(maxWidth: 320, alignment: .trailing) - Button("Choose") { viewModel.selectGameFolder() } + Button(L10n.text("Choose")) { viewModel.selectGameFolder() } .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.glyph) if viewModel.gameFilesFolder != nil { - Button("Clear") { viewModel.selectedGameFolder = nil } + Button(L10n.text("Clear")) { viewModel.selectedGameFolder = nil } .buttonStyle(.plain) .seleneFont(14, .semibold) .foregroundColor(SeleneTheme.danger) diff --git a/Sources/NXLibrary/Views/Settings/SettingsLookSection.swift b/Sources/NXLibrary/Views/Settings/SettingsLookSection.swift index 12cb0a2..da99dd0 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsLookSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsLookSection.swift @@ -54,13 +54,13 @@ struct SettingsLookSection: View { var body: some View { VStack(spacing: canvas.u(8)) { - SettingsSubheading("Theme") + SettingsSubheading(L10n.text("Theme")) SeleneCycleRow("Color theme", subtitle: "Interface colours", value: themeName) { stepTheme($0) } - SettingsSubheading("Wallpaper") + SettingsSubheading(L10n.text("Wallpaper")) SeleneCycleRow("Wallpaper", subtitle: "App background", @@ -84,7 +84,7 @@ struct SettingsLookSection: View { subtitle: "Wallpaper & moonlight", isOn: $settings.settings.animatedBackground) - SettingsSubheading("Library screens") + SettingsSubheading(L10n.text("Library screens")) SeleneToggleRow("Game backgrounds", subtitle: "Spotlight & Collection", @@ -100,7 +100,7 @@ struct SettingsLookSection: View { subtitle: "Text readability", isOn: $settings.settings.readableText) - SettingsSubheading("Screen") + SettingsSubheading(L10n.text("Screen")) SeleneToggleRow("Clock", subtitle: "Top bar", @@ -114,7 +114,7 @@ struct SettingsLookSection: View { subtitle: "Dock & app", value: viewModel.iconAppearanceMode.label) { stepIcon($0) } - SettingsSubheading("Artwork") + SettingsSubheading(L10n.text("Artwork")) SeleneButtonRow("Box covers", subtitle: "GameTDB covers", diff --git a/Sources/NXLibrary/Views/Settings/SettingsNavigationSection.swift b/Sources/NXLibrary/Views/Settings/SettingsNavigationSection.swift index f33f80d..62b3dff 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsNavigationSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsNavigationSection.swift @@ -8,7 +8,7 @@ struct SettingsNavigationSection: View { private var buttonNamesTitle: String { switch settings.settings.buttonNames { - case .automatic: return "Automatic" + case .automatic: return L10n.text("Automatic") case .nintendo: return "Nintendo" case .xbox: return "Xbox" case .playstation: return "PlayStation" diff --git a/Sources/NXLibrary/Views/Settings/SettingsSearchSection.swift b/Sources/NXLibrary/Views/Settings/SettingsSearchSection.swift index 6077dab..c65d73f 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsSearchSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsSearchSection.swift @@ -16,7 +16,7 @@ struct SettingsSearchSection: View { var body: some View { VStack(alignment: .leading, spacing: canvas.u(8)) { - TextField("Search settings", text: $query) + TextField(L10n.text("Search settings"), text: $query) .textFieldStyle(.plain) .seleneFont(19) .foregroundColor(SeleneTheme.ink) @@ -37,7 +37,7 @@ struct SettingsSearchSection: View { .padding(.bottom, canvas.u(8)) if !query.isEmpty && results.isEmpty { - Text("No settings match.") + Text(L10n.text("No settings match.")) .seleneFont(19) .foregroundColor(SeleneTheme.ink60) } @@ -82,7 +82,7 @@ struct SettingsChangedSection: View { var body: some View { VStack(alignment: .leading, spacing: canvas.u(8)) { if rows.isEmpty { - Text("Nothing changed yet.") + Text(L10n.text("Nothing changed yet.")) .seleneFont(19) .foregroundColor(SeleneTheme.ink60) } @@ -103,7 +103,7 @@ struct SettingsDescriptorList: View { let onOpenGroup: (SettingsSection) -> Void @Environment(\.seleneCanvas) private var canvas - static func title(_ row: SettingDescriptor) -> String { "\(row.group.title) · \(row.title)" } + static func title(_ row: SettingDescriptor) -> String { "\(row.group.title) · \(L10n.text(row.title))" } static func actions(_ rows: [SettingDescriptor], onOpenGroup: @escaping (SettingsSection) -> Void) -> [SettingsRowAction] { diff --git a/Sources/NXLibrary/Views/Settings/SettingsSectionContent.swift b/Sources/NXLibrary/Views/Settings/SettingsSectionContent.swift index 091728f..84ac9ed 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsSectionContent.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsSectionContent.swift @@ -7,6 +7,8 @@ struct SettingsSectionContent: View { @ObservedObject var settings: AppSettingsStore @ObservedObject var artwork: ArtworkStore let onViewControls: () -> Void + let onChooseLanguage: () -> Void + let onChooseRegion: () -> Void let onOpenConsole: () -> Void let onOpenGroup: (SettingsSection) -> Void let onSearchSubmit: () -> Void @@ -21,7 +23,7 @@ struct SettingsSectionContent: View { SettingsChangedSection(settings: settings, viewModel: viewModel, onOpenGroup: onOpenGroup, onActions: onActions) case .startUp: - SettingsStartUpSection(viewModel: viewModel, settings: settings, onActions: onActions) + SettingsStartUpSection(viewModel: viewModel, settings: settings, onActions: onActions, onChooseLanguage: onChooseLanguage, onChooseRegion: onChooseRegion) case .navigation: SettingsNavigationSection(settings: settings, onActions: onActions) case .spotlight: @@ -67,7 +69,7 @@ struct SettingsSubheading: View { } func seleneBytes(_ bytes: Int64) -> String { - ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) + bytes.formatted(.byteCount(style: .file).locale(L10n.context.formatLocale)) } func seleneShort(_ commit: String?) -> String { diff --git a/Sources/NXLibrary/Views/Settings/SettingsSidebar.swift b/Sources/NXLibrary/Views/Settings/SettingsSidebar.swift index 7db1f91..dafa733 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsSidebar.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsSidebar.swift @@ -6,10 +6,11 @@ import NXLibraryCore struct SettingsSidebar: View { @Binding var selection: SettingsSection var isFocused: Bool = false + let context: AppLocale @Environment(\.seleneCanvas) private var canvas private var version: String { - "Version \(AppUpdate.versionLabel(Bundle.main.infoDictionary))" + L10n.format("Version %@", [AppUpdate.versionLabel(Bundle.main.infoDictionary)], context: context) } var body: some View { @@ -48,7 +49,7 @@ struct SettingsSidebar: View { .frame(width: canvas.u(48), height: canvas.u(48)) .accessibilityHidden(true) VStack(alignment: .leading, spacing: canvas.u(2)) { - Text("NX Library") + Text(L10n.text("NX Library")) .seleneFont(28, .semibold) .foregroundColor(SeleneTheme.ink) Text(version) @@ -68,7 +69,7 @@ struct SettingsSidebar: View { // Selene's section icons take the row's ink, like the title. MaterialIconView(section.icon, size: 24) .foregroundColor(lit ? SeleneTheme.onFocus : SeleneTheme.ink) - Text(section.title) + Text(section.title(in: context)) .seleneFont(21, .medium) .foregroundColor(lit ? SeleneTheme.onFocus : SeleneTheme.ink) Spacer(minLength: 0) @@ -82,7 +83,7 @@ struct SettingsSidebar: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .accessibilityLabel(section.title) + .accessibilityLabel(section.title(in: context)) .accessibilityAddTraits(selected ? .isSelected : []) } } diff --git a/Sources/NXLibrary/Views/Settings/SettingsSoundSection.swift b/Sources/NXLibrary/Views/Settings/SettingsSoundSection.swift index b4d6876..1382fff 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsSoundSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsSoundSection.swift @@ -32,7 +32,7 @@ struct SettingsSoundSection: View { info: "Movement and selection sounds stay quiet during launch. Game starting and Game ready still play.", isOn: $settings.settings.quietLaunch) - SettingsSubheading("Theme assets") + SettingsSubheading(L10n.text("Theme assets")) SeleneButtonRow("Import sounds and wallpapers from Selene", subtitle: importNote @@ -65,15 +65,15 @@ struct SettingsSoundSection: View { panel.canChooseDirectories = true panel.canChooseFiles = false panel.allowsMultipleSelection = false - panel.prompt = "Import" - panel.message = "Choose your Selene folder (the one that contains theme/theme.yaml)." + panel.prompt = L10n.text("Import") + panel.message = L10n.text("Choose your Selene folder (the one that contains theme/theme.yaml).") guard panel.runModal() == .OK, let url = panel.url else { return } do { let report = try SeleneImporter().importFrom(seleneRoot: url) SoundEffectService.shared.reloadSounds() NotificationCenter.default.post(name: .nxSeleneImportFinished, object: nil) - importNote = "Imported \(report.copied.count) files" - + (report.missing.isEmpty ? "." : "; \(report.missing.count) not found in that folder.") + importNote = L10n.format("Imported %@ files", [String(describing: (report.copied.count))]) + + (report.missing.isEmpty ? "." : L10n.format("; %@ not found in that folder.", [String(describing: (report.missing.count))])) SoundEffectService.shared.play(.success) } catch { importNote = error.localizedDescription diff --git a/Sources/NXLibrary/Views/Settings/SettingsSpotlightSection.swift b/Sources/NXLibrary/Views/Settings/SettingsSpotlightSection.swift index f5100f1..0d6982d 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsSpotlightSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsSpotlightSection.swift @@ -34,12 +34,12 @@ struct SettingsSpotlightSection: View { var body: some View { VStack(spacing: canvas.u(8)) { - SettingsSubheading("Cards") + SettingsSubheading(L10n.text("Cards")) ForEach(SpotlightCard.allCases, id: \.self) { card in SeleneToggleRow(card.title, isOn: cardBinding(card)) } - SettingsSubheading("Row") + SettingsSubheading(L10n.text("Row")) SeleneCycleRow("Starting tab", subtitle: "The filter Spotlight and Collection open on.", value: settings.settings.startingTab) { stepTab($0) } diff --git a/Sources/NXLibrary/Views/Settings/SettingsStartUpSection.swift b/Sources/NXLibrary/Views/Settings/SettingsStartUpSection.swift index 80049c1..31a5758 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsStartUpSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsStartUpSection.swift @@ -5,13 +5,15 @@ struct SettingsStartUpSection: View { @ObservedObject var viewModel: AppViewModel @ObservedObject var settings: AppSettingsStore let onActions: ([SettingsRowAction]) -> Void + let onChooseLanguage: () -> Void + let onChooseRegion: () -> Void @Environment(\.seleneCanvas) private var canvas private var startOnTitle: String { switch settings.settings.startOn { case .spotlight: return "Spotlight" case .collection: return "Collection" - case .last: return "Where I left" + case .last: return L10n.text("Where I left") } } @@ -24,6 +26,16 @@ struct SettingsStartUpSection: View { var body: some View { VStack(spacing: canvas.u(8)) { + SettingsSubheading(L10n.text("Language & region")) + SeleneButtonRow("App language", + subtitle: "Menus, genres and available game descriptions. The games’ own language stays separate.", + buttonTitle: settings.settings.appLanguage.nativeName, action: onChooseLanguage) + SeleneButtonRow("Regional formats", id: "Region", + subtitle: "Dates, numbers and sizes only. Game descriptions follow the app language.", + buttonTitle: settings.settings.appRegion.flatMap { + L10n.context.formatLocale.localizedString(forRegionCode: $0) + } ?? L10n.text("Automatic (Mac)"), action: onChooseRegion) + SeleneCycleRow("Start on", subtitle: "The screen NX Library opens on.", value: startOnTitle) { step in @@ -55,7 +67,7 @@ struct SettingsStartUpSection: View { subtitle: "Plays a short opening with the boot sound when the app opens.", isOn: $settings.settings.introAnimation) - SettingsSubheading("Setup") + SettingsSubheading(L10n.text("Setup")) SeleneButtonRow("Setup", subtitle: "Open the Setup check: what NX Library needs and what is ready.", @@ -71,15 +83,15 @@ struct SettingsStartUpSection: View { } .disabled(viewModel.isBuilding || viewModel.isUpdatingApp) - SelenePanel("Readiness", - info: "Summarizes the 4 essential gates: valid prod.keys, cloned nxnative repository, Python build environment, and certified Metal compiler version.") { + SelenePanel(L10n.text("Readiness"), + info: L10n.text("Summarizes the 4 essential gates: valid prod.keys, cloned nxnative repository, Python build environment, and certified Metal compiler version.")) { HStack(spacing: 9) { Circle() .fill(readiness ? SeleneTheme.success : SeleneTheme.untested) .frame(width: 8, height: 8) - Text(viewModel.isCheckingEnvironment ? "Checking…" - : viewModel.environmentStatus == nil && viewModel.setupCompleted ? "Not checked" - : readiness ? "Ready to build and play" : "Setup needed or builds blocked") + Text(viewModel.isCheckingEnvironment ? L10n.text("Checking…") + : viewModel.environmentStatus == nil && viewModel.setupCompleted ? L10n.text("Not checked") + : readiness ? L10n.text("Ready to build and play") : L10n.text("Setup needed or builds blocked")) .seleneFont(16, .semibold) .foregroundColor(SeleneTheme.ink) Spacer() @@ -94,19 +106,23 @@ struct SettingsStartUpSection: View { .onAppear(perform: publishActions) .onChange(of: viewModel.isBuilding) { _ in publishActions() } .onChange(of: viewModel.isUpdatingApp) { _ in publishActions() } + .onChange(of: settings.settings.appLanguage) { _ in publishActions() } + .onChange(of: settings.settings.appRegion) { _ in publishActions() } } private var readinessLine: String { let engine = seleneShort(viewModel.pinnedEngineCommit) - let metal = viewModel.installedMetalVersion ?? "Metal unknown" - let xcode = viewModel.environmentStatus?.xcodeVersion ?? "Xcode unknown" - let python = viewModel.environmentStatus?.pythonURL == nil ? "Python missing" - : viewModel.engineToolchain.isBundled ? "Python included" : "Python venv ready" - return "Engine \(engine) · \(metal) · \(xcode) · \(python)" + let metal = viewModel.installedMetalVersion ?? L10n.text("Metal unknown") + let xcode = viewModel.environmentStatus?.xcodeVersion ?? L10n.text("Xcode unknown") + let python = viewModel.environmentStatus?.pythonURL == nil ? L10n.text("Python missing") + : viewModel.engineToolchain.isBundled ? L10n.text("Python included") : L10n.text("Python venv ready") + return L10n.format("Engine %@ · %@ · %@ · %@", [String(describing: (engine)), String(describing: (metal)), String(describing: (xcode)), String(describing: (python))]) } private func publishActions() { onActions([ + .init(id: "appLanguage", title: "App language", activate: onChooseLanguage), + .init(id: "appRegion", title: L10n.text("Regional formats"), rowID: "Region", activate: onChooseRegion), .value("startOn", "Start on") { step in settings.settings.startOn = settings.settings.startOn.stepped(step) }, diff --git a/Sources/NXLibrary/Views/Settings/SettingsStorageSection.swift b/Sources/NXLibrary/Views/Settings/SettingsStorageSection.swift index b4b680f..a62e1c5 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsStorageSection.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsStorageSection.swift @@ -17,16 +17,25 @@ struct SettingsStorageSection: View { var body: some View { VStack(spacing: 9) { - SelenePanel("Storage") { + SelenePanel(L10n.text("Storage")) { if let usage = viewModel.storageUsage { - usageRow("Library apps", bytes: usage.libraryBytes) + usageRow(L10n.text("Library apps"), bytes: usage.libraryBytes) usageRow("Build caches", bytes: usage.cacheBytes) usageRow("Logs", bytes: usage.logsBytes) - usageRow("Free on volume", bytes: usage.freeBytes, positive: true) + if let free = viewModel.storageSnapshot?.freeBytes { + usageRow(L10n.text("Free on game apps volume"), bytes: free, positive: true) + } else { + Text(L10n.text("Game apps volume unavailable.")) + .seleneFont(15).foregroundColor(SeleneTheme.ink60) + } + if viewModel.storageSnapshot?.hasUnavailableFiles == true { + Text(L10n.text("Some folders are unavailable. Totals include readable files only. Connect their drives or check folder access, then recalculate.")) + .seleneFont(15).foregroundColor(SeleneTheme.ink60) + } } else { HStack(spacing: 8) { ProgressView().controlSize(.small) - Text("Calculating storage...") + Text(L10n.text("Calculating storage...")) .seleneFont(15) .foregroundColor(SeleneTheme.ink60) } @@ -34,27 +43,59 @@ struct SettingsStorageSection: View { } SeleneButtonRow("Storage usage", - subtitle: "Recalculate apps, build caches, logs and free space.", - buttonTitle: "Recalculate") { + subtitle: "Apps, build caches and logs for each game, including hidden games.", + buttonTitle: viewModel.isCalculatingStorage ? "Calculating…" : "Recalculate") { viewModel.refreshStorageUsage() } + .disabled(viewModel.isCalculatingStorage || viewModel.isCleaningStorage) + + if viewModel.isCleaningStorage { + HStack(spacing: 8) { + ProgressView().controlSize(.small) + Text(L10n.text("Deleting files…")).seleneFont(15).foregroundColor(SeleneTheme.ink60) + } + } else if let message = viewModel.storageCleanupMessage { + Text(message).seleneFont(15).foregroundColor(SeleneTheme.ink90) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityLabel(message) + } + + SettingsSubheading(L10n.text("By game")) + Text(L10n.text("Largest build caches first. Sizes describe file contents; the space recovered can differ. Shared folders count once in totals.")) + .seleneFont(15).foregroundColor(SeleneTheme.ink60) + .frame(maxWidth: .infinity, alignment: .leading) + if let snapshot = viewModel.storageSnapshot { + if snapshot.games.isEmpty { + Text(L10n.text("Add a game to see its storage here.")).seleneFont(15).foregroundColor(SeleneTheme.ink60) + } + ForEach(snapshot.games) { game in + SeleneButtonRow(game.name, id: game.rowID, + subtitle: gameSummary(game), verbatimTitle: true, + buttonTitle: "Delete cache") { + confirmCacheCleanup(game) + } + .disabled(!viewModel.canCleanStorage || !game.canDeleteCache) + } + } + + SettingsSubheading(L10n.text("All games")) SeleneButtonRow("Build caches", subtitle: viewModel.isBuilding ? "Available after the active build finishes or is cancelled." : "Extracted build caches for every library game. Apps, sources and saves are untouched.", info: "Intermediate files created during shader translation and staging in the app-owned workspace. Deleting them frees disk space; your built games and saves will not be touched.", - buttonTitle: "Delete") { + buttonTitle: "Delete all") { confirmCleanup(.caches) } - .disabled(viewModel.isBuilding) + .disabled(!viewModel.canCleanStorage || (viewModel.storageUsage?.cacheBytes ?? 0) == 0) SeleneButtonRow("Logs", subtitle: "Build and launch logs. Apps, sources and saves are untouched.", buttonTitle: "Delete") { confirmCleanup(.logs) } - .disabled(viewModel.isBuilding) + .disabled(!viewModel.canCleanStorage) SeleneValueRow("Metadata cache", subtitle: "Nintendo Store descriptions and links.", @@ -91,7 +132,45 @@ struct SettingsStorageSection: View { artworkBytes = artwork.cacheSize() } .onAppear(perform: publishActions) - .onChange(of: viewModel.isBuilding) { _ in publishActions() } + .onChange(of: viewModel.isBuilding) { building in + publishActions() + if !building { viewModel.refreshStorageUsage() } + } + .onChange(of: viewModel.isCleaningStorage) { _ in publishActions() } + .onChange(of: viewModel.isCalculatingStorage) { _ in publishActions() } + .onChange(of: viewModel.isUpdatingApp) { _ in publishActions() } + .onChange(of: viewModel.storageSnapshot) { _ in publishActions() } + .onChange(of: viewModel.libraryGames) { _ in viewModel.refreshStorageUsage() } + .onChange(of: viewModel.libraryFolder) { _ in viewModel.refreshStorageUsage() } + } + + private func sizeText(_ size: StorageSize, missing: String) -> String { + switch size { + case .bytes(let bytes): return seleneBytes(bytes) + case .missing: return missing + case .unavailable: return L10n.text("Unavailable") + } + } + + private func gameSummary(_ game: GameStorageUsage) -> String { + var text = L10n.format("App: %@ · Cache: %@ · Logs: %@", [String(describing: (sizeText(game.app, missing: L10n.text("Not found")))), String(describing: (sizeText(game.cache, missing: "None"))), String(describing: (sizeText(game.logs, missing: "None")))]) + if !game.sharedCacheWith.isEmpty { + text += L10n.format("\nCache shared with %@.", [String(describing: (game.sharedCacheWith.joined(separator: ", ")))]) + } + return text + } + + private func confirmCacheCleanup(_ usage: GameStorageUsage) { + guard viewModel.canCleanStorage, usage.canDeleteCache, + let game = viewModel.libraryGames.first(where: { $0.id == usage.id }) else { return } + var message = L10n.format("Delete %@ of build cache for %@? This permanently removes extracted and compiled build files. The next rebuild may take longer. The game app, original game files, saves and logs are kept.", [String(describing: (seleneBytes(usage.cache.knownBytes ?? 0))), String(describing: (game.name))]) + if !usage.sharedCacheWith.isEmpty { + message += L10n.format(" This cache is also used by %@; their next rebuilds may take longer too.", [String(describing: (usage.sharedCacheWith.joined(separator: ", ")))]) + } + viewModel.showSettingsNotice(title: L10n.text("Delete this game's cache?"), message: message, + actionTitle: L10n.text("Delete cache"), cancelTitle: L10n.text("Cancel")) { + viewModel.deleteCaches(for: game) + } } private func confirmCleanup(_ kind: CleanupKind) { @@ -99,10 +178,10 @@ struct SettingsStorageSection: View { ? seleneBytes(viewModel.storageUsage?.logsBytes ?? 0) : seleneBytes(viewModel.storageUsage?.cacheBytes ?? 0) let message = kind == .logs - ? "Remove \(amount) of logs for all games? Your games, sources and saves will be kept." - : "Remove \(amount) of build caches? Future portings will extract the sources again. Your games, sources and saves will be kept." - viewModel.showSettingsNotice(title: kind == .logs ? "Delete logs?" : "Delete build caches?", - message: message, actionTitle: "Delete", cancelTitle: "Cancel") { + ? L10n.format("Remove %@ of logs for all games? Your games, sources and saves will be kept.", [String(describing: (amount))]) + : L10n.format("Permanently remove %@ of build caches for all games? Future rebuilds may take longer. Your games, original game files, saves and logs will be kept. Unavailable folders will be left alone.", [String(describing: (amount))]) + viewModel.showSettingsNotice(title: kind == .logs ? L10n.text("Delete logs?") : L10n.text("Delete build caches?"), + message: message, actionTitle: L10n.text("Delete"), cancelTitle: L10n.text("Cancel")) { switch kind { case .caches: viewModel.deleteCaches() case .logs: viewModel.deleteLogs() @@ -112,7 +191,7 @@ struct SettingsStorageSection: View { private func usageRow(_ title: String, bytes: Int64, positive: Bool = false) -> some View { HStack { - Text(title) + Text(L10n.text(title)) .seleneFont(15) .foregroundColor(SeleneTheme.ink90) Spacer() @@ -127,14 +206,22 @@ struct SettingsStorageSection: View { } private func publishActions() { - onActions([ - .init(id: "recalc", title: "Storage usage") { + var actions: [SettingsRowAction] = [ + .init(id: "recalc", title: "Storage usage", isEnabled: !viewModel.isCalculatingStorage && !viewModel.isCleaningStorage) { viewModel.refreshStorageUsage() - }, - .init(id: "deleteCaches", title: "Build caches", isEnabled: !viewModel.isBuilding) { + } + ] + actions += (viewModel.storageSnapshot?.games ?? []).map { game in + SettingsRowAction(id: game.rowID, title: game.name, rowID: game.rowID, + isEnabled: viewModel.canCleanStorage && game.canDeleteCache) { + confirmCacheCleanup(game) + } + } + actions += [ + .init(id: "deleteCaches", title: "Build caches", isEnabled: viewModel.canCleanStorage && (viewModel.storageUsage?.cacheBytes ?? 0) > 0) { confirmCleanup(.caches) }, - .init(id: "deleteLogs", title: "Logs", isEnabled: !viewModel.isBuilding) { + .init(id: "deleteLogs", title: "Logs", isEnabled: viewModel.canCleanStorage) { confirmCleanup(.logs) }, .init(id: "metadataClear", title: "Metadata cache") { @@ -151,6 +238,7 @@ struct SettingsStorageSection: View { .init(id: "console", title: "Console") { onOpenConsole() } - ]) + ] + onActions(actions) } } diff --git a/Sources/NXLibrary/Views/Settings/SettingsView.swift b/Sources/NXLibrary/Views/Settings/SettingsView.swift index 1f50769..02b481d 100644 --- a/Sources/NXLibrary/Views/Settings/SettingsView.swift +++ b/Sources/NXLibrary/Views/Settings/SettingsView.swift @@ -14,6 +14,7 @@ public struct SettingsView: View { @State private var section: SettingsSection @State private var showingControls = false + @State private var localePicker: LocalePickerMode? @State private var focusArea: SettingsFocusArea = .sidebar @State private var rowFocus = FocusIndex() /// The button in focus inside the focused row: 0 its own, 1 a second one (Back to tested beside Update). @@ -35,6 +36,7 @@ public struct SettingsView: View { } /// A palette change redraws the whole screen; selection and focus live here and survive it. @ObservedObject private var theme = ThemeStore.shared + @Environment(\.nxAppLocale) private var localization public init(initialSection: SettingsSection, viewModel: AppViewModel, @@ -52,7 +54,7 @@ public struct SettingsView: View { public var body: some View { VStack(spacing: 0) { HStack(alignment: .top, spacing: canvas.u(24)) { - SettingsSidebar(selection: $section, isFocused: focusArea == .sidebar) + SettingsSidebar(selection: $section, isFocused: focusArea == .sidebar, context: localization) .frame(width: canvas.u(400)) content } @@ -64,7 +66,7 @@ public struct SettingsView: View { .id(theme.palette.id) .overlay(alignment: .bottom) { if let resetNotice { - SeleneSuccessNotice(title: "Group reset", message: resetNotice) + SeleneSuccessNotice(title: L10n.text("Group reset"), message: resetNotice) .padding(.bottom, canvas.u(100)) .allowsHitTesting(false) } @@ -106,6 +108,15 @@ public struct SettingsView: View { }) { showingControls = false } } } + .overlay { + if let mode = localePicker { + LocalePicker(mode: mode, settings: settings, canHandleInput: { + !router.isMenuPresented && !NativeMenuCommands.shared.isTracking + && !viewModel.showSetupCheck && !viewModel.isUpdateOfferOpen + && viewModel.settingsNotice == nil && NSApp.modalWindow == nil + }) { localePicker = nil } + } + } .environment(\.seleneMinimumFont, 12) } @@ -123,6 +134,8 @@ public struct SettingsView: View { settings: settings, artwork: artwork, onViewControls: { showingControls = true }, + onChooseLanguage: { localePicker = .language }, + onChooseRegion: { localePicker = .region }, onOpenConsole: { router.openConsole() }, onOpenGroup: openGroup, onSearchSubmit: enterRows, @@ -152,16 +165,16 @@ public struct SettingsView: View { } private var commandHints: [HintItem] { - var hints = [HintItem(.b, "Back") { handleBack() }] + var hints = [HintItem(.b, L10n.text("Back")) { handleBack() }] if canResetSection { - hints.append(HintItem(.x, "Reset group") { resetSection() }) + hints.append(HintItem(.x, L10n.text("Reset group")) { resetSection() }) } if focusArea == .sidebar { - hints.append(HintItem(.a, "Open") { enterRows() }) + hints.append(HintItem(.a, L10n.text("Open")) { enterRows() }) } else if focusedRow?.cycle != nil { - hints.append(HintItem(.a, "Change")) + hints.append(HintItem(.a, L10n.text("Change"))) } else { - hints.append(HintItem(.a, "Select")) + hints.append(HintItem(.a, L10n.text("Select"))) } return hints } @@ -215,6 +228,8 @@ public struct SettingsView: View { var next = settings.settings switch section { case .startUp: + next.appLanguage = defaults.appLanguage + next.appRegion = defaults.appRegion next.startOn = defaults.startOn next.restoreLastGame = defaults.restoreLastGame next.startInFullscreen = defaults.startInFullscreen @@ -249,7 +264,7 @@ public struct SettingsView: View { next.reduceMotion = defaults.reduceMotion viewModel.iconAppearanceMode = .auto case .games: - resetNotice = "Cloud sync kept" + resetNotice = L10n.text("Cloud sync kept") next.launchStyle = defaults.launchStyle next.launchMotion = defaults.launchMotion next.launchShade = defaults.launchShade @@ -266,7 +281,7 @@ public struct SettingsView: View { next.gameDefaults.rumble = defaults.gameDefaults.rumble next.gameDefaults.rumbleGain = defaults.gameDefaults.rumbleGain case .library: - resetNotice = "Sources folder kept" + resetNotice = L10n.text("Sources folder kept") next.notifyPortingEnds = defaults.notifyPortingEnds viewModel.libraryFolder = URL(fileURLWithPath: viewModel.defaultInstallPath) .appendingPathComponent("Applications") @@ -321,7 +336,7 @@ public struct SettingsView: View { !NativeMenuCommands.shared.isTracking, !router.isMenuPresented, !viewModel.showSetupCheck, - !showingControls, !viewModel.isEditingSteamGridDBKey, + !showingControls, localePicker == nil, !viewModel.isEditingSteamGridDBKey, viewModel.alertMessage == nil, viewModel.settingsNotice == nil, viewModel.gameWithInstallFailure == nil, let window = event.window, @@ -376,6 +391,10 @@ public struct SettingsView: View { private func handleGamepad(_ action: GamepadAction) { // The key dialog takes the controller while it is open. if viewModel.isEditingSteamGridDBKey { return } + if localePicker != nil { + NotificationCenter.default.post(name: .nxLocalePickerGamepadAction, object: action) + return + } if showingControls { NotificationCenter.default.post(name: .nxControlsGamepadAction, object: action) return diff --git a/Sources/NXLibrary/Views/Setup/SetupCheckView.swift b/Sources/NXLibrary/Views/Setup/SetupCheckView.swift index a34e538..8db09f7 100644 --- a/Sources/NXLibrary/Views/Setup/SetupCheckView.swift +++ b/Sources/NXLibrary/Views/Setup/SetupCheckView.swift @@ -36,10 +36,10 @@ struct SetupCheckView: View { SeleneBackdrop() VStack(spacing: 0) { SetupStepIndicator(page: .requirements).padding(.bottom, canvas.u(18)) - Text("Let's get NX Library ready") + Text(L10n.text("Let's get NX Library ready")) .seleneFont(46, .semibold) .foregroundColor(SeleneTheme.ink) - Text("Check what you need to build. Your games come next.") + Text(L10n.text("Check what you need to build. Your games come next.")) .seleneFont(19) .foregroundColor(SeleneTheme.ink75) .padding(.top, canvas.u(10)) @@ -51,7 +51,7 @@ struct SetupCheckView: View { .offset(y: canvas.u(-20)) VStack { Spacer(minLength: 0) - SeleneHintsBar(right: isFirstRun ? [HintItem(.a, "Select")] : [HintItem(.a, "Select"), HintItem(.b, "Back")], + SeleneHintsBar(right: isFirstRun ? [HintItem(.a, L10n.text("Select"))] : [HintItem(.a, L10n.text("Select")), HintItem(.b, L10n.text("Back"))], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } } @@ -93,7 +93,7 @@ struct SetupCheckView: View { MaterialIconView(Self.icon(item.id), size: 26) .foregroundColor(SeleneTheme.glyph) .frame(height: canvas.u(30)) - Text(item.title) + Text(L10n.text(item.title)) .seleneFont(18, .semibold) .foregroundColor(SeleneTheme.ink) .lineLimit(1) @@ -131,7 +131,6 @@ struct SetupCheckView: View { switch id { case .xcode: return .build case .metal: return .monitor - case .dotnet: return .terminal case .engine: return .rocket case .runtime: return .storage case .keys: return .key @@ -155,7 +154,7 @@ struct SetupCheckView: View { .frame(width: canvas.u(40)) VStack(alignment: .leading, spacing: canvas.u(6)) { HStack(spacing: canvas.u(12)) { - Text(story.title).seleneFont(24, .semibold).foregroundColor(SeleneTheme.ink) + Text(L10n.text(story.title)).seleneFont(24, .semibold).foregroundColor(SeleneTheme.ink) Text(story.state.uppercased()) .seleneFont(13, .bold) .foregroundColor(story.tone) @@ -201,29 +200,29 @@ struct SetupCheckView: View { case .installAll: let names = missingNames if model.installAllRunning, let progress = model.installAllProgress { - return Story(icon: .sync, tone: SeleneTheme.glyph, title: "Install all", state: progress, - about: names.isEmpty ? "Finishing…" : names.joined(separator: " · "), - detail: "Each card shows how it is going.", action: nil) + return Story(icon: .sync, tone: SeleneTheme.glyph, title: L10n.text("Install all"), state: progress, + about: names.isEmpty ? L10n.text("Finishing…") : names.joined(separator: " · "), + detail: L10n.text("Each card shows how it is going."), action: nil) } if names.isEmpty { - return Story(icon: .check, tone: SeleneTheme.success, title: "Install all", state: "Done", - about: "Everything NX Library can install by itself is installed.", - detail: "Import your keys on their card if needed.", action: nil) + return Story(icon: .check, tone: SeleneTheme.success, title: L10n.text("Install all"), state: "Done", + about: L10n.text("Everything NX Library can install by itself is installed."), + detail: L10n.text("Import your keys on their card if needed."), action: nil) } - return Story(icon: .sync, tone: SeleneTheme.glyph, title: "Install all", state: "\(names.count) to go", + return Story(icon: .sync, tone: SeleneTheme.glyph, title: L10n.text("Install all"), state: L10n.format("%@ to go", [String(describing: (names.count))]), about: names.joined(separator: " · "), - detail: "Import your keys on their card if needed.", action: "Install all") + detail: L10n.text("Import your keys on their card if needed."), action: "Install all") default: let required = items.filter(\.isRequired) let ready = required.filter { $0.state == .ready }.count let left = required.filter { $0.state != .ready }.map(\.title) if left.isEmpty { - return Story(icon: .check, tone: SeleneTheme.success, title: "All set", state: "Ready", - about: "Everything NX Library needs is here.", detail: "", action: "Continue") + return Story(icon: .check, tone: SeleneTheme.success, title: L10n.text("All set"), state: L10n.text("Ready"), + about: L10n.text("Everything NX Library needs is here."), detail: "", action: "Continue") } - return Story(icon: .checklist, tone: SeleneTheme.glyph, title: "\(ready) of \(required.count) ready", - state: "\(left.count) left", about: "Still to do: \(left.joined(separator: ", ")).", - detail: "Build setup can wait. Existing game apps still play.", + return Story(icon: .checklist, tone: SeleneTheme.glyph, title: L10n.format("%@ of %@ ready", [String(describing: (ready)), String(describing: (required.count))]), + state: L10n.format("%@ left", [String(describing: (left.count))]), about: L10n.format("Still to do: %@.", [String(describing: (left.joined(separator: ", ")))]), + detail: L10n.text("Build setup can wait. Existing game apps still play."), action: "Continue") } } @@ -236,7 +235,7 @@ struct SetupCheckView: View { private var buttons: some View { let count = model.missingAutomatic.count - let title = model.installAllProgress ?? (count > 0 ? "Install all (\(count))" : "All installed") + let title = model.installAllProgress ?? (count > 0 ? L10n.format("Install all (%@)", [String(describing: (count))]) : L10n.text("All installed")) return HStack(spacing: canvas.u(22)) { SelenePillButton(title: title, minWidth: 240, focused: focus == .installAll, enabled: count > 0 && !model.installAllRunning) { @@ -244,7 +243,7 @@ struct SetupCheckView: View { } .onHover { if $0 { move(to: .installAll) } } .opacity(model.installAllRunning ? 0.75 : 1) - SelenePillButton(title: "Continue", minWidth: 200, focused: focus == .carryOn) { + SelenePillButton(title: L10n.text("Continue"), minWidth: 200, focused: focus == .carryOn) { move(to: .carryOn); activate() } .onHover { if $0 { move(to: .carryOn) } } diff --git a/Sources/NXLibrary/Views/Setup/YourGamesSetupView.swift b/Sources/NXLibrary/Views/Setup/YourGamesSetupView.swift index d9ad25b..f09cd98 100644 --- a/Sources/NXLibrary/Views/Setup/YourGamesSetupView.swift +++ b/Sources/NXLibrary/Views/Setup/YourGamesSetupView.swift @@ -18,14 +18,14 @@ struct YourGamesSetupView: View { FirstRunSetup.libraryState(isScanning: viewModel.isScanningLibrary, gameCount: viewModel.libraryGames.count) } private var folderProblem: String? { GameAppDestination.folderProblem(at: viewModel.gameAppsFolder) } - private var title: String { libraryState == .empty ? "Add your first game" : "Your games" } + private var title: String { libraryState == .empty ? L10n.text("Add your first game") : L10n.text("Your games") } private var subtitle: String { switch libraryState { - case .scanning: return "Looking for existing game apps…" - case .empty: return "Choose a game file, or add one later." + case .scanning: return L10n.text("Looking for existing game apps…") + case .empty: return L10n.text("Choose a game file, or add one later.") case .ready: let count = viewModel.libraryGames.count - return "\(count) game\(count == 1 ? "" : "s") found." + return L10n.count("%lld games found.", count, arguments: []) } } @@ -57,7 +57,7 @@ struct YourGamesSetupView: View { enabled: folderProblem == nil, action: onChooseFiles) .padding(.top, canvas.u(12)) - SelenePillButton(title: "Choose a folder instead", icon: .folder, iconSize: 19, + SelenePillButton(title: L10n.text("Choose a folder instead"), icon: .folder, iconSize: 19, minWidth: 0, height: 44, horizontalPadding: 24, focused: focus == .folder, enabled: folderProblem == nil) { activate(.folder) @@ -73,9 +73,9 @@ struct YourGamesSetupView: View { } HStack(spacing: canvas.u(22)) { - SelenePillButton(title: "Back", focused: focus == .back) { activate(.back) } + SelenePillButton(title: L10n.text("Back"), focused: focus == .back) { activate(.back) } .onHover { if $0 { move(to: .back) } } - SelenePillButton(title: libraryState == .ready ? "Open library" : "Skip for now", + SelenePillButton(title: libraryState == .ready ? L10n.text("Open library") : L10n.text("Skip for now"), minWidth: 220, focused: focus == .finish) { activate(.finish) } .onHover { if $0 { move(to: .finish) } } } @@ -86,7 +86,7 @@ struct YourGamesSetupView: View { VStack { Spacer(minLength: 0) - SeleneHintsBar(left: [HintItem(.b, "Back")], right: [HintItem(.a, "Select")], + SeleneHintsBar(left: [HintItem(.b, L10n.text("Back"))], right: [HintItem(.a, L10n.text("Select"))], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } } @@ -184,12 +184,12 @@ struct SetupStepIndicator: View { var body: some View { HStack(spacing: canvas.u(14)) { - step(1, "Get ready", active: page == .requirements) + step(1, L10n.text("Get ready"), active: page == .requirements) Capsule().fill(SeleneTheme.ink15).frame(width: canvas.u(38), height: canvas.u(1)) step(2, "Your games", active: page == .games) } .accessibilityElement(children: .ignore) - .accessibilityLabel(page == .requirements ? "Step 1 of 2: Get ready" : "Step 2 of 2: Your games") + .accessibilityLabel(page == .requirements ? L10n.text("Step 1 of 2: Get ready") : L10n.text("Step 2 of 2: Your games")) } private func step(_ number: Int, _ title: String, active: Bool) -> some View { diff --git a/Sources/NXLibrary/Views/Shell/AppShellView.swift b/Sources/NXLibrary/Views/Shell/AppShellView.swift index 50a8946..7cf701b 100644 --- a/Sources/NXLibrary/Views/Shell/AppShellView.swift +++ b/Sources/NXLibrary/Views/Shell/AppShellView.swift @@ -14,6 +14,7 @@ public struct AppShellView: View { @StateObject private var updater = AppUpdater() @StateObject private var nativeMenus = NativeMenuCommands.shared @ObservedObject private var theme = ThemeStore.shared + @ObservedObject private var localization = AppLocalization.shared @State private var gestureKeyMonitor: Any? @State private var setupSourceEntryID: UUID? /// Dismisses the Close Game confirmation when its game quits by itself. @@ -21,13 +22,20 @@ public struct AppShellView: View { @Environment(\.accessibilityReduceMotion) private var accessibilityReduceMotion - public init() { - let store = AppSettingsStore() + public init(settingsStore store: AppSettingsStore) { _settings = StateObject(wrappedValue: store) _viewModel = StateObject(wrappedValue: AppViewModel(settingsStore: store)) _router = StateObject(wrappedValue: Self.startingRouter(saved: store.settings)) } + private func updateLocale() { + let previousLanguage = AppLocalization.shared.context.language + AppLocalization.shared.configure(settings.settings) + if previousLanguage != AppLocalization.shared.context.language { + viewModel.refreshLocalizedMetadata() + } + } + /// The router, already on the screen the Start on setting names. private static func startingRouter(saved: AppSettings) -> AppRouter { // The palette is in place before the first frame. @@ -255,9 +263,9 @@ public struct AppShellView: View { guard error != nil else { return } let writing = settings.errorKind == .writing viewModel.showSettingsNotice( - title: writing ? "Settings not saved" : "Settings unavailable", - message: writing ? "Your changes could not be saved. Try again." - : "Defaults are in use. Your previous settings file has been kept.", + title: writing ? L10n.text("Settings not saved") : L10n.text("Settings unavailable"), + message: writing ? L10n.text("Your changes could not be saved. Try again.") + : L10n.text("Defaults are in use. Your previous settings file has been kept."), retry: writing ? { _ = settings.save() } : nil) viewModel.settingsNotice?.onDismiss = { settings.dismissError() } } @@ -296,8 +304,12 @@ public struct AppShellView: View { } } - public var body: some View { + private var nativeGameCommands: some View { styled + .environment(\.locale, localization.context.formatLocale) + .environment(\.nxAppLocale, localization.context) + .onChange(of: settings.settings.appLanguage) { _ in updateLocale() } + .onChange(of: settings.settings.appRegion) { _ in updateLocale() } .onReceive(NotificationCenter.default.publisher(for: .nxLibraryPlayFocused)) { _ in guard !router.isMenuPresented, nativeMenuAvailability.play, @@ -334,6 +346,10 @@ public struct AppShellView: View { break } } + } + + public var body: some View { + nativeGameCommands .onReceive(NotificationCenter.default.publisher(for: .nxLibraryCheckAppUpdates)) { _ in guard nativeMenuAvailability.general else { return } router.closeMenu() @@ -497,17 +513,18 @@ public struct AppShellView: View { viewModel.router = router updater.blockedReason = { [weak viewModel, weak runningGames] in guard let viewModel else { return nil } - if viewModel.isBuilding { return "After the porting" } - if viewModel.launchingGameId != nil { return "After the game starts" } - if viewModel.saveGate.hasActiveOperations { return "After the save operation" } + if viewModel.isBuilding { return L10n.text("After the porting") } + if viewModel.isCleaningStorage { return L10n.text("After storage cleanup") } + if viewModel.launchingGameId != nil { return L10n.text("After the game starts") } + if viewModel.saveGate.hasActiveOperations { return L10n.text("After the save operation") } if let ids = runningGames?.runningGameIDs, let id = ids.first { let name = viewModel.libraryGames.first { $0.id == id }?.name ?? "the game" - return "After you quit \(name)" + return L10n.format("After you quit %@", [String(describing: (name))]) } return nil } updater.acquireInstallLease = { [weak viewModel, weak runningGames] in - guard let viewModel, !viewModel.isBuilding, viewModel.launchingGameId == nil, + guard let viewModel, !viewModel.isBuilding, !viewModel.isCleaningStorage, viewModel.launchingGameId == nil, runningGames?.runningGameIDs.isEmpty == true, let token = viewModel.saveGate.beginAppUpdate() else { return nil } let gate = viewModel.saveGate @@ -628,7 +645,7 @@ public struct AppShellView: View { private func presentLegacyNotice() { guard let message = viewModel.alertMessage else { return } viewModel.alertMessage = nil - viewModel.showSettingsNotice(title: "NX Library", message: message) + viewModel.showSettingsNotice(title: L10n.text("NX Library"), message: message) } private func settingsNoticeInput(_ action: GamepadAction) { @@ -647,10 +664,10 @@ public struct AppShellView: View { return ZStack { SeleneTheme.scrim45.ignoresSafeArea().contentShape(Rectangle()) .onTapGesture { viewModel.gameWithInstallFailure = nil } - SeleneConfirmDialog(title: "Installation failed", - message: "The new build of \(game.name) has been kept." - + (currentExists ? "\nYour current app is still available." : "\nYou can retry installation."), - confirmTitle: "Retry", cancelTitle: "Close", + SeleneConfirmDialog(title: L10n.text("Installation failed"), + message: L10n.format("The new build of %@ has been kept.", [String(describing: (game.name))]) + + (currentExists ? L10n.text("\nYour current app is still available.") : L10n.text("\nYou can retry installation.")), + confirmTitle: "Retry", cancelTitle: L10n.text("Close"), focus: $viewModel.installFailureFocus, onCancel: { viewModel.gameWithInstallFailure = nil }, onConfirm: { viewModel.retryFailedInstallation() }) @@ -684,8 +701,8 @@ public struct AppShellView: View { .ignoresSafeArea() .contentShape(Rectangle()) .onTapGesture { viewModel.dismissCloseGame() } - SeleneConfirmDialog(title: "Close \(game.name)?", - message: "Unsaved progress in the game will be lost.", + SeleneConfirmDialog(title: L10n.format("Close %@?", [String(describing: (game.name))]), + message: L10n.text("Unsaved progress in the game will be lost."), confirmTitle: "Close", focus: $viewModel.closeGameFocus, onCancel: { viewModel.dismissCloseGame() }, @@ -723,8 +740,8 @@ public struct AppShellView: View { .ignoresSafeArea() .contentShape(Rectangle()) .onTapGesture { viewModel.dismissRemoveGame() } - SeleneConfirmDialog(title: "Delete \(game.name)?", - message: "It leaves the library. Your game files and the build cache stay on disk.", + SeleneConfirmDialog(title: L10n.format("Delete %@?", [String(describing: (game.name))]), + message: L10n.text("It leaves the library. Your game files and the build cache stay on disk."), confirmTitle: "Delete", focus: $viewModel.removeGameFocus, onCancel: { viewModel.dismissRemoveGame() }, @@ -1011,7 +1028,7 @@ public struct AppShellView: View { do { try helper.run() } catch { - viewModel.alertMessage = "NX Library could not restart: \(error.localizedDescription)" + viewModel.alertMessage = L10n.format("NX Library could not restart: %@", [String(describing: (error.localizedDescription))]) return } NSApplication.shared.terminate(nil) diff --git a/Sources/NXLibrary/Views/Shell/MenuOverlayView.swift b/Sources/NXLibrary/Views/Shell/MenuOverlayView.swift index 6cf121d..338ccd7 100644 --- a/Sources/NXLibrary/Views/Shell/MenuOverlayView.swift +++ b/Sources/NXLibrary/Views/Shell/MenuOverlayView.swift @@ -54,7 +54,7 @@ struct MenuOverlayView: View { .padding(.bottom, canvas.u(rowBottom)) // Where the screen underneath had its hints, which step aside while this is open. - SeleneHintsBar(left: [HintItem(.b, "Close")], right: [HintItem(.a, "Select")], + SeleneHintsBar(left: [HintItem(.b, L10n.text("Close"))], right: [HintItem(.a, L10n.text("Select"))], bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) } .onAppear { presented = true } diff --git a/Sources/NXLibrary/Views/Spotlight/SeleneArtBackground.swift b/Sources/NXLibrary/Views/Spotlight/SeleneArtBackground.swift index bca76d0..4153d78 100644 --- a/Sources/NXLibrary/Views/Spotlight/SeleneArtBackground.swift +++ b/Sources/NXLibrary/Views/Spotlight/SeleneArtBackground.swift @@ -163,7 +163,7 @@ final class SeleneArtLayerView: NSView { } } CATransaction.commit() - Self.log.notice("art \(cg == nil ? "none" : "in", privacy: .public) layers \(self.stack.count) interrupted \(interrupted) \(String(key.prefix(8)), privacy: .public)") + Self.log.notice("art \(cg == nil ? L10n.text("none") : L10n.text("in"), privacy: .public) layers \(self.stack.count) interrupted \(interrupted) \(String(key.prefix(8)), privacy: .public)") } /// What `art` shows at `time`: its fade in flight, else its settled opacity. diff --git a/Sources/NXLibrary/Views/Spotlight/SpotlightCardInfoView.swift b/Sources/NXLibrary/Views/Spotlight/SpotlightCardInfoView.swift index 4514acb..a735c12 100644 --- a/Sources/NXLibrary/Views/Spotlight/SpotlightCardInfoView.swift +++ b/Sources/NXLibrary/Views/Spotlight/SpotlightCardInfoView.swift @@ -21,7 +21,12 @@ struct SpotlightCardInfoView: View { var modCounts: (mods: Int, games: Int, looking: Bool)? = nil /// Monday first, whatever the locale (matches `Activity.summary`). - private static let dayLetters = ["M", "T", "W", "T", "F", "S", "S"] + private static var dayLetters: [String] { + let formatter = DateFormatter() + formatter.locale = L10n.context.formatLocale + let days = formatter.veryShortStandaloneWeekdaySymbols ?? ["S", "M", "T", "W", "T", "F", "S"] + return Array(days.dropFirst()) + [days[0]] + } var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -47,19 +52,19 @@ struct SpotlightCardInfoView: View { switch card { case .collection: return "Collection" case .surprise: return "Surprise me" - case .activity: return "Activity" - case .mods: return "Mods" - case .addGame: return "Add a game" + case .activity: return L10n.text("Activity") + case .mods: return L10n.text("Mods") + case .addGame: return L10n.text("Add a game") } } private var about: String { switch card { - case .collection: return "Every game in your library, in one row." - case .surprise: return "Can't decide? Pick a game for me." - case .activity: return "What you played this week." - case .mods: return "Mods that work with your games, from GameBanana." - case .addGame: return "Build a game from your own dumps." + case .collection: return L10n.text("Every game in your library, in one row.") + case .surprise: return L10n.text("Can't decide? Pick a game for me.") + case .activity: return L10n.text("What you played this week.") + case .mods: return L10n.text("Mods that work with your games, from GameBanana.") + case .addGame: return L10n.text("Build a game from your own dumps.") } } @@ -69,7 +74,7 @@ struct SpotlightCardInfoView: View { case .surprise: return "Pick" // Activity and Mods have no button here: the hint in the corner says it once. case .activity, .mods: return nil - case .addGame: return "Add" + case .addGame: return L10n.text("Add") } } @@ -78,12 +83,12 @@ struct SpotlightCardInfoView: View { switch card { case .collection: HStack(spacing: canvas.u(26)) { - stat("GAMES", "\(gameCount)") + stat(L10n.text("GAMES"), "\(gameCount)") divider - stat("INSTALLED", "\(installedCount)") + stat(L10n.text("INSTALLED"), "\(installedCount)") } case .surprise: - stat("PICKS FROM", poolLabel) + stat(L10n.text("PICKS FROM"), poolLabel) case .activity: activityDetail case .mods: @@ -91,10 +96,10 @@ struct SpotlightCardInfoView: View { HStack(spacing: canvas.u(26)) { stat("MODS", "\(counts.mods)") divider - stat("FOR", counts.games == 1 ? "1 game" : "\(counts.games) games") + stat(L10n.text("FOR"), counts.games == 1 ? L10n.text("1 game") : L10n.format("%@ games", [String(describing: (counts.games))])) } } else { - stat("MODS", modCounts?.looking == false ? "None yet" : "Looking…") + stat("MODS", modCounts?.looking == false ? L10n.text("None yet") : L10n.text("Looking…")) } case .addGame: EmptyView() @@ -104,12 +109,12 @@ struct SpotlightCardInfoView: View { private var activityDetail: some View { VStack(alignment: .leading, spacing: canvas.u(18)) { HStack(alignment: .bottom, spacing: canvas.u(44)) { - stat("THIS WEEK", Self.duration(activity.thisWeek)) + stat(L10n.text("THIS WEEK"), Self.duration(activity.thisWeek)) bars } if !topGames.isEmpty { VStack(alignment: .leading, spacing: canvas.u(8)) { - Text("MOST PLAYED").seleneFont(12, .semibold).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("MOST PLAYED")).seleneFont(12, .semibold).foregroundColor(SeleneTheme.ink60) HStack(spacing: canvas.u(14)) { ForEach(Array(topGames.enumerated()), id: \.element.id) { index, game in topTile(game, focused: focusedTop == index) @@ -125,6 +130,7 @@ struct SpotlightCardInfoView: View { private var bars: some View { let peak = max(activity.perDay.max() ?? 0, 1) + let days = Self.dayLetters return HStack(alignment: .bottom, spacing: canvas.u(10)) { ForEach(0..<7, id: \.self) { day in let value = day < activity.perDay.count ? activity.perDay[day] : 0 @@ -132,7 +138,7 @@ struct SpotlightCardInfoView: View { RoundedRectangle(cornerRadius: canvas.u(4), style: .continuous) .fill(value > 0 ? SeleneTheme.focus : SeleneTheme.ink15) .frame(width: canvas.u(22), height: canvas.u(value > 0 ? max(6, 56 * value / peak) : 4)) - Text(Self.dayLetters[day]).seleneFont(12, .semibold).foregroundColor(SeleneTheme.ink60) + Text(days[day]).seleneFont(12, .semibold).foregroundColor(SeleneTheme.ink60) } } } @@ -155,7 +161,7 @@ struct SpotlightCardInfoView: View { private func stat(_ label: String, _ value: String) -> some View { VStack(alignment: .leading, spacing: canvas.u(4)) { - Text(label).seleneFont(12, .semibold).foregroundColor(SeleneTheme.ink60) + Text(L10n.text(label)).seleneFont(12, .semibold).foregroundColor(SeleneTheme.ink60) Text(value).seleneFont(21, .semibold).foregroundColor(SeleneTheme.ink) } } @@ -167,16 +173,16 @@ struct SpotlightCardInfoView: View { private func actionChip(_ label: String) -> some View { HStack(spacing: canvas.u(10)) { SelenePromptBadge(button: .a) - Text(label).seleneFont(19).foregroundColor(SeleneTheme.ink90) + Text(L10n.text(label)).seleneFont(19).foregroundColor(SeleneTheme.ink90) } } /// "0 min", "42 min", "3 h 20 min". static func duration(_ interval: TimeInterval) -> String { let minutes = Int(max(0, interval) / 60) - if minutes < 60 { return "\(minutes) min" } + if minutes < 60 { return L10n.format("%@ min", [String(describing: (minutes))]) } let rest = minutes % 60 - return rest == 0 ? "\(minutes / 60) h" : "\(minutes / 60) h \(rest) min" + return rest == 0 ? "\(minutes / 60) h" : L10n.format("%@ h %@ min", [String(describing: (minutes / 60)), String(describing: (rest))]) } } @@ -184,10 +190,10 @@ struct SpotlightCardInfoView: View { struct SpotlightCardMenu: View { @Environment(\.seleneCanvas) private var canvas - static let rows: [(title: String, subtitle: String, icon: MaterialIcon)] = [ - ("Move", "Change its place on Spotlight", .swap), - ("Hide card", "Bring it back in Settings", .hidden) - ] + static var rows: [(title: String, subtitle: String, icon: MaterialIcon)] { [ + ("Move", L10n.text("Change its place on Spotlight"), .swap), + (L10n.text("Hide card"), L10n.text("Bring it back in Settings"), .hidden) + ] } let card: SpotlightCard let focusedIndex: Int @@ -196,7 +202,7 @@ struct SpotlightCardMenu: View { var body: some View { VStack(alignment: .leading, spacing: canvas.u(14)) { - Text(card.title).seleneFont(28, .semibold).foregroundColor(SeleneTheme.ink) + Text(L10n.text(card.title)).seleneFont(28, .semibold).foregroundColor(SeleneTheme.ink) VStack(spacing: canvas.u(8)) { ForEach(Array(Self.rows.enumerated()), id: \.offset) { index, row in let focused = index == focusedIndex @@ -238,7 +244,7 @@ struct SpotlightFilterPill: View { var body: some View { HStack(spacing: canvas.u(12)) { - Text("\(filter.rawValue) · \(count)") + Text("\(L10n.text(filter.rawValue)) · \(count)") .seleneFont(17, .semibold) .foregroundColor(SeleneTheme.ink) HStack(spacing: canvas.u(6)) { diff --git a/Sources/NXLibrary/Views/Spotlight/SpotlightInfoView.swift b/Sources/NXLibrary/Views/Spotlight/SpotlightInfoView.swift index 14309b2..f6aeef3 100644 --- a/Sources/NXLibrary/Views/Spotlight/SpotlightInfoView.swift +++ b/Sources/NXLibrary/Views/Spotlight/SpotlightInfoView.swift @@ -45,7 +45,7 @@ struct SpotlightGameInfoView: View { .padding(.top, canvas.u(24)) } if case .failed(let failure) = pillSet { - Text(failure == .cancelled ? "Porting cancelled" : "Porting failed") + Text(failure == .cancelled ? L10n.text("Porting cancelled") : L10n.text("Porting failed")) .seleneFont(19, .semibold) .foregroundColor(PortingPillCopy.softRed) .padding(.leading, canvas.u(4)) @@ -128,10 +128,10 @@ struct SpotlightGameInfoView: View { Text(reason.detail).seleneFont(15).foregroundColor(SeleneTheme.ink60) case .normal: if !game.sources.isEmpty, !GameResourceState.sourcesAvailable(for: game) { - Text("Game files not found · Rebuild unavailable") + Text(L10n.text("Game files not found · Rebuild unavailable")) .seleneFont(15).foregroundColor(SeleneTheme.ink60) } else if !game.isPlayableLocally { - Text("Not built yet").seleneFont(15).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("Not built yet")).seleneFont(15).foregroundColor(SeleneTheme.ink60) } } } @@ -139,9 +139,9 @@ struct SpotlightGameInfoView: View { @ViewBuilder private var pills: some View { switch pillSet { case .porting: - pill(0, SelenePillButton(title: "Porting", height: 52, focused: focusedPill == 0, + pill(0, SelenePillButton(title: L10n.text("Porting"), height: 52, focused: focusedPill == 0, waveDots: true) { onPill(0) }) - .accessibilityLabel("Porting, show progress") + .accessibilityLabel(L10n.text("Porting, show progress")) case .failed: HStack(spacing: canvas.u(14)) { ForEach(Array(PortingPillCopy.failedTitles.enumerated()), id: \.offset) { index, title in @@ -158,19 +158,19 @@ struct SpotlightGameInfoView: View { HStack(spacing: canvas.u(14)) { pill(0, SelenePillButton(title: primaryTitle, height: 52, focused: focusedPill == 0, enabled: isRunning || pillSet.unavailableReason == nil) { onPill(0) }) - pill(1, SelenePillButton(title: isRunning ? "Close Game" : "Options", height: 52, + pill(1, SelenePillButton(title: isRunning ? L10n.text("Close Game") : L10n.text("Options"), height: 52, focused: focusedPill == 1) { onPill(1) }) pill(2, SelenePillButton(title: nil, icon: game.isFavorite ? .favorite : .favoriteBorder, iconSize: 20, minWidth: 52, height: 52, horizontalPadding: 0, focused: focusedPill == 2) { onPill(2) }) - .accessibilityLabel(game.isFavorite ? "Remove from favorites" : "Add to favorites") + .accessibilityLabel(game.isFavorite ? L10n.text("Remove from favorites") : L10n.text("Add to favorites")) } } private var primaryTitle: String { - if isRunning { return "Resume" } - if pillSet.unavailableReason != nil { return "Unavailable" } - return game.isPlayableLocally ? "Play" : "Build" + if isRunning { return L10n.text("Resume") } + if pillSet.unavailableReason != nil { return L10n.text("Unavailable") } + return game.isPlayableLocally ? L10n.text("Play") : "Build" } /// Selene's pill Button is 60 high around a 52 high capsule. @@ -185,11 +185,11 @@ struct SpotlightGameInfoView: View { NowPlayingDot() .frame(width: canvas.u(20), height: canvas.u(20)) .padding(.trailing, canvas.u(9)) - Text("NOW PLAYING").seleneFont(14, .bold).foregroundColor(SeleneTheme.success) + Text(L10n.text("NOW PLAYING")).seleneFont(14, .bold).foregroundColor(SeleneTheme.success) Rectangle().fill(SeleneTheme.ink25) .frame(width: max(1, canvas.u(1)), height: canvas.u(18)) .padding(.horizontal, canvas.u(14)) - Text("Continue where you left off").seleneFont(17).foregroundColor(SeleneTheme.ink90) + Text(L10n.text("Continue where you left off")).seleneFont(17).foregroundColor(SeleneTheme.ink90) } .padding(EdgeInsets(top: canvas.u(6), leading: canvas.u(12), bottom: canvas.u(7), trailing: canvas.u(18))) .background(RoundedRectangle(cornerRadius: canvas.u(19), style: .continuous).fill(SeleneTheme.glassLight)) @@ -262,8 +262,8 @@ enum PortingPillCopy { static let softRed = Color(hex: 0xF2A0A0) static func lastFailure(_ failure: PortingFailure, reason: PortingFailureReason? = nil) -> String { - if failure == .cancelled { return "Last porting cancelled · Open log" } - return "\(reason?.title ?? "Last porting failed") · Open log" + if failure == .cancelled { return L10n.text("Last porting cancelled · Open log") } + return L10n.format("%@ · Open log", [String(describing: (reason?.title ?? L10n.text("Last porting failed")))]) } } diff --git a/Sources/NXLibrary/Views/Spotlight/SpotlightPageSections.swift b/Sources/NXLibrary/Views/Spotlight/SpotlightPageSections.swift index 82785c4..cd7b269 100644 --- a/Sources/NXLibrary/Views/Spotlight/SpotlightPageSections.swift +++ b/Sources/NXLibrary/Views/Spotlight/SpotlightPageSections.swift @@ -66,6 +66,7 @@ struct SpotlightGenreChips: View { struct SpotlightDescription: View { @Environment(\.seleneCanvas) private var canvas let text: String + var languageNote: String? = nil let excerpt: DescriptionExcerpt let expanded: Bool let focused: Bool @@ -75,15 +76,19 @@ struct SpotlightDescription: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - DetailsBandTitle(text: "About this game") - Text(text.isEmpty ? "No description for this game yet." : (expanded ? text : excerpt.text)) + DetailsBandTitle(text: L10n.text("About this game")) + if let languageNote { + Text(languageNote).seleneFont(14).foregroundColor(SeleneTheme.ink60) + .padding(.bottom, canvas.u(8)) + } + Text(text.isEmpty ? L10n.text("No description for this game yet.") : (expanded ? text : excerpt.text)) .seleneFont(17) .lineSpacing(canvas.u(3)) .foregroundColor(text.isEmpty ? SeleneTheme.ink60 : SeleneTheme.ink90) .fixedSize(horizontal: false, vertical: true) .multilineTextAlignment(.leading) if excerpt.truncated { - Button(expanded ? "Show less" : "Read more", action: onToggle) + Button(expanded ? L10n.text("Show less") : L10n.text("Read more"), action: onToggle) .buttonStyle(.plain) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink) diff --git a/Sources/NXLibrary/Views/Spotlight/SpotlightRowView.swift b/Sources/NXLibrary/Views/Spotlight/SpotlightRowView.swift index 95166c2..78a1f23 100644 --- a/Sources/NXLibrary/Views/Spotlight/SpotlightRowView.swift +++ b/Sources/NXLibrary/Views/Spotlight/SpotlightRowView.swift @@ -189,11 +189,11 @@ struct SpotlightRowView: View { private func emptyMessage(_ filter: LibraryFilter) -> some View { let (title, hint): (String, String) = { switch filter { - case .installed: return ("Nothing installed", "Build a game and it shows up here.") - case .recentlyPlayed: return ("Nothing played lately", "Games you play show up here.") - case .favorites: return ("No favorites yet", "Choose the heart on a game to add it to your favorites.") - case .notPlayed: return ("You've started everything", "Every game in your library has been played at least once.") - case .all: return ("No games yet", "Add a game to start your collection.") + case .installed: return (L10n.text("Nothing installed"), L10n.text("Build a game and it shows up here.")) + case .recentlyPlayed: return (L10n.text("Nothing played lately"), L10n.text("Games you play show up here.")) + case .favorites: return (L10n.text("No favorites yet"), L10n.text("Choose the heart on a game to add it to your favorites.")) + case .notPlayed: return (L10n.text("You've started everything"), L10n.text("Every game in your library has been played at least once.")) + case .all: return (L10n.text("No games yet"), L10n.text("Add a game to start your collection.")) } }() return VStack(alignment: .leading, spacing: canvas.u(6)) { diff --git a/Sources/NXLibrary/Views/Spotlight/SpotlightScreenshotStrip.swift b/Sources/NXLibrary/Views/Spotlight/SpotlightScreenshotStrip.swift index cc90416..b8ab9c7 100644 --- a/Sources/NXLibrary/Views/Spotlight/SpotlightScreenshotStrip.swift +++ b/Sources/NXLibrary/Views/Spotlight/SpotlightScreenshotStrip.swift @@ -49,7 +49,7 @@ struct SpotlightScreenshotStrip: View { var body: some View { if shown.isEmpty { if known { - Text("No screenshots for this game") + Text(L10n.text("No screenshots for this game")) .seleneFont(18) .foregroundColor(SeleneTheme.ink60) .shadow(color: .black.opacity(0.45), radius: canvas.u(4)) @@ -129,7 +129,7 @@ private struct ScreenshotTile: View { .scaleEffect(focused ? style.focusScale : 1) .animation(reduceMotion ? nil : SpotlightScreenshotStrip.motion, value: focused) .accessibilityElement() - .accessibilityLabel("Screenshot \(index + 1)") + .accessibilityLabel(L10n.format("Screenshot %@", [String(describing: (index + 1))])) .task(id: "\(titleID)|\(index)|\(url.absoluteString)") { image = nil guard let file = await artwork.screenshot(titleID: titleID, index: index, remoteURL: url), diff --git a/Sources/NXLibrary/Views/Spotlight/SpotlightTopBar.swift b/Sources/NXLibrary/Views/Spotlight/SpotlightTopBar.swift index db7584d..b2b965f 100644 --- a/Sources/NXLibrary/Views/Spotlight/SpotlightTopBar.swift +++ b/Sources/NXLibrary/Views/Spotlight/SpotlightTopBar.swift @@ -41,7 +41,7 @@ struct SpotlightTopBar: View { private func tab(_ item: LibraryFilter) -> some View { let on = item == filter - return Text(item.rawValue) + return Text(L10n.text(item.rawValue)) .seleneFont(19, on ? .semibold : .regular) .foregroundColor(on ? SeleneTheme.onFocus : SeleneTheme.ink.opacity(0.7)) .padding(EdgeInsets(top: canvas.u(5), leading: canvas.u(18), diff --git a/Sources/NXLibrary/Views/Spotlight/SpotlightView.swift b/Sources/NXLibrary/Views/Spotlight/SpotlightView.swift index 3ab8eb3..819ae18 100644 --- a/Sources/NXLibrary/Views/Spotlight/SpotlightView.swift +++ b/Sources/NXLibrary/Views/Spotlight/SpotlightView.swift @@ -437,8 +437,8 @@ public struct SpotlightView: View { static func poolLabel(_ pool: SurprisePool) -> String { switch pool { - case .installed: return "Installed games" - case .notPlayed: return "Games not played yet" + case .installed: return L10n.text("Installed games") + case .notPlayed: return L10n.text("Games not played yet") case .all: return "All games" } } @@ -488,44 +488,44 @@ public struct SpotlightView: View { private func contextHints(_ model: Snapshot, _ state: SpotlightState) -> [HintItem] { if state.isLifted { - return [HintItem(nil, "◂ ▸ Move"), HintItem(.a, "Drop"), HintItem(.b, "Cancel")] + return [HintItem(nil, L10n.text("◂ ▸ Move")), HintItem(.a, L10n.text("Drop")), HintItem(.b, L10n.text("Cancel"))] } if cardMenu != nil { - return [HintItem(.a, "Select"), HintItem(.b, "Back")] + return [HintItem(.a, L10n.text("Select")), HintItem(.b, L10n.text("Back"))] } switch state.focus { case .pills: - return [HintItem(.b, "Back"), HintItem(.a, "Select")] + return [HintItem(.b, L10n.text("Back")), HintItem(.a, L10n.text("Select"))] case .strip: - return [HintItem(.b, "Back"), HintItem(.a, "View")] + return [HintItem(.b, L10n.text("Back")), HintItem(.a, L10n.text("View"))] case .mods, .about, .actions: - return [HintItem(.b, "Back"), HintItem(.a, "Select")] + return [HintItem(.b, L10n.text("Back")), HintItem(.a, L10n.text("Select"))] case .more: - return [HintItem(.b, "Back"), HintItem(.a, "Show")] + return [HintItem(.b, L10n.text("Back")), HintItem(.a, L10n.text("Show"))] case .topGames: - return [HintItem(.a, "Show")] + return [HintItem(.a, L10n.text("Show"))] case .store: // On the sort bar A sorts, and only when the focused order is not the one shown already. - guard storeHost.focus.kind == .sort else { return [HintItem(.b, "Back"), HintItem(.a, "View")] } + guard storeHost.focus.kind == .sort else { return [HintItem(.b, L10n.text("Back")), HintItem(.a, L10n.text("View"))] } let chosen = ModStoreSort.allCases.firstIndex(of: storeHost.sort) == storeHost.focus.index - return chosen ? [HintItem(.b, "Back")] : [HintItem(.b, "Back"), HintItem(.a, "Sort")] + return chosen ? [HintItem(.b, L10n.text("Back"))] : [HintItem(.b, L10n.text("Back")), HintItem(.a, L10n.text("Sort"))] case .row: switch model.selectedItem { case .card(let card): - let more = [HintItem(.y, "Card options")] - return card == .activity ? more : more + [HintItem(.a, card == .surprise ? "Pick" : card == .mods ? "Browse" : "Open")] + let more = [HintItem(.y, L10n.text("Card options"))] + return card == .activity ? more : more + [HintItem(.a, card == .surprise ? L10n.text("Pick") : card == .mods ? L10n.text("Browse") : L10n.text("Open"))] case .game: guard let game = model.selectedGame else { return [] } if viewModel.portingPills(game) == .porting { - return [HintItem(.y, "Options"), HintItem(.a, "Porting")] + return [HintItem(.y, L10n.text("Options")), HintItem(.a, L10n.text("Porting"))] } if viewModel.portingPills(game).unavailableReason != nil, !runningGames.isRunning(game) { - return [HintItem(.y, "Options")] + return [HintItem(.y, L10n.text("Options"))] } if game.isPlayableLocally { - return [HintItem(.y, "Options"), HintItem(.a, "Play")] + return [HintItem(.y, L10n.text("Options")), HintItem(.a, L10n.text("Play"))] } - return [HintItem(.y, "Options"), HintItem(.a, "Build")] + return [HintItem(.y, L10n.text("Options")), HintItem(.a, L10n.text("Build"))] case nil: return [] } @@ -897,7 +897,7 @@ public struct SpotlightView: View { private func pageBelow(_ game: PortedGame, page: SpotlightPageModel, state: SpotlightState) -> some View { VStack(alignment: .leading, spacing: 0) { HStack(alignment: .top, spacing: 0) { - SpotlightDescription(text: page.presentation.description, excerpt: aboutExcerpt(page), + SpotlightDescription(text: page.presentation.description, languageNote: page.presentation.descriptionLanguageNote, excerpt: aboutExcerpt(page), expanded: aboutExpanded, focused: state.focus == .about, onToggle: { aboutExpanded.toggle() }) .spotlightBlock(.about) diff --git a/Sources/NXLibrary/Views/Store/ModStorePage.swift b/Sources/NXLibrary/Views/Store/ModStorePage.swift index 3d945b3..570fddb 100644 --- a/Sources/NXLibrary/Views/Store/ModStorePage.swift +++ b/Sources/NXLibrary/Views/Store/ModStorePage.swift @@ -69,7 +69,7 @@ struct ModStorePage: View { Text(line).seleneFont(18, .medium).foregroundColor(SeleneTheme.ink75) } } - Text("Mod data provided by GameBanana · only mods that work with NX Library are shown") + Text(L10n.text("Mod data provided by GameBanana · only mods that work with NX Library are shown")) .seleneFont(14) .foregroundColor(SeleneTheme.ink60) } @@ -79,15 +79,15 @@ struct ModStorePage: View { private var statusLine: String? { switch catalog.phase { - case .offline: return "No connection. Mods you installed still work." - case .loading: return "Looking for mods that work with your games…" + case .offline: return L10n.text("No connection. Mods you installed still work.") + case .loading: return L10n.text("Looking for mods that work with your games…") case .idle: return nil case .ready: if catalog.games.isEmpty { - return catalog.refreshing ? "Looking for mods that work with your games…" - : "No mods for your games work with NX Library yet." + return catalog.refreshing ? L10n.text("Looking for mods that work with your games…") + : L10n.text("No mods for your games work with NX Library yet.") } - return catalog.refreshing ? "Looking for more mods…" : nil + return catalog.refreshing ? L10n.text("Looking for more mods…") : nil } } } @@ -124,7 +124,7 @@ struct ModStoreSortBar: View { .contentShape(Capsule()) .onTapGesture { onPick(index) } .accessibilityElement(children: .ignore) - .accessibilityLabel("Sort by \(choice.title)") + .accessibilityLabel(L10n.format("Sort by %@", [String(describing: (choice.title))])) .accessibilityAddTraits(chosen ? [.isButton, .isSelected] : .isButton) } } @@ -166,7 +166,7 @@ struct ModStoreFeatured<Chip: View>: View { .contentShape(shape) .onTapGesture(perform: onOpen) .accessibilityElement(children: .ignore) - .accessibilityLabel(entry.map { "Featured: \($0.mod.name), for \($0.gameName), by \($0.mod.author)" } ?? "Featured") + .accessibilityLabel(entry.map { L10n.format("Featured: %@, for %@, by %@", [String(describing: ($0.mod.name)), String(describing: ($0.gameName)), String(describing: ($0.mod.author))]) } ?? L10n.text("Featured")) .accessibilityAddTraits(.isButton) } @@ -181,7 +181,7 @@ struct ModStoreFeatured<Chip: View>: View { startPoint: .leading, endPoint: .trailing) HStack(alignment: .center, spacing: canvas.u(48)) { VStack(alignment: .leading, spacing: canvas.u(12)) { - Text("FEATURED · \(entry.gameName.uppercased())") + Text(L10n.format("FEATURED · %@", [String(describing: (entry.gameName.uppercased()))])) .seleneFont(15, .semibold) .foregroundColor(SeleneTheme.ink75) .lineLimit(1) @@ -190,7 +190,7 @@ struct ModStoreFeatured<Chip: View>: View { .foregroundColor(SeleneTheme.ink) .lineLimit(2) .fixedSize(horizontal: false, vertical: true) - Text("by \(entry.mod.author)") + Text(L10n.format("by %@", [String(describing: (entry.mod.author))])) .seleneFont(19, .medium) .foregroundColor(SeleneTheme.ink82) HStack(spacing: canvas.u(22)) { @@ -203,7 +203,7 @@ struct ModStoreFeatured<Chip: View>: View { chip(entry) HStack(spacing: canvas.u(8)) { SelenePromptBadge(button: .a) - Text("View").seleneFont(18).foregroundColor(SeleneTheme.ink90) + Text(L10n.text("View")).seleneFont(18).foregroundColor(SeleneTheme.ink90) } } .padding(.top, canvas.u(10)) @@ -276,7 +276,7 @@ struct ModStoreRow: View { .iconOnCaps(of: Self.titleSize, .semibold) } DetailsBandTitle(text: section.title, size: Self.titleSize) - Text(section.entries.count == 1 ? "1 mod" : "\(section.entries.count) mods") + Text(section.entries.count == 1 ? L10n.text("1 mod") : L10n.format("%@ mods", [String(describing: (section.entries.count))])) .seleneFont(16, .medium) .foregroundColor(SeleneTheme.ink60) } @@ -329,7 +329,7 @@ struct ModStoreTile: View { .seleneFont(19, .semibold) .foregroundColor(SeleneTheme.ink) .lineLimit(1) - Text(showsGame ? entry.gameName : "by \(entry.mod.author)") + Text(showsGame ? entry.gameName : L10n.format("by %@", [String(describing: (entry.mod.author))])) .seleneFont(14, .medium) .foregroundColor(SeleneTheme.ink75) .lineLimit(1) @@ -349,7 +349,7 @@ struct ModStoreTile: View { .animation(reduceMotion ? nil : SpotlightScreenshotStrip.motion, value: focused) .contentShape(Rectangle()) .accessibilityElement(children: .ignore) - .accessibilityLabel("\(entry.mod.name), for \(entry.gameName), by \(entry.mod.author)") + .accessibilityLabel(L10n.format("%@, for %@, by %@", [String(describing: (entry.mod.name)), String(describing: (entry.gameName)), String(describing: (entry.mod.author))])) .accessibilityAddTraits(focused ? [.isButton, .isSelected] : .isButton) } } diff --git a/Sources/NXLibrary/Views/Theme/AppLocaleEnvironment.swift b/Sources/NXLibrary/Views/Theme/AppLocaleEnvironment.swift new file mode 100644 index 0000000..0cc224f --- /dev/null +++ b/Sources/NXLibrary/Views/Theme/AppLocaleEnvironment.swift @@ -0,0 +1,14 @@ +import SwiftUI +import NXLibraryCore + +private struct AppLocaleEnvironmentKey: EnvironmentKey { + static let defaultValue = AppLocale(language: .english, region: "US") +} + +extension EnvironmentValues { + /// Declares a view's dependency on app-language changes without replacing its identity or state. + var nxAppLocale: AppLocale { + get { self[AppLocaleEnvironmentKey.self] } + set { self[AppLocaleEnvironmentKey.self] = newValue } + } +} diff --git a/Sources/NXLibrary/Views/Theme/OnlineBrowsePanel.swift b/Sources/NXLibrary/Views/Theme/OnlineBrowsePanel.swift index c87f5e2..575004a 100644 --- a/Sources/NXLibrary/Views/Theme/OnlineBrowsePanel.swift +++ b/Sources/NXLibrary/Views/Theme/OnlineBrowsePanel.swift @@ -141,10 +141,10 @@ struct OnlineBrowsePanel: View { private var status: String { let source = state.source.title - if failed { return "\(source.uppercased()) IS UNAVAILABLE RIGHT NOW" } - if state.category.isSearch && state.query.isEmpty { return "TYPE WHAT YOU'RE LOOKING FOR, THEN RETURN" } - if state.items.isEmpty { return loading ? "LOOKING ON \(source.uppercased())…" : "NOTHING FOUND" } - return "ONLINE · \(source.uppercased()) · \(state.category.title.uppercased())" + if failed { return L10n.format("%@ IS UNAVAILABLE RIGHT NOW", [String(describing: (source.uppercased()))]) } + if state.category.isSearch && state.query.isEmpty { return L10n.text("TYPE WHAT YOU'RE LOOKING FOR, THEN RETURN") } + if state.items.isEmpty { return loading ? L10n.format("LOOKING ON %@…", [String(describing: (source.uppercased()))]) : L10n.text("NOTHING FOUND") } + return L10n.format("ONLINE · %@ · %@", [String(describing: (source.uppercased())), String(describing: (state.category.displayTitle.uppercased()))]) } private var chips: some View { @@ -153,7 +153,7 @@ struct OnlineBrowsePanel: View { HStack(spacing: canvas.u(6)) { ForEach(Array(state.categories.enumerated()), id: \.offset) { index, category in let selected = index == state.categoryIndex - Text(category.title) + Text(category.displayTitle) .seleneFont(16, .semibold) .foregroundColor(selected ? SeleneTheme.ink : SeleneTheme.ink60) .padding(.horizontal, canvas.u(16)) @@ -181,7 +181,7 @@ struct OnlineBrowsePanel: View { private var searchField: some View { HStack(spacing: canvas.u(10)) { MaterialIconView(.search, size: 22).foregroundColor(SeleneTheme.ink60) - TextField("", text: $searchText, prompt: Text("Search \(state.source.title)").foregroundColor(SeleneTheme.ink60)) + TextField("", text: $searchText, prompt: Text(L10n.format("Search %@", [String(describing: (state.source.title))])).foregroundColor(SeleneTheme.ink60)) .textFieldStyle(.plain) .seleneFont(18) .foregroundColor(SeleneTheme.ink) @@ -292,7 +292,7 @@ private struct OnlineTile: View { .padding(.bottom, canvas.u(14)) .contentShape(Rectangle()) .accessibilityElement(children: .ignore) - .accessibilityLabel("\(item.title), \(item.width) by \(item.height)") + .accessibilityLabel(L10n.format("%@, %@ by %@", [String(describing: (item.title)), String(describing: (item.width)), String(describing: (item.height))])) .accessibilityAddTraits(focused ? [.isButton, .isSelected] : .isButton) } } diff --git a/Sources/NXLibrary/Views/Theme/ThemePickerView.swift b/Sources/NXLibrary/Views/Theme/ThemePickerView.swift index 076b85e..af9225b 100644 --- a/Sources/NXLibrary/Views/Theme/ThemePickerView.swift +++ b/Sources/NXLibrary/Views/Theme/ThemePickerView.swift @@ -22,7 +22,7 @@ struct ThemePickerView: View { @ObservedObject private var theme = ThemeStore.shared @State private var state = ThemePickerState(themeIndex: 0, backgroundIndex: 0) @State private var opened = false - @State private var errorTitle = "Couldn't add the picture" + @State private var errorTitle = L10n.text("Couldn't add the picture") @StateObject private var browser = OnlineBrowser() @StateObject private var transfer = OnlineTransfer() @State private var searchText = "" @@ -67,7 +67,7 @@ struct ThemePickerView: View { .frame(maxHeight: .infinity, alignment: .bottom) .padding(.bottom, canvas.u(124)) - SeleneHintsBar(left: [HintItem(.b, transfer.itemID != nil ? "Cancel" : "Back", action: back)], right: rightHints, + SeleneHintsBar(left: [HintItem(.b, transfer.itemID != nil ? L10n.text("Cancel") : L10n.text("Back"), action: back)], right: rightHints, bottom: SeleneBottomScrim.hintsBottom, leftInset: 18) .frame(maxHeight: .infinity, alignment: .bottom) @@ -102,27 +102,27 @@ struct ThemePickerView: View { private var rightHints: [HintItem] { switch state.step { case .theme: - return [HintItem(.a, "Next", action: accept)] + return [HintItem(.a, L10n.text("Next"), action: accept)] case .background: if let browse = browser.state { guard transfer.itemID == nil else { return [] } if let item = browse.focusedItem { let have = theme.wallpapers.contains { $0.id == OnlineDownload.wallpaperID(for: item) } - return [HintItem(.x, "Open page", action: { NSWorkspace.shared.open(item.pageURL) }), - HintItem(.a, have ? "Done" : "Download", action: accept)] + return [HintItem(.x, L10n.text("Open page"), action: { NSWorkspace.shared.open(item.pageURL) }), + HintItem(.a, have ? L10n.text("Done") : L10n.text("Download"), action: accept)] } - return browse.category.isSearch ? [HintItem(.a, "Search", action: accept)] : [] + return browse.category.isSearch ? [HintItem(.a, L10n.text("Search"), action: accept)] : [] } var hints: [HintItem] = [] if case .wallpaper(let id) = focusedBackground, let credit = theme.credits[id] { - hints.append(HintItem(.x, "Open page", action: { NSWorkspace.shared.open(credit.pageURL) })) + hints.append(HintItem(.x, L10n.text("Open page"), action: { NSWorkspace.shared.open(credit.pageURL) })) } if case .wallpaper(let id) = focusedBackground, ThemeStore.isRemovable(id) { - hints.append(HintItem(.y, "Remove", action: removeFocused)) + hints.append(HintItem(.y, L10n.text("Remove"), action: removeFocused)) } let verb: String switch focusedBackground { - case .add: verb = "Add" + case .add: verb = L10n.text("Add") case .online: verb = "Browse" default: verb = "Done" } @@ -155,7 +155,7 @@ struct ThemePickerView: View { private var themeStep: some View { let palette = palettes[min(state.themeIndex, palettes.count - 1)] return ZStack(alignment: .top) { - header("Choose your theme", "Colors for all of NX Library. Move through them to see each one in the preview.") + header(L10n.text("Choose your theme"), L10n.text("Colors for all of NX Library. Move through them to see each one in the preview.")) HStack(alignment: .top, spacing: canvas.u(60)) { grid(count: palettes.count, columns: ThemePickerState.themeColumns) { index in swatch(palettes[index], focused: index == state.themeIndex) @@ -167,7 +167,7 @@ struct ThemePickerView: View { chips(palette) .padding(.top, canvas.u(18)) .padding(.leading, canvas.u(4)) - Text(palette.name + (palette.mode == .light ? " · light" : "")) + Text(palette.name + (palette.mode == .light ? L10n.text(" · light") : "")) .seleneFont(26) .foregroundColor(SeleneTheme.ink) .padding(.top, canvas.u(12)) @@ -193,7 +193,7 @@ struct ThemePickerView: View { .init(color: background.opacity(0), location: 1)], startPoint: .leading, endPoint: .trailing) HStack { - Text("Aa").seleneFont(22).foregroundColor(Color(hex: palette.ink)) + Text(L10n.text("Aa")).seleneFont(22).foregroundColor(Color(hex: palette.ink)) Spacer(minLength: 0) Circle() .fill(Color(hex: palette.glyph)) @@ -258,7 +258,7 @@ struct ThemePickerView: View { if let browse = browser.state { header(browse.source.title, Self.about(browse.source)) } else { - header("Choose your background", "Behind the menus and the home when no game is selected.") + header(L10n.text("Choose your background"), L10n.text("Behind the menus and the home when no game is selected.")) } HStack(alignment: .top, spacing: canvas.u(50)) { if let browse = browser.state { @@ -337,7 +337,7 @@ struct ThemePickerView: View { HStack(spacing: canvas.u(8)) { SelenePromptBadge(button: .lb, height: 26).accessibilityHidden(true) ForEach(BackgroundTab.allCases, id: \.self) { tab in - Text(tab == .wallpapers ? "Wallpapers" : "Live") + Text(tab == .wallpapers ? L10n.text("Wallpapers") : L10n.text("Live")) .seleneFont(17, .semibold) .foregroundColor(tab == state.tab ? SeleneTheme.ink : SeleneTheme.ink60) .padding(.horizontal, canvas.u(20)) @@ -355,10 +355,10 @@ struct ThemePickerView: View { /// The line under a source's name while browsing it. static func about(_ source: BackgroundSourceID) -> String { switch source { - case .wallhaven: return "Safe for work, 2K and up. Every picture belongs to its owner." - case .danbooru: return "General rating only, 2K and up. Every picture belongs to its artist." - case .motionbgs: return "Looping live wallpapers in 4K and HD. Every video belongs to its owner." - case .steam: return "Animated profile backgrounds, made by the games' studios for Steam." + case .wallhaven: return L10n.text("Safe for work, 2K and up. Every picture belongs to its owner.") + case .danbooru: return L10n.text("General rating only, 2K and up. Every picture belongs to its artist.") + case .motionbgs: return L10n.text("Looping live wallpapers in 4K and HD. Every video belongs to its owner.") + case .steam: return L10n.text("Animated profile backgrounds, made by the games' studios for Steam.") } } @@ -375,7 +375,7 @@ struct ThemePickerView: View { if let browse = browser.state { guard let item = browse.focusedItem else { return nil } var parts = [item.source.title, item.title] - if let author = item.author { parts.append("art by \(author)") } + if let author = item.author { parts.append(L10n.format("art by %@", [String(describing: (author))])) } parts.append("\(item.width) × \(item.height)") return parts.joined(separator: " · ") } @@ -411,12 +411,12 @@ struct ThemePickerView: View { if state.tab == .live { MoonlightBackdrop(accent: SeleneTheme.glyph, ink: SeleneTheme.ink, moving: false, asImage: true) } - tileLabel(state.tab == .live ? "Moonlight" : "None") + tileLabel(state.tab == .live ? L10n.text("Moonlight") : "None") case .add: SeleneTheme.glassSolid VStack(spacing: canvas.u(6)) { MaterialIconView(.add, size: 34).foregroundColor(SeleneTheme.ink) - Text(state.tab == .live ? "Add your video…" : "Add your own…").seleneFont(15).foregroundColor(SeleneTheme.ink90) + Text(state.tab == .live ? L10n.text("Add your video…") : L10n.text("Add your own…")).seleneFont(15).foregroundColor(SeleneTheme.ink90) } .frame(maxWidth: .infinity, maxHeight: .infinity) case .wallpaper(let id): @@ -452,7 +452,7 @@ struct ThemePickerView: View { .contentShape(Rectangle()) .accessibilityElement(children: .ignore) .accessibilityLabel(label(choice)) - .accessibilityValue(inUse ? "In use" : "") + .accessibilityValue(inUse ? L10n.text("In use") : "") .accessibilityAddTraits(focused ? [.isButton, .isSelected] : .isButton) } @@ -462,7 +462,7 @@ struct ThemePickerView: View { VStack(spacing: canvas.u(6)) { MaterialIconView(.language, size: 30).foregroundColor(SeleneTheme.ink) Text(source.title).seleneFont(18, .semibold).foregroundColor(SeleneTheme.ink) - Text("Browse online").seleneFont(13).foregroundColor(SeleneTheme.ink60) + Text(L10n.text("Browse online")).seleneFont(13).foregroundColor(SeleneTheme.ink60) } } } @@ -480,11 +480,11 @@ struct ThemePickerView: View { private func label(_ choice: ThemeBackgroundChoice) -> String { switch choice { - case .theme: return "Theme's own" - case .none: return state.tab == .live ? "Moonlight" : "None" - case .add: return state.tab == .live ? "Add your video" : "Add your own" + case .theme: return L10n.text("Theme's own") + case .none: return state.tab == .live ? L10n.text("Moonlight") : L10n.text("None") + case .add: return state.tab == .live ? L10n.text("Add your video") : L10n.text("Add your own") case .wallpaper(let id): return theme.wallpaperTitle(for: id) - case .online(let source): return "Browse \(source.title)" + case .online(let source): return L10n.format("Browse %@", [String(describing: (source.title))]) } } @@ -676,7 +676,7 @@ struct ThemePickerView: View { theme.reloadWallpapers() use(id) case .failure(let error): - errorTitle = "Couldn't download the wallpaper" + errorTitle = L10n.text("Couldn't download the wallpaper") viewModel.showSettingsNotice(title: errorTitle, message: error.localizedDescription) } } @@ -731,7 +731,7 @@ struct ThemePickerView: View { } private func placementDialog(_ pending: PendingWallpaper) -> some View { - let name = pending.id == WallpaperCatalog.none ? "No wallpaper" + let name = pending.id == WallpaperCatalog.none ? L10n.text("No wallpaper") : pending.id.flatMap { theme.credits[$0]?.title } ?? theme.wallpaperTitle(for: pending.id) return ZStack { SeleneTheme.scrim45 @@ -739,7 +739,7 @@ struct ThemePickerView: View { .contentShape(Rectangle()) .onTapGesture { place(.notNow) } SeleneChoiceDialog(title: WallpaperPlacement.dialogTitle, - message: "\(name). You can change both wallpapers any time in Settings → Look.", + message: L10n.format("%@. You can change both wallpapers any time in Settings → Look.", [String(describing: (name))]), choices: WallpaperPlacement.allCases.map(\.title), focus: $placementFocus) { index in place(WallpaperPlacement.allCases[index]) } @@ -762,13 +762,13 @@ struct ThemePickerView: View { let live = state.tab == .live let panel = NSOpenPanel() panel.message = live - ? "Choose a video for your live background (MP4, MOV or M4V). NX Library keeps its own copy and plays it muted." - : "Choose a picture for your background. NX Library keeps its own copy." + ? L10n.text("Choose a video for your live background (MP4, MOV or M4V). NX Library keeps its own copy and plays it muted.") + : L10n.text("Choose a picture for your background. NX Library keeps its own copy.") panel.allowedContentTypes = live ? WallpaperCatalog.videoExtensions.compactMap { UTType(filenameExtension: $0) } : [.image] panel.allowsMultipleSelection = false - errorTitle = live ? "Couldn't add the video" : "Couldn't add the picture" + errorTitle = live ? L10n.text("Couldn't add the video") : L10n.text("Couldn't add the picture") guard panel.runModal() == .OK, let url = panel.url else { return } do { let id = try theme.importWallpaper(from: url) @@ -817,7 +817,7 @@ struct ThemePickerView: View { state.backgroundIndex = min(state.backgroundIndex, backgrounds.count - 1) SoundEffectService.shared.play(.click) } catch { - errorTitle = "Couldn't remove it" + errorTitle = L10n.text("Couldn't remove it") viewModel.showSettingsNotice(title: errorTitle, message: error.localizedDescription) } } @@ -825,20 +825,20 @@ struct ThemePickerView: View { /// A calm question: what goes, where it goes, and that it can come back. private func removalDialog(_ id: String) -> some View { let video = theme.wallpaper(for: id)?.kind == .video - let thing = video ? "video" : "picture" + let removalTitle = video ? L10n.text("Remove this video?") : L10n.text("Remove this picture?") let downloaded = !id.hasPrefix(WallpaperCatalog.custom + "/") let name = theme.credits[id]?.title ?? theme.wallpaperTitle(for: id) var message = downloaded - ? "\(name) goes to the Trash, so you can bring it back from there, or download it again whenever you like." - : "NX Library's copy of \(name) goes to the Trash, so you can bring it back from there. Your original file stays where it is." - if settings.settings.wallpaperID == id { message += " Your theme's own background takes its place." } + ? L10n.format("%@ goes to the Trash, so you can bring it back from there, or download it again whenever you like.", [String(describing: (name))]) + : L10n.format("NX Library's copy of %@ goes to the Trash, so you can bring it back from there. Your original file stays where it is.", [String(describing: (name))]) + if settings.settings.wallpaperID == id { message += L10n.text(" Your theme’s own background takes its place.") } return ZStack { SeleneTheme.scrim45 .ignoresSafeArea() .contentShape(Rectangle()) .onTapGesture { keepIt() } - SeleneConfirmDialog(title: "Remove this \(thing)?", message: message, confirmTitle: "Remove", - cancelTitle: "Keep it", focus: $removalFocus, + SeleneConfirmDialog(title: removalTitle, message: message, confirmTitle: "Remove", + cancelTitle: L10n.text("Keep it"), focus: $removalFocus, onCancel: keepIt, onConfirm: confirmRemoval) } } diff --git a/Sources/NXLibrary/Views/Theme/ThemePreview.swift b/Sources/NXLibrary/Views/Theme/ThemePreview.swift index 891d56e..12bce29 100644 --- a/Sources/NXLibrary/Views/Theme/ThemePreview.swift +++ b/Sources/NXLibrary/Views/Theme/ThemePreview.swift @@ -26,7 +26,7 @@ struct ThemePreview: View { let h = w * 9 / 16 let shape = RoundedRectangle(cornerRadius: canvas.u(16), style: .continuous) return VStack(alignment: .leading, spacing: canvas.u(10)) { - Text("PREVIEW") + Text(L10n.text("PREVIEW")) .seleneFont(13, .semibold) .foregroundColor(SeleneTheme.ink.opacity(0.55)) .padding(.leading, canvas.u(2)) @@ -72,8 +72,8 @@ struct ThemePreview: View { let tile = w * 0.085 return VStack(alignment: .leading, spacing: 0) { HStack(spacing: w * 0.02) { - Text("Spotlight").foregroundColor(ink) - Text("Collection").foregroundColor(ink.opacity(0.6)) + Text(L10n.text("Spotlight")).foregroundColor(ink) + Text(L10n.text("Collection")).foregroundColor(ink.opacity(0.6)) } .font(.system(size: w * 0.018, weight: .semibold)) HStack(spacing: w * 0.016) { @@ -96,18 +96,18 @@ struct ThemePreview: View { } } .padding(.top, h * 0.12) - Text(games.first?.name ?? "Your games") + Text(games.first?.name ?? L10n.text("Your games")) .font(.system(size: w * 0.034, weight: .semibold)) .foregroundColor(ink) .lineLimit(1) .padding(.top, h * 0.2) HStack(spacing: w * 0.012) { - Text("Play") + Text(L10n.text("Play")) .foregroundColor(Color(hex: palette.onFocus)) .padding(.horizontal, w * 0.04) .padding(.vertical, w * 0.01) .background(Capsule().fill(focus)) - Text("Details") + Text(L10n.text("Details")) .foregroundColor(ink) .padding(.horizontal, w * 0.032) .padding(.vertical, w * 0.01) diff --git a/Sources/NXLibrary/Views/Theme/ThemeStore.swift b/Sources/NXLibrary/Views/Theme/ThemeStore.swift index 791d028..ddae2b5 100644 --- a/Sources/NXLibrary/Views/Theme/ThemeStore.swift +++ b/Sources/NXLibrary/Views/Theme/ThemeStore.swift @@ -89,7 +89,7 @@ public final class ThemeStore: ObservableObject { func settingsWallpaperTitle(for settingsID: String?) -> String { switch settingsID { - case nil: return "Same as app" + case nil: return L10n.text("Same as app") case WallpaperCatalog.themeOwn?: return wallpaperTitle(for: nil) case let id?: return wallpaperTitle(for: id) } @@ -99,12 +99,12 @@ public final class ThemeStore: ObservableObject { func wallpaperTitle(for wallpaperID: String?) -> String { switch wallpaperID { case nil: - return wallpaper(for: nil).map { "Theme (\($0.title))" } ?? "Theme (none)" + return wallpaper(for: nil).map { L10n.format("Theme (%@)", [String(describing: ($0.title))]) } ?? L10n.text("Theme (none)") case WallpaperCatalog.none?: return "None" case let id?: if let credit = credits[id] { return "\(credit.title) · \(credit.source.title)" } - return wallpapers.first { $0.id == id }?.title ?? "Missing" + return wallpapers.first { $0.id == id }?.title ?? L10n.text("Missing") } } } diff --git a/Sources/NXLibraryCore/Models/AppIconProgress.swift b/Sources/NXLibraryCore/Models/AppIconProgress.swift index 4ce6bfa..4c1c2c1 100644 --- a/Sources/NXLibraryCore/Models/AppIconProgress.swift +++ b/Sources/NXLibraryCore/Models/AppIconProgress.swift @@ -7,18 +7,18 @@ public enum AppIconProgress: Equatable { public var message: String { switch self { - case .searching: return "Looking for an icon…" - case .updating: return "Updating the app icon in Finder and the Dock… large games can take a minute" - case .updated: return "App icon updated in Finder and the Dock" - case .launcherOnly: return "Icon used only in NX Library" - case .original: return "Using the game's original app icon" - case .unavailable: return "No icon available. You can choose one." - case .failed(let reason): return "The app icon was not changed: \(reason)" + case .searching: return L10n.text("Looking for an icon…") + case .updating: return L10n.text("Updating the app icon in Finder and the Dock… large games can take a minute") + case .updated: return L10n.text("App icon updated in Finder and the Dock") + case .launcherOnly: return L10n.text("Icon used only in NX Library") + case .original: return L10n.text("Using the game's original app icon") + case .unavailable: return L10n.text("No icon available. You can choose one.") + case .failed(let reason): return L10n.format("The app icon was not changed: %@", [String(describing: (reason))]) } } public var buildMessage: String { - if self == .unavailable { return "Game ready. No icon found; add one in Change artwork." } + if self == .unavailable { return L10n.text("Game ready. No icon found; add one in Change artwork.") } if case .failed = self { return "Game built. \(message)" } return "Game ready. \(message)" } diff --git a/Sources/NXLibraryCore/Models/AppLocale.swift b/Sources/NXLibraryCore/Models/AppLocale.swift new file mode 100644 index 0000000..e92c20b --- /dev/null +++ b/Sources/NXLibraryCore/Models/AppLocale.swift @@ -0,0 +1,61 @@ +import Foundation + +public enum AppLanguage: String, CaseIterable, Codable, Identifiable { + case automatic = "auto", english = "en", italian = "it", french = "fr", german = "de", spanish = "es" + case portugueseBrazil = "pt-BR" + + public var id: String { rawValue } + public var nativeName: String { + switch self { + case .automatic: return L10n.text("Automatic (Mac)") + case .english: return "English" + case .italian: return "Italiano" + case .french: return "Français" + case .german: return "Deutsch" + case .spanish: return "Español" + case .portugueseBrazil: return "Português (Brasil)" + } + } +} + +public struct AppLocale: Equatable { + public let language: AppLanguage + public let region: String + public var metadataLanguage: String { String(language.rawValue.prefix(2)) } + public var formatLocale: Locale { Locale(identifier: "\(metadataLanguage)_\(region)") } + public var cacheKey: String { "\(language.rawValue)_\(region)" } + + public init(language: AppLanguage, region: String?, preferredLanguages: [String] = Locale.preferredLanguages, + systemLocale: Locale = .current) { + if language != .automatic { self.language = language } + else { + self.language = preferredLanguages.lazy.compactMap { tag in + let base = tag.replacingOccurrences(of: "_", with: "-").lowercased().split(separator: "-").first.map(String.init) ?? "" + if base == "pt" { return AppLanguage.portugueseBrazil } + return AppLanguage.allCases.first { $0 != .automatic && $0.rawValue == base } + }.first ?? .english + } + self.region = Self.validRegion(region) ?? Self.validRegion(systemLocale.regionCode) ?? "US" + } + + public static func validRegion(_ value: String?) -> String? { + guard let value = value?.uppercased(), Locale.isoRegionCodes.contains(value) else { return nil } + return value + } +} + +public struct AppRegionOption: Identifiable, Equatable { + public let id: String + public let name: String + + public static func matching(_ query: String, context: AppLocale) -> [AppRegionOption] { + let locale = context.formatLocale + let needle = query.trimmingCharacters(in: .whitespacesAndNewlines) + return Locale.isoRegionCodes.map { code in + AppRegionOption(id: code, name: locale.localizedString(forRegionCode: code) ?? code) + }.filter { + needle.isEmpty || $0.id.localizedCaseInsensitiveContains(needle) + || $0.name.range(of: needle, options: [.caseInsensitive, .diacriticInsensitive], locale: locale) != nil + }.sorted { $0.name.compare($1.name, options: [.caseInsensitive, .diacriticInsensitive], locale: locale) == .orderedAscending } + } +} diff --git a/Sources/NXLibraryCore/Models/AppSettings.swift b/Sources/NXLibraryCore/Models/AppSettings.swift index 39f533f..448a1d1 100644 --- a/Sources/NXLibraryCore/Models/AppSettings.swift +++ b/Sources/NXLibraryCore/Models/AppSettings.swift @@ -4,7 +4,7 @@ public enum BackgroundShade: String, Codable, CaseIterable, Identifiable, Equata case light, balanced, deep public var id: String { rawValue } - public var label: String { rawValue.capitalized } + public var label: String { L10n.text(rawValue.capitalized) } public var heroScrimOpacity: Double { switch self { @@ -36,9 +36,9 @@ public enum BackgroundSizing: String, Codable, CaseIterable, Identifiable, Equat public var id: String { rawValue } public var title: String { switch self { - case .fill: return "Fill" - case .fit: return "Fit" - case .stretch: return "Stretch" + case .fill: return L10n.text("Fill") + case .fit: return L10n.text("Fit") + case .stretch: return L10n.text("Stretch") } } } @@ -52,9 +52,9 @@ public enum WallpaperBrightness: String, Codable, CaseIterable, Identifiable, Eq public var id: String { rawValue } public var title: String { switch self { - case .dim: return "Dim" - case .balanced: return "Bright on the right" - case .bright: return "Bright" + case .dim: return L10n.text("Dim") + case .balanced: return L10n.text("Bright on the right") + case .bright: return L10n.text("Bright") } } @@ -85,10 +85,10 @@ public enum LaunchStyle: String, Codable, CaseIterable, Identifiable, Equatable public var id: String { rawValue } public var title: String { switch self { - case .artAndLogo: return "Art and logo" - case .artOnly: return "Art only" - case .logoOnBlack: return "Logo on black" - case .minimal: return "Minimal" + case .artAndLogo: return L10n.text("Art and logo") + case .artOnly: return L10n.text("Art only") + case .logoOnBlack: return L10n.text("Logo on black") + case .minimal: return L10n.text("Minimal") } } } @@ -100,14 +100,16 @@ public enum LaunchShade: String, Codable, CaseIterable, Identifiable, Equatable public var id: String { rawValue } public var title: String { switch self { - case .none: return "None" - case .soft: return "Soft" - case .dark: return "Dark" + case .none: return L10n.text("None") + case .soft: return L10n.text("Soft") + case .dark: return L10n.text("Dark") } } } public struct AppSettings: Codable, Equatable { + public var appLanguage: AppLanguage = .automatic + public var appRegion: String? = nil public var backgroundShade: BackgroundShade public var readableText: Bool public var reduceMotion: Bool @@ -243,6 +245,7 @@ public struct AppSettings: Codable, Equatable { } enum CodingKeys: String, CodingKey { + case appLanguage, appRegion case backgroundShade, readableText, reduceMotion, showButtonHints, showClock case restoreLastGame, checkEngineAtLaunch, refreshLibraryAtLaunch, lastSelectedGameID case startInFullscreen @@ -257,6 +260,8 @@ public struct AppSettings: Codable, Equatable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + appLanguage = (try container.decodeIfPresent(String.self, forKey: .appLanguage)).flatMap(AppLanguage.init(rawValue:)) ?? .automatic + appRegion = AppLocale.validRegion(try container.decodeIfPresent(String.self, forKey: .appRegion)) self.backgroundShade = try container.decodeIfPresent(BackgroundShade.self, forKey: .backgroundShade) ?? .balanced self.readableText = try container.decodeIfPresent(Bool.self, forKey: .readableText) ?? true self.reduceMotion = try container.decodeIfPresent(Bool.self, forKey: .reduceMotion) ?? false diff --git a/Sources/NXLibraryCore/Models/BuildWarningCopy.swift b/Sources/NXLibraryCore/Models/BuildWarningCopy.swift index dd0774f..425cd04 100644 --- a/Sources/NXLibraryCore/Models/BuildWarningCopy.swift +++ b/Sources/NXLibraryCore/Models/BuildWarningCopy.swift @@ -5,12 +5,12 @@ import Foundation public enum BuildWarningCopy { public static func text(stage: String, summary: String?) -> String { if stage.localizedCaseInsensitiveContains("pipeline archive") { - return "The pipeline archive could not be made, so the app was packed without one. " - + "The game prepares its shaders the first time it needs them, so its first start can take longer. " - + "The log has the reason; rebuilding tries again." + return L10n.text("The pipeline archive could not be made, so the app was packed without one. ") + + L10n.text("The game prepares its shaders the first time it needs them, so its first start can take longer. ") + + L10n.text("The log has the reason; rebuilding tries again.") } let detail = summary.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.flatMap { $0.isEmpty ? nil : $0 } let name = stage.prefix(1).uppercased() + stage.dropFirst() - return "\(name) finished with a warning" + (detail.map { ": \($0)." } ?? ".") + " The log has the details." + return L10n.format("%@ finished with a warning", [String(describing: (name))]) + (detail.map { ": \($0)." } ?? ".") + L10n.text(" The log has the details.") } } diff --git a/Sources/NXLibraryCore/Models/ButtonPrompts.swift b/Sources/NXLibraryCore/Models/ButtonPrompts.swift index 0fa525f..10a4c69 100644 --- a/Sources/NXLibraryCore/Models/ButtonPrompts.swift +++ b/Sources/NXLibraryCore/Models/ButtonPrompts.swift @@ -33,14 +33,14 @@ public enum ButtonPrompts { public static func keyboardKey(_ button: PromptButton) -> KeyCap { switch button { case .a: return .symbol("return") - case .b: return .text("esc") + case .b: return .text(L10n.text("esc")) case .x: return .text("X") case .y: return .text("Y") case .lb: return .text("Q") case .rb, .rs: return .text("E") - case .lt: return .text("home") - case .rt: return .text("end") - case .view: return .text("tab") + case .lt: return .text(L10n.text("home")) + case .rt: return .text(L10n.text("end")) + case .view: return .text(L10n.text("tab")) case .menu: return .text("⌘K") } } @@ -146,8 +146,8 @@ public enum ButtonPrompts { switch b { case .a: return "A" case .b: return "B" - case .x: return "X" - case .y: return "Y" + case .x: return L10n.text("X") + case .y: return L10n.text("Y") case .lb: return "L" case .rb: return "R" case .lt: return "ZL" @@ -162,8 +162,8 @@ public enum ButtonPrompts { switch b { case .a: return "A" case .b: return "B" - case .x: return "X" - case .y: return "Y" + case .x: return L10n.text("X") + case .y: return L10n.text("Y") case .lb: return "LB" case .rb: return "RB" case .lt: return "LT" diff --git a/Sources/NXLibraryCore/Models/CheatCopy.swift b/Sources/NXLibraryCore/Models/CheatCopy.swift index a50c20a..df2283d 100644 --- a/Sources/NXLibraryCore/Models/CheatCopy.swift +++ b/Sources/NXLibraryCore/Models/CheatCopy.swift @@ -4,15 +4,16 @@ import Foundation public enum CheatCopy { /// Under the header: what a cheat is, against a mod. Players look for cheats among GameBanana's mods, where the /// cheat-like ones (unbreakable weapons, more items) are file mods and belong in Mods. - public static let about = - "Cheats change values while you play, for this version only. New items, looks or gameplay are in Mods." + public static var about: String { + L10n.text("Cheats change values while you play, for this version only. New items, looks or gameplay are in Mods.") + } /// "Version 1.2.0 · build B424BE15…": the build ID cut to its first 8 hex digits. public static func header(version: String?, buildID: String?) -> String { let build = buildID.map { "build \($0.prefix(8))…" } switch (version, build) { - case let (version?, build?): return "Version \(version) · \(build)" - case let (version?, nil): return "Version \(version)" + case let (version?, build?): return L10n.format("Version %@", [version]) + " · " + build + case let (version?, nil): return L10n.format("Version %@", [version]) case let (nil, build?): return build.prefix(1).uppercased() + build.dropFirst() case (nil, nil): return "" } @@ -23,14 +24,18 @@ public enum CheatCopy { switch decision { case .install, .confirmUnnamed: return nil case .otherBuild(let other): - return "These cheats are for another version of \(gameName) (build \(other)). Yours is build \(buildID)." - case .otherTitle(let other): return "These cheats are for another game (title ID \(other))." + return L10n.format("These cheats are for another version of %@ (build %@). Yours is build %@.", [String(describing: (gameName)), String(describing: (other)), String(describing: (buildID))]) + case .otherTitle(let other): return L10n.format("These cheats are for another game (title ID %@).", [String(describing: (other))]) case .refused(let reason): return reason } } - public static let several = "Several files could be these cheats: add the one you want." - public static let noFile = "There is no cheat file (.txt) there." + public static var several: String { + L10n.text("Several files could be these cheats: add the one you want.") + } + public static var noFile: String { + L10n.text("There is no cheat file (.txt) there.") + } /// The confirmation before every install: the parser's summary, the warning for a file whose name says no build, /// and what replacing keeps. @@ -38,18 +43,18 @@ public enum CheatCopy { buildID: String) -> (title: String, message: String, confirm: String) { var parts: [String] = [] if unnamed { - let target = version.map { "version \($0) (build \(buildID))" } ?? "build \(buildID)" - parts.append("Install as cheats for \(target)? Cheats made for another version can crash the game.") + let target = version.map { L10n.format("version %@ (build %@)", [String(describing: ($0)), String(describing: (buildID))]) } ?? "build \(buildID)" + parts.append(L10n.format("Install as cheats for %@? Cheats made for another version can crash the game.", [String(describing: (target))])) } parts.append(file.summaryText) - parts.append(replacing ? "Cheats that were on and keep their name stay on; the rest start off." - : "Every cheat starts off.") - let title = replacing ? "Replace cheats?" : unnamed ? "Install these cheats?" : "Install cheats?" + parts.append(replacing ? L10n.text("Cheats that were on and keep their name stay on; the rest start off.") + : L10n.text("Every cheat starts off.")) + let title = replacing ? L10n.text("Replace cheats?") : unnamed ? L10n.text("Install these cheats?") : L10n.text("Install cheats?") return (title, parts.joined(separator: "\n\n"), replacing ? "Replace" : "Install") } public static func removal(build: String) -> (title: String, message: String, confirm: String) { - ("Remove cheats?", "The cheat files for build \(build) go to the Trash.", "Remove") + ("Remove cheats?", L10n.format("The cheat files for build %@ go to the Trash.", [String(describing: (build))]), "Remove") } /// A switch's name: an on / off pair shows its base name ("Moon Jump"), every other entry its key. @@ -60,14 +65,16 @@ public enum CheatCopy { return split.base } - public static func otherBuild(_ build: String) -> String { "Build \(build) — not used by this version" } + public static func otherBuild(_ build: String) -> String { L10n.format("Build %@ — not used by this version", [String(describing: (build))]) } public static func running(gameName: String) -> String { - "\(gameName) is running: change cheats in its Cheats panel (⌘⇧C)." + L10n.format("%@ is running: change cheats in its Cheats panel (⌘⇧C).", [String(describing: (gameName))]) } - public static let safeMode = "The last session closed unexpectedly with cheats on. Next start, cheats begin off; " - + "turn them back on here or in the game's Cheats panel (⌘⇧C)." + public static var safeMode: String { + L10n.text("The last session closed unexpectedly with cheats on. Next start, cheats begin off; ") + + L10n.text("turn them back on here or in the game's Cheats panel (⌘⇧C).") + } } extension CheatInstall { diff --git a/Sources/NXLibraryCore/Models/CheatFile.swift b/Sources/NXLibraryCore/Models/CheatFile.swift index 09d2370..cb4cc2a 100644 --- a/Sources/NXLibraryCore/Models/CheatFile.swift +++ b/Sources/NXLibraryCore/Models/CheatFile.swift @@ -313,7 +313,7 @@ enum CheatDecoder { } static func length(_ p: ArraySlice<UInt32>) -> Result<Int, Refusal> { - guard let w = p.first else { return .failure(Refusal(reason: "truncated")) } + guard let w = p.first else { return .failure(Refusal(reason: L10n.text("truncated"))) } let n = p.count func refuse(_ r: String) -> Result<Int, Refusal> { .failure(Refusal(reason: r)) } func widthOK(_ t: UInt32) -> Bool { t == 1 || t == 2 || t == 4 || t == 8 } diff --git a/Sources/NXLibraryCore/Models/CloudSaves.swift b/Sources/NXLibraryCore/Models/CloudSaves.swift index caf6127..06a03f3 100644 --- a/Sources/NXLibraryCore/Models/CloudSaves.swift +++ b/Sources/NXLibraryCore/Models/CloudSaves.swift @@ -30,9 +30,9 @@ public enum CloudSaves: Codable, Equatable { public var title: String { switch self { - case .off: return "Off" - case .iCloudDrive: return "iCloud Drive" - case .folder: return "Folder" + case .off: return L10n.text("Off") + case .iCloudDrive: return L10n.text("iCloud Drive") + case .folder: return L10n.text("Folder") } } diff --git a/Sources/NXLibraryCore/Models/GameDetailPresentation.swift b/Sources/NXLibraryCore/Models/GameDetailPresentation.swift index 30f2184..9afe36c 100644 --- a/Sources/NXLibraryCore/Models/GameDetailPresentation.swift +++ b/Sources/NXLibraryCore/Models/GameDetailPresentation.swift @@ -6,6 +6,8 @@ public struct GameDetailPresentation: Equatable { public let console: String public let eyebrow: String public let description: String + public let descriptionIsEnglishFallback: Bool + public let descriptionLanguageNote: String? public let genres: [String] public let released: String? public let developerLine: String? @@ -33,16 +35,18 @@ public struct GameDetailPresentation: Equatable { totalPlaytime: TimeInterval, lastPlayed: Date?, now: Date = Date(), sessions: [PlaySession] = [], libraryGames: [PortedGame] = [], playtimeByGame: [UUID: TimeInterval] = [:], - calendar: Calendar = .current, locale: Locale = .current) { + calendar: Calendar = .current, locale: Locale = L10n.context.formatLocale) { title = game.name console = game.consoleName eyebrow = Self.eyebrow(from: metadata?.name ?? "") description = metadata?.description ?? "" - var seenGenres = Set<String>() - genres = (metadata?.genres ?? []).filter { - let g = $0.trimmingCharacters(in: .whitespacesAndNewlines) - return !g.isEmpty && g.caseInsensitiveCompare("Other") != .orderedSame && seenGenres.insert(g).inserted - } + descriptionIsEnglishFallback = L10n.context.metadataLanguage != "en" && metadata?.descriptionLanguage == "en" && !description.isEmpty + if let language = metadata?.descriptionLanguage, language != "custom", + language != L10n.context.metadataLanguage, !description.isEmpty { + let name = L10n.context.formatLocale.localizedString(forLanguageCode: language) ?? language + descriptionLanguageNote = L10n.format("Description in %@.", [name]) + } else { descriptionLanguageNote = nil } + genres = GameGenre.list(metadata?.genres ?? []) released = Self.released(metadata?.releaseDate, locale: locale) developerLine = Self.credit(metadata) let week = Activity.summary(sessions: sessions.filter { $0.gameID == game.id }, now: now, calendar: calendar) @@ -53,7 +57,7 @@ public struct GameDetailPresentation: Equatable { playtime = LibraryPresentation.duration(totalPlaytime) self.lastPlayed = LibraryPresentation.relative(lastPlayed, now: now) version = game.versionString ?? "—" - size = ByteCountFormatter.string(fromByteCount: game.sizeBytes, countStyle: .file) + size = game.sizeBytes.formatted(.byteCount(style: .file).locale(locale)) engineCommitShort = game.toolchain.engineCommit.map { String($0.prefix(7)) } ?? "—" toolchainVerified = game.toolchain.verified toolchainNote = game.toolchain.note diff --git a/Sources/NXLibraryCore/Models/GameFileItem.swift b/Sources/NXLibraryCore/Models/GameFileItem.swift index f1c2064..f73c4ba 100644 --- a/Sources/NXLibraryCore/Models/GameFileItem.swift +++ b/Sources/NXLibraryCore/Models/GameFileItem.swift @@ -11,10 +11,10 @@ public enum SourceKind: String, Codable, Equatable { public var title: String { switch self { - case .base: return "Base Game" - case .update: return "Update" + case .base: return L10n.text("Base Game") + case .update: return L10n.text("Game update") case .dlc: return "DLC" - case .unknown: return "Switch File" + case .unknown: return L10n.text("Switch File") } } @@ -70,6 +70,6 @@ public struct GameFileItem: Identifiable, Codable, Equatable { public var kind: SourceKind { type } public var formattedSize: String { - ByteCountFormatter.string(fromByteCount: sizeBytes, countStyle: .file) + sizeBytes.formatted(.byteCount(style: .file).locale(L10n.context.formatLocale)) } } diff --git a/Sources/NXLibraryCore/Models/GameFoldersCopy.swift b/Sources/NXLibraryCore/Models/GameFoldersCopy.swift index 2657f9b..5e32c8b 100644 --- a/Sources/NXLibraryCore/Models/GameFoldersCopy.swift +++ b/Sources/NXLibraryCore/Models/GameFoldersCopy.swift @@ -1,20 +1,26 @@ /// Shared folder wording for Setup, Settings and their pickers. public enum GameFoldersCopy { public static let appsTitle = "Game apps" - public static let appsAbout = "Choose where to keep your game apps." - public static let appsContents = "Existing games and new builds." - public static let appsPickerMessage = "Choose where to keep your game apps. Put any existing nxnative games (.app) here. Change it later in Settings → Library." + public static var appsAbout: String { + L10n.text("Choose where to keep your game apps.") + } + public static var appsContents: String { + L10n.text("Existing games and new builds.") + } + public static var appsPickerMessage: String { + L10n.text("Choose where to keep your game apps. Put any existing nxnative games (.app) here. Change it later in Settings → Library.") + } public static let sourcesTitle = "Game files" public static func sourceFormats(extractedFolders: Bool) -> String { - "NSP, XCI, NSZ and XCZ files" + (extractedFolders ? ", or Switch 2 NUP files and extracted folders" : "") + L10n.text("NSP, XCI, NSZ and XCZ files") + (extractedFolders ? L10n.text(", or Switch 2 NUP files and extracted folders") : "") } public static func sourcesAbout(extractedFolders: Bool) -> String { - "Optional: \(sourceFormats(extractedFolders: extractedFolders)) for building." + L10n.format("Optional: %@ for building.", [String(describing: (sourceFormats(extractedFolders: extractedFolders)))]) } public static func sourcesPickerMessage(extractedFolders: Bool) -> String { - "Choose a folder with your \(sourceFormats(extractedFolders: extractedFolders)). Existing apps play without these files. Change it later in Settings → Library." + L10n.format("Choose a folder with your %@. Existing apps play without these files. Change it later in Settings → Library.", [String(describing: (sourceFormats(extractedFolders: extractedFolders)))]) } } diff --git a/Sources/NXLibraryCore/Models/GameGenre.swift b/Sources/NXLibraryCore/Models/GameGenre.swift new file mode 100644 index 0000000..5a9aec7 --- /dev/null +++ b/Sources/NXLibraryCore/Models/GameGenre.swift @@ -0,0 +1,47 @@ +import Foundation + +/// Translate known catalog genres at presentation time. Raw metadata and personal choices stay untouched. +public enum GameGenre { + public static func title(_ raw: String) -> String { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + if L10n.context.language == .english { return trimmed } + let normalized = trimmed.lowercased().replacingOccurrences(of: "-", with: " ") + let known: String + switch normalized { + case "action": known = "Action" + case "adventure": known = "Adventure" + case "action adventure": known = "Action-adventure" + case "role playing", "roleplaying", "rpg": known = "Role-playing" + case "puzzle": known = "Puzzle" + case "platformer", "platform", "platforming": known = "Platformer" + case "racing": known = "Racing" + case "sports", "sport": known = "Sports" + case "simulation", "simulator": known = "Simulation" + case "strategy": known = "Strategy" + case "fighting", "fighter": known = "Fighting" + case "shooter", "shooting": known = "Shooter" + case "arcade": known = "Arcade" + case "party": known = "Party" + case "music", "rhythm", "music/rhythm": known = "Music" + case "board game", "board games", "board": known = "Board game" + case "card game", "card games": known = "Card game" + case "education", "educational", "study": known = "Education" + case "fitness", "training": known = "Fitness" + case "communication": known = "Communication" + case "lifestyle", "practical": known = "Lifestyle" + case "utility", "utilities": known = "Utility" + default: return trimmed + } + return L10n.text("Game genre: " + known) + } + + public static func list(_ raw: [String]) -> [String] { + var seen = Set<String>() + return raw.compactMap { value in + let value = value.trimmingCharacters(in: .whitespacesAndNewlines) + guard !value.isEmpty, value.caseInsensitiveCompare("Other") != .orderedSame else { return nil } + let localized = title(value) + return seen.insert(localized.lowercased()).inserted ? localized : nil + } + } +} diff --git a/Sources/NXLibraryCore/Models/GameOptionsPresentation.swift b/Sources/NXLibraryCore/Models/GameOptionsPresentation.swift index 1323262..8cf8ba6 100644 --- a/Sources/NXLibraryCore/Models/GameOptionsPresentation.swift +++ b/Sources/NXLibraryCore/Models/GameOptionsPresentation.swift @@ -23,36 +23,36 @@ public enum GameOptionsRow: String, CaseIterable, Sendable { public var title: String { switch self { - case .play: return "Play" - case .move: return "Move" - case .favorite: return "Favorite" - case .hidden: return "Hidden" - case .fullscreen: return "Fullscreen" - case .fps60: return "60 FPS" - case .frameRate: return "Frame rate" - case .gameUpscaler: return "Game's upscaler" - case .moreSettings: return "More settings" - case .metalFX: return "MetalFX upscaling" - case .metalHUD: return "Metal HUD" - case .consoleMode: return "Console mode" - case .language: return "Game language" - case .rumble: return "Rumble" - case .rumbleStrength: return "Rumble strength" - case .renderScale: return "3D render scale" - case .restoreDefaults: return "Use new game defaults" - case .cheats: return "Cheats" - case .mods: return "Mods" - case .rebuild: return "Rebuild" - case .showInFinder: return "Show in Finder" - case .openSaves: return "Saves" - case .editDetails: return "Edit details" - case .removeGame: return "Remove game" - case .changeArtwork: return "Change artwork" - case .changeMetadata: return "Change metadata" - case .storePage: return "Store page" - case .attachSources: return "Attach sources…" - case .openLog: return "Open log" - case .removeSources: return "Remove sources & build cache" + case .play: return L10n.text("Play") + case .move: return L10n.text("Move") + case .favorite: return L10n.text("Favorite") + case .hidden: return L10n.text("Hidden") + case .fullscreen: return L10n.text("Fullscreen") + case .fps60: return L10n.text("60 FPS") + case .frameRate: return L10n.text("Frame rate") + case .gameUpscaler: return L10n.text("Game's upscaler") + case .moreSettings: return L10n.text("More settings") + case .metalFX: return L10n.text("MetalFX upscaling") + case .metalHUD: return L10n.text("Metal HUD") + case .consoleMode: return L10n.text("Console mode") + case .language: return L10n.text("Game language") + case .rumble: return L10n.text("Rumble") + case .rumbleStrength: return L10n.text("Rumble strength") + case .renderScale: return L10n.text("3D render scale") + case .restoreDefaults: return L10n.text("Use new game defaults") + case .cheats: return L10n.text("Cheats") + case .mods: return L10n.text("Mods") + case .rebuild: return L10n.text("Rebuild") + case .showInFinder: return L10n.text("Show in Finder") + case .openSaves: return L10n.text("Saves") + case .editDetails: return L10n.text("Edit details") + case .removeGame: return L10n.text("Remove game") + case .changeArtwork: return L10n.text("Change artwork") + case .changeMetadata: return L10n.text("Change metadata") + case .storePage: return L10n.text("Store page") + case .attachSources: return L10n.text("Attach sources…") + case .openLog: return L10n.text("Open log") + case .removeSources: return L10n.text("Remove sources & build cache") } } @@ -193,7 +193,7 @@ public struct GameOptionsPresentation: Equatable { self.gameName = gameName self.appPath = appPath self.canDeleteBundle = canDeleteBundle - self.resolutionNote = "Output follows the window/display. Render scale changes the internal 3D resolution only; native 1440p/4K is not available in this engine pin." + self.resolutionNote = L10n.text("Output follows the window/display. Render scale changes the internal 3D resolution only; native 1440p/4K is not available in this engine pin.") } // MARK: - Derived copy and state @@ -204,28 +204,28 @@ public struct GameOptionsPresentation: Equatable { /// Says where to fix a dimmed Rebuild: the sources or the Metal gate. public var rebuildAbout: String { - if sourcesDetached { return "Game files are missing or not attached. Attach sources in Edit details." } - if toolchainBlocked { return toolchainHint.map { $0 + "." } ?? "Blocked by the Metal gate. See Settings → Engine." } - return "Build a new app; the old app stays if the build fails." + if sourcesDetached { return L10n.text("Game files are missing or not attached. Attach sources in Edit details.") } + if toolchainBlocked { return toolchainHint.map { $0 + "." } ?? L10n.text("Blocked by the Metal gate. See Settings → Engine.") } + return L10n.text("Build a new app; the old app stays if the build fails.") } public var attachAbout: String { sourcesDetached - ? "Game files are missing or not attached. Attach them to rebuild." - : "Sources attached. Rebuild without touching saves." + ? L10n.text("Game files are missing or not attached. Attach them to rebuild.") + : L10n.text("Sources attached. Rebuild without touching saves.") } public var removal: GameRemovalCopy { if canDeleteBundle { return GameRemovalCopy( - title: "Delete \(gameName)?", + title: L10n.format("Delete %@?", [String(describing: (gameName))]), confirmTitle: "Delete", - message: "This deletes \(appPath). Sources, saves and the build cache are not deleted.") + message: L10n.format("This deletes %@. Sources, saves and the build cache are not deleted.", [String(describing: (appPath))])) } return GameRemovalCopy( - title: "Remove \(gameName)?", + title: L10n.format("Remove %@?", [String(describing: (gameName))]), confirmTitle: "Remove", - message: "This removes the library record only; \(appPath) stays on disk.") + message: L10n.format("This removes the library record only; %@ stays on disk.", [String(describing: (appPath))])) } /// A row that looks disabled. It still takes focus, and its action explains why it cannot run. @@ -246,37 +246,37 @@ public struct GameOptionsPresentation: Equatable { /// The row's one-line description, when it has one. public func about(_ row: GameOptionsRow) -> String? { - if isFixed(row) { return "Set in env.txt" } + if isFixed(row) { return L10n.text("Set in env.txt") } switch row { - case .play: return "Launch the installed Mac app." - case .cheats: return "Install and switch this game's cheats." - case .mods: return "Turn this game's file mods on and off, and choose which one wins." - case .move: return "Change its place on Spotlight." - case .favorite: return "On your Favorites tab." - case .hidden: return "Hidden games stay out of your lists." - case .fullscreen: return "Launch mode" - case .fps60, .frameRate: return "Frame-rate target" - case .gameUpscaler: return "The game's own 3D upscaler, or MetalFX instead" - case .moreSettings: return "Other settings this build offers." - case .metalFX: return "Sharper scaling of the whole picture to your screen" - case .metalHUD: return "Performance overlay" - case .consoleMode: return "Console profile" - case .language: return "Game language" - case .rumble: return "Controller vibration" - case .rumbleStrength: return "Vibration strength" - case .renderScale: return "Internal resolution" - case .restoreDefaults: return "Copy current defaults" + case .play: return L10n.text("Launch the installed Mac app.") + case .cheats: return L10n.text("Install and switch this game's cheats.") + case .mods: return L10n.text("Turn this game's file mods on and off, and choose which one wins.") + case .move: return L10n.text("Change its place on Spotlight.") + case .favorite: return L10n.text("On your Favorites tab.") + case .hidden: return L10n.text("Hidden games stay out of your lists.") + case .fullscreen: return L10n.text("Launch mode") + case .fps60, .frameRate: return L10n.text("Frame-rate target") + case .gameUpscaler: return L10n.text("The game's own 3D upscaler, or MetalFX instead") + case .moreSettings: return L10n.text("Other settings this build offers.") + case .metalFX: return L10n.text("Sharper scaling of the whole picture to your screen") + case .metalHUD: return L10n.text("Performance overlay") + case .consoleMode: return L10n.text("Console profile") + case .language: return L10n.text("Game language") + case .rumble: return L10n.text("Controller vibration") + case .rumbleStrength: return L10n.text("Vibration strength") + case .renderScale: return L10n.text("Internal resolution") + case .restoreDefaults: return L10n.text("Copy current defaults") case .rebuild: return rebuildAbout - case .showInFinder: return "Reveal the app in Finder." - case .openSaves: return "Back up, import and restore progress." - case .editDetails: return "Artwork, metadata, store page, sources and the build log." + case .showInFinder: return L10n.text("Reveal the app in Finder.") + case .openSaves: return L10n.text("Back up, import and restore progress.") + case .editDetails: return L10n.text("Artwork, metadata, store page, sources and the build log.") case .removeGame: return removal.message - case .changeArtwork: return "Cover, background, icon and logo, from GameTDB, Title DB or a file." - case .changeMetadata: return "Description, genres, developer, publisher and release date." - case .storePage: return "Open the game's Nintendo store page." + case .changeArtwork: return L10n.text("Cover, background, icon and logo, from GameTDB, Title DB or a file.") + case .changeMetadata: return L10n.text("Description, genres, developer, publisher and release date.") + case .storePage: return L10n.text("Open the game's Nintendo store page.") case .attachSources: return attachAbout - case .openLog: return "Reveal the latest build log." - case .removeSources: return "Deletes only the source files named in this game and its app-owned workspace. The app and saves stay." + case .openLog: return L10n.text("Reveal the latest build log.") + case .removeSources: return L10n.text("Deletes only the source files named in this game and its app-owned workspace. The app and saves stay.") } } @@ -284,21 +284,21 @@ public struct GameOptionsPresentation: Equatable { public func info(_ row: GameOptionsRow) -> String? { switch row { case .fps60: - return "On targets 60 FPS; Off targets 30 FPS. Some games run too fast at 60 FPS." + return L10n.text("On targets 60 FPS; Off targets 30 FPS. Some games run too fast at 60 FPS.") case .metalFX: - return "Enlarges the game's finished picture, menus and HUD included, to your window or screen with Apple's MetalFX instead of smooth scaling: sharper edges and text, for a little more GPU time. Nothing changes when the picture is already as large as the screen. Not the same as Game's upscaler. (DM_PRESENT_UPSCALE)" + return L10n.text("Enlarges the game's finished picture, menus and HUD included, to your window or screen with Apple's MetalFX instead of smooth scaling: sharper edges and text, for a little more GPU time. Nothing changes when the picture is already as large as the screen. Not the same as Game's upscaler. (DM_PRESENT_UPSCALE)") case .gameUpscaler: - return "Some Switch 2 games, such as Donkey Kong Bananza, enlarge their 3D picture inside the frame with an upscaler of their own before drawing the HUD. Game's own keeps it; MetalFX does the same step with Apple's scaler in a fraction of the GPU time, with Apple's look instead of the game's. MetalFX upscaling is a later step: it enlarges the finished picture to your screen. Both can be on. (DM_UPSCALER)" + return L10n.text("Some Switch 2 games, such as Donkey Kong Bananza, enlarge their 3D picture inside the frame with an upscaler of their own before drawing the HUD. Game's own keeps it; MetalFX does the same step with Apple's scaler in a fraction of the GPU time, with Apple's look instead of the game's. MetalFX upscaling is a later step: it enlarges the finished picture to your screen. Both can be on. (DM_UPSCALER)") case .metalHUD: - return "Displays Apple's real-time Metal developer HUD (DM_METAL_HUD=1). Shows frame rate, frame pacing graphs, GPU time per frame in milliseconds, and memory allocation." + return L10n.text("Displays Apple's real-time Metal developer HUD (DM_METAL_HUD=1). Shows frame rate, frame pacing graphs, GPU time per frame in milliseconds, and memory allocation.") case .consoleMode: - return "Docked uses the game's TV profile; Handheld uses its portable profile." + return L10n.text("Docked uses the game's TV profile; Handheld uses its portable profile.") case .renderScale: - return "Scales internal 3D scene resolution from 50% to 100% (DM_RENDER_SCALE). Decreasing internal resolution boosts framerates on demanding titles while UI overlays remain crisp." + return L10n.text("Scales internal 3D scene resolution from 50% to 100% (DM_RENDER_SCALE). Decreasing internal resolution boosts framerates on demanding titles while UI overlays remain crisp.") case .restoreDefaults: - return "Copies your defaults for new games once. Later changes to those defaults leave this game unchanged." + return L10n.text("Copies your defaults for new games once. Later changes to those defaults leave this game unchanged.") case .removeSources: - return "Frees disk space by deleting original NSP/XCI dump files and the compilation cache workspace. The installed game app and all your save files are completely safe." + return L10n.text("Frees disk space by deleting original NSP/XCI dump files and the compilation cache workspace. The installed game app and all your save files are completely safe.") default: return nil } @@ -316,7 +316,7 @@ public struct GameOptionsPresentation: Equatable { .filter { $0.settingKey.map(settings.visible) ?? false } .map(GameOptionsEntry.row) if !settings.isOffered("DM_RENDER_SCALE") { - entries.append(.note(title: "Internal resolution", text: resolutionNote)) + entries.append(.note(title: L10n.text("Internal resolution"), text: resolutionNote)) } if !settings.extraSettings.isEmpty { entries.append(.row(.moreSettings)) } } else { @@ -325,7 +325,7 @@ public struct GameOptionsPresentation: Equatable { if showRenderScale { entries.append(.row(.renderScale)) } else { - entries.append(.note(title: "Internal resolution", text: resolutionNote)) + entries.append(.note(title: L10n.text("Internal resolution"), text: resolutionNote)) } } entries += [.row(.restoreDefaults), .row(.rebuild), .row(.showInFinder), @@ -436,7 +436,7 @@ public enum GameSettingValues { /// A choice's option label, On / Off for a toggle, a percentage for a range (as the engine's window shows it). public static func label(_ setting: LauncherSetting, _ value: String) -> String { switch setting.kind { - case "toggle": return value == setting.on ? "On" : "Off" + case "toggle": return value == setting.on ? L10n.text("On") : L10n.text("Off") case "range": guard let x = LauncherSetting.number(value) else { return value } return "\(Int((x * 100).rounded()))%" diff --git a/Sources/NXLibraryCore/Models/GameResourceState.swift b/Sources/NXLibraryCore/Models/GameResourceState.swift index 43e7ff8..0039950 100644 --- a/Sources/NXLibraryCore/Models/GameResourceState.swift +++ b/Sources/NXLibraryCore/Models/GameResourceState.swift @@ -6,15 +6,15 @@ public enum GameUnavailableReason: Equatable { public var title: String { switch self { - case .appMissing: return "App not found" - case .sourcesMissing: return "Game files not found" + case .appMissing: return L10n.text("App not found") + case .sourcesMissing: return L10n.text("Game files not found") } } public var detail: String { switch self { - case .appMissing: return "It may have been moved or removed." - case .sourcesMissing: return "They may have been moved or removed." + case .appMissing: return L10n.text("It may have been moved or removed.") + case .sourcesMissing: return L10n.text("They may have been moved or removed.") } } } diff --git a/Sources/NXLibraryCore/Models/GameStorageUsage.swift b/Sources/NXLibraryCore/Models/GameStorageUsage.swift new file mode 100644 index 0000000..ce600dc --- /dev/null +++ b/Sources/NXLibraryCore/Models/GameStorageUsage.swift @@ -0,0 +1,36 @@ +import Foundation + +/// Missing files occupy no space; an unreadable folder or disconnected drive has an unknown size. +public enum StorageSize: Equatable { + case bytes(Int64), missing, unavailable + + public var knownBytes: Int64? { + switch self { + case .bytes(let bytes): return bytes + case .missing: return 0 + case .unavailable: return nil + } + } +} + +public struct GameStorageUsage: Identifiable, Equatable { + public let id: UUID + public let name: String + public let app: StorageSize + public let cache: StorageSize + public let logs: StorageSize + public let canDeleteCache: Bool + public let sharedCacheWith: [String] + + public var rowID: String { "storage-game-\(id.uuidString)" } +} + +public struct StorageSnapshot: Equatable { + public let games: [GameStorageUsage] + public let total: StorageUsage + public let freeBytes: Int64? + + public var hasUnavailableFiles: Bool { + games.contains { $0.app == .unavailable || $0.cache == .unavailable || $0.logs == .unavailable } + } +} diff --git a/Sources/NXLibraryCore/Models/LibraryPresentation.swift b/Sources/NXLibraryCore/Models/LibraryPresentation.swift index e5be9f0..02fd523 100644 --- a/Sources/NXLibraryCore/Models/LibraryPresentation.swift +++ b/Sources/NXLibraryCore/Models/LibraryPresentation.swift @@ -18,7 +18,7 @@ public struct LibraryPresentation: Equatable { totalPlaytime: TimeInterval, lastPlayed: Date?, now: Date = Date()) { title = game.name eyebrow = metadata.map { Self.eyebrow(from: $0.name) } ?? "" - genres = Array((metadata?.genres ?? []).prefix(3)) + genres = Array(GameGenre.list(metadata?.genres ?? []).prefix(3)) publisher = metadata?.publishers.first releaseYear = metadata?.releaseYear playtime = Self.duration(totalPlaytime) @@ -50,17 +50,17 @@ public struct LibraryPresentation: Equatable { let totalMinutes = Int(interval / 60) let hours = totalMinutes / 60 let minutes = totalMinutes % 60 - if hours == 0 { return "\(minutes)m" } - return "\(hours)h \(minutes)m" + if hours == 0 { return L10n.format("%@m", [minutes.formatted(.number.locale(L10n.context.formatLocale))]) } + return L10n.format("%@h %@m", [hours.formatted(.number.locale(L10n.context.formatLocale)), minutes.formatted(.number.locale(L10n.context.formatLocale))]) } static func relative(_ date: Date?, now: Date) -> String { - guard let date else { return "Never" } + guard let date else { return L10n.text("Never") } let seconds = max(0, now.timeIntervalSince(date)) - if seconds < 60 { return "Just now" } - if seconds < 3_600 { return "\(Int(seconds / 60))m ago" } - if seconds < 86_400 { return "\(Int(seconds / 3_600))h ago" } - if seconds < 604_800 { return "\(Int(seconds / 86_400))d ago" } - return date.formatted(date: .abbreviated, time: .omitted) + if seconds < 60 { return L10n.text("Just now") } + if seconds < 3_600 { return L10n.format("%@m ago", [String(describing: (Int(seconds / 60)))]) } + if seconds < 86_400 { return L10n.format("%@h ago", [String(describing: (Int(seconds / 3_600)))]) } + if seconds < 604_800 { return L10n.format("%@d ago", [String(describing: (Int(seconds / 86_400)))]) } + return date.formatted(.dateTime.day().month(.abbreviated).year().locale(L10n.context.formatLocale)) } } diff --git a/Sources/NXLibraryCore/Models/MenuSelectionModel.swift b/Sources/NXLibraryCore/Models/MenuSelectionModel.swift index d8356f1..c4503df 100644 --- a/Sources/NXLibraryCore/Models/MenuSelectionModel.swift +++ b/Sources/NXLibraryCore/Models/MenuSelectionModel.swift @@ -7,13 +7,13 @@ public enum MenuAction: String, CaseIterable, Identifiable, Equatable { public var title: String { switch self { - case .addGame: return "Add Game" - case .refreshLibrary: return "Library refresh" - case .settings: return "Settings" + case .addGame: return L10n.text("Add Game") + case .refreshLibrary: return L10n.text("Library refresh") + case .settings: return L10n.text("Settings") case .theme: return "Theme" case .console: return "Console" - case .setupCheck: return "Setup check" - case .power: return "Power" + case .setupCheck: return L10n.text("Setup check") + case .power: return L10n.text("Power") } } @@ -37,9 +37,9 @@ public enum PowerAction: String, CaseIterable, Identifiable, Equatable { public var title: String { switch self { - case .quit: return "Quit" - case .hide: return "Hide" - case .relaunch: return "Restart app" + case .quit: return L10n.text("Quit") + case .hide: return L10n.text("Hide") + case .relaunch: return L10n.text("Restart app") } } diff --git a/Sources/NXLibraryCore/Models/OnlineBackgrounds.swift b/Sources/NXLibraryCore/Models/OnlineBackgrounds.swift index c23bd10..248a59f 100644 --- a/Sources/NXLibraryCore/Models/OnlineBackgrounds.swift +++ b/Sources/NXLibraryCore/Models/OnlineBackgrounds.swift @@ -38,6 +38,7 @@ public struct BackgroundCategory: Equatable, Hashable, Sendable { public static let searchID = "search" public let id: String public let title: String + public var displayTitle: String { L10n.text(title) } public var isSearch: Bool { id == Self.searchID } public init(id: String, title: String) { diff --git a/Sources/NXLibraryCore/Models/PendingInstall.swift b/Sources/NXLibraryCore/Models/PendingInstall.swift index 62bbc94..270f2a6 100644 --- a/Sources/NXLibraryCore/Models/PendingInstall.swift +++ b/Sources/NXLibraryCore/Models/PendingInstall.swift @@ -20,9 +20,11 @@ public struct PendingInstall: Codable, Equatable { /// The porting screen's stage line, title and message while the new app waits. public static let stageName = "Ready: installs when you quit the game" - public static let title = "Ready when you quit" + public static var title: String { + L10n.text("Ready when you quit") + } public static func message(gameName: String) -> String { - "\(gameName) is open, so the new app waits beside it and replaces it as soon as you quit the game." + L10n.format("%@ is open, so the new app waits beside it and replaces it as soon as you quit the game.", [String(describing: (gameName))]) } } diff --git a/Sources/NXLibraryCore/Models/PortingFailureReason.swift b/Sources/NXLibraryCore/Models/PortingFailureReason.swift index f4a82ab..a537f97 100644 --- a/Sources/NXLibraryCore/Models/PortingFailureReason.swift +++ b/Sources/NXLibraryCore/Models/PortingFailureReason.swift @@ -47,88 +47,88 @@ public struct PortingFailureReason: Codable, Equatable, Sendable { public var title: String { switch kind { - case .rosetta: return "The build ran as Intel code" - case .translator: return "The shader translator did not run" - case .zbic: return "A Python package is missing" - case .cltSelected: return "Xcode is not selected" - case .otherMetal: return "Your Metal compiler is not certified" - case .shaderRegression: return "Some shaders broke in this engine" - case .damagedNSP: return "Some game files are damaged" - case .a32Program: return "This game is not supported" - case .keys: return isMissingTicket ? "Game ticket missing" : "Your keys could not be read" - case .keysTooOld: return "Your keys are too old" - case .keysWrong: return "These keys do not open the game" - case .severalGames: return "The folder holds more than one game" - case .noTitleID: return "No game found in the files" - case .diskFull: return "Not enough disk space" - case .driveMissing: return "A drive is not connected" - case .cloudFolder: return "The library is in a cloud folder" - case .noProfile: return "This game is not supported yet" - case .unknown: return missingLanguage == nil ? "The porting stopped" : "The game does not have this language" + case .rosetta: return L10n.text("The build ran as Intel code") + case .translator: return L10n.text("The shader translator did not run") + case .zbic: return L10n.text("A Python package is missing") + case .cltSelected: return L10n.text("Xcode is not selected") + case .otherMetal: return L10n.text("Your Metal compiler is not certified") + case .shaderRegression: return L10n.text("Some shaders broke in this engine") + case .damagedNSP: return L10n.text("Some game files are damaged") + case .a32Program: return L10n.text("This game is not supported") + case .keys: return isMissingTicket ? L10n.text("Game ticket missing") : L10n.text("Your keys could not be read") + case .keysTooOld: return L10n.text("Your keys are too old") + case .keysWrong: return L10n.text("These keys do not open the game") + case .severalGames: return L10n.text("The folder holds more than one game") + case .noTitleID: return L10n.text("No game found in the files") + case .diskFull: return L10n.text("Not enough disk space") + case .driveMissing: return L10n.text("A drive is not connected") + case .cloudFolder: return L10n.text("The library is in a cloud folder") + case .noProfile: return L10n.text("This game is not supported yet") + case .unknown: return missingLanguage == nil ? L10n.text("The porting stopped") : L10n.text("The game does not have this language") } } public func what(game: String) -> String { switch kind { - case .rosetta: return "The porting started an Intel Python, so its tools ran under Rosetta." - case .translator: return "The shader translator needs the .NET 10 runtime for this Mac." - case .zbic: return "The engine needs the zbic package to read this game's code." - case .cltSelected: return "The selected developer folder is the Command Line Tools, which cannot build the app." - case .otherMetal: return "The shaders were tested with another Metal compiler than the one on this Mac." - case .shaderRegression: return "Shaders that translated before fail with the engine's current translator." + case .rosetta: return L10n.text("The porting started an Intel Python, so its tools ran under Rosetta.") + case .translator: return L10n.text("The engine's shader translator is missing or damaged.") + case .zbic: return L10n.text("The engine needs the zbic package to read this game's code.") + case .cltSelected: return L10n.text("The selected developer folder is the Command Line Tools, which cannot build the app.") + case .otherMetal: return L10n.text("The shaders were tested with another Metal compiler than the one on this Mac.") + case .shaderRegression: return L10n.text("Shaders that translated before fail with the engine's current translator.") case .damagedNSP: return isUnreadableDamage - ? "\(game)'s package is cut off or its own tables fail their hash check: an incomplete copy or a broken download." - : "Some files of \(game) fail their hash check: an incomplete copy or a broken download." - case .a32Program: return "\(game) is a 32-bit ARM program, and nxnative runs only 64-bit games." + ? L10n.format("%@'s package is cut off or its own tables fail their hash check: an incomplete copy or a broken download.", [String(describing: (game))]) + : L10n.format("Some files of %@ fail their hash check: an incomplete copy or a broken download.", [String(describing: (game))]) + case .a32Program: return L10n.format("%@ is a 32-bit ARM program, and nxnative runs only 64-bit games.", [String(describing: (game))]) case .keys: return isMissingTicket - ? "The package has no common ticket needed to open \(game)." - : "prod.keys is missing, unreadable or has no header_key." - case .keysTooOld: return "\(game) needs keys newer than the ones you imported." - case .keysWrong: return "header_key does not open \(detail ?? "the game's files")." - case .severalGames: return "Each porting needs one game; this folder holds files of several." - case .noTitleID: return "None of the files holds a game the engine can read." + ? L10n.format("The package has no common ticket needed to open %@.", [String(describing: (game))]) + : L10n.text("prod.keys is missing, unreadable or has no header_key.") + case .keysTooOld: return L10n.format("%@ needs keys newer than the ones you imported.", [String(describing: (game))]) + case .keysWrong: return L10n.format("header_key does not open %@.", [String(describing: (detail ?? "the game's files"))]) + case .severalGames: return L10n.text("Each porting needs one game; this folder holds files of several.") + case .noTitleID: return L10n.text("None of the files holds a game the engine can read.") case .diskFull: - if let gb = detail { return "The porting needs at least \(gb) GB free on the work volume." } - return "The disk filled up during the porting." - case .driveMissing: return "\(game)'s files are on \(detail ?? "a drive") that is not connected." - case .cloudFolder: return "Files in iCloud Drive or another synced folder carry data that code signing refuses." + if let gb = detail { return L10n.format("The porting needs at least %@ GB free on the work volume.", [String(describing: (gb))]) } + return L10n.text("The disk filled up during the porting.") + case .driveMissing: return L10n.format("%@'s files are on %@ that is not connected.", [String(describing: (game)), String(describing: (detail ?? L10n.text("a drive")))]) + case .cloudFolder: return L10n.text("Files in iCloud Drive or another synced folder carry data that code signing refuses.") case .noProfile: - let at = detail.map { " (rewrite stopped at 0x\($0))" } ?? "" - return "\(game) has no verified nxnative game profile\(at); the pinned engine does not support it yet." + let at = detail.map { L10n.format(" (rewrite stopped at 0x%@)", [String(describing: ($0))]) } ?? "" + return L10n.format("%@ has no verified nxnative game profile%@; the pinned engine does not support it yet.", [String(describing: (game)), String(describing: (at))]) case .unknown: - if let language = missingLanguage { return "\(game) has no \(language), the language set in its Game options." } - return "The engine stopped without a message NX Library recognises." + if let language = missingLanguage { return L10n.format("%@ has no %@, the language set in its Game options.", [String(describing: (game)), String(describing: (language))]) } + return L10n.text("The engine stopped without a message NX Library recognises.") } } public var next: String { switch kind { - case .rosetta: return "Reinstall NX Library so the porting uses its own arm64 Python." - case .translator: return "Open Setup and install the .NET 10 SDK." - case .zbic: return "Update NX Library: its built-in Python includes zbic from 0.1.3.2. Installing zbic with pip does not reach it." - case .cltSelected: return "Open Setup and select Xcode." - case .otherMetal: return "Install the certified compiler from Setup, or allow untested toolchains in Settings → Engine." - case .shaderRegression: return "Port anyway to keep the broken shaders out, or wait for an engine update." + case .rosetta: return L10n.text("Reinstall NX Library so the porting uses its own arm64 Python.") + case .translator: return L10n.text("Open Setup and check the nxnative engine card.") + case .zbic: return L10n.text("Update NX Library: its built-in Python includes zbic from 0.1.3.2. Installing zbic with pip does not reach it.") + case .cltSelected: return L10n.text("Open Setup and select Xcode.") + case .otherMetal: return L10n.text("Install the certified compiler from Setup, or allow untested toolchains in Settings → Engine.") + case .shaderRegression: return L10n.text("Port anyway to keep the broken shaders out, or wait for an engine update.") case .damagedNSP: return isUnreadableDamage - ? "Dump or download the game again." - : "Dump or download the game again, or port anyway with the damaged files as read." - case .a32Program: return "Nothing to fix here: this title cannot run." + ? L10n.text("Dump or download the game again.") + : L10n.text("Dump or download the game again, or port anyway with the damaged files as read.") + case .a32Program: return L10n.text("Nothing to fix here: this title cannot run.") case .keys: return isMissingTicket - ? "Export the complete game package again from your console, including its ticket." - : "Import prod.keys from your console again." + ? L10n.text("Export the complete game package again from your console, including its ticket.") + : L10n.text("Import prod.keys from your console again.") case .keysTooOld: - if let firmware = detail { return "Dump prod.keys again from a console on firmware \(firmware) or later." } - return "Dump prod.keys again from an up-to-date console." - case .keysWrong: return "Import prod.keys from the console these files were dumped with." - case .severalGames: return "Choose a folder with one game's base, update and DLC." - case .noTitleID: return "Check that the files are complete NSP, NSZ or XCI dumps." - case .diskFull: return "Free some space, then try again." - case .driveMissing: return "Connect \(detail ?? "the drive"), then try again." - case .cloudFolder: return "Move the library to a folder outside iCloud Drive." - case .noProfile: return "Watch for an engine update that adds this game." + if let firmware = detail { return L10n.format("Dump prod.keys again from a console on firmware %@ or later.", [String(describing: (firmware))]) } + return L10n.text("Dump prod.keys again from an up-to-date console.") + case .keysWrong: return L10n.text("Import prod.keys from the console these files were dumped with.") + case .severalGames: return L10n.text("Choose a folder with one game's base, update and DLC.") + case .noTitleID: return L10n.text("Check that the files are complete NSP, NSZ or XCI dumps.") + case .diskFull: return L10n.text("Free some space, then try again.") + case .driveMissing: return L10n.format("Connect %@, then try again.", [String(describing: (detail ?? L10n.text("the drive")))]) + case .cloudFolder: return L10n.text("Move the library to a folder outside iCloud Drive.") + case .noProfile: return L10n.text("Watch for an engine update that adds this game.") case .unknown: - if missingLanguage != nil { return "In Game options, set Language to Game Default or to one of the game's languages, then port again." } - return "Open the log, or copy diagnostics for a bug report." + if missingLanguage != nil { return L10n.text("In Game options, set Language to Game Default or to one of the game's languages, then port again.") } + return L10n.text("Open the log, or copy diagnostics for a bug report.") } } diff --git a/Sources/NXLibraryCore/Models/PortingPresentation.swift b/Sources/NXLibraryCore/Models/PortingPresentation.swift index ebd7ec2..bc57d25 100644 --- a/Sources/NXLibraryCore/Models/PortingPresentation.swift +++ b/Sources/NXLibraryCore/Models/PortingPresentation.swift @@ -49,9 +49,9 @@ public enum PortingETA { let total = minutes(seconds) let hours = total / 60 let mins = total % 60 - if hours == 0 { return "\(mins) min" } - if mins == 0 { return "\(hours) hr" } - return "\(hours) hr \(mins) min" + if hours == 0 { return L10n.format("%@ min", [String(describing: (mins))]) } + if mins == 0 { return L10n.format("%@ hr", [String(describing: (hours))]) } + return L10n.format("%@ hr %@ min", [String(describing: (hours)), String(describing: (mins))]) } } diff --git a/Sources/NXLibraryCore/Models/SaveLocation.swift b/Sources/NXLibraryCore/Models/SaveLocation.swift index 6cea2bc..346c008 100644 --- a/Sources/NXLibraryCore/Models/SaveLocation.swift +++ b/Sources/NXLibraryCore/Models/SaveLocation.swift @@ -3,21 +3,22 @@ import Darwin public enum SaveFailure: LocalizedError, Equatable { case invalidTitleID, unsafePath, customSavePath, unreadableConfiguration - case cloudEnabled(String), running, busy, changed, emptyImport, invalidSnapshot, recoveryRequired + case cloudEnabled(String), running, busy, changed, emptyImport, invalidSnapshot, recoveryRequired, cloudPullPending case io(String) public var errorDescription: String? { switch self { - case .invalidTitleID: return "This game needs a valid title ID to manage saves." - case .unsafePath: return "This save location contains a link or an unsafe path. Manage it in Finder." - case .customSavePath: return "This game uses a custom save location. Manage it outside this panel." - case .unreadableConfiguration: return "The game's save configuration could not be read." - case .cloudEnabled(let origin): return "Cloud saves are enabled in \(origin). Turn them off there before importing or restoring." - case .running: return "Close the game before managing its saves. Keep it closed until this operation finishes." - case .busy: return "A save operation or app installation is in progress for this game." - case .changed: return "The files changed since the preview. Choose the folder again." - case .emptyImport: return "Choose a folder containing this game's actual save files." - case .invalidSnapshot: return "This backup is incomplete, changed, or belongs to another game." - case .recoveryRequired: return "Save recovery is required before Play. Your backup has been kept." + case .invalidTitleID: return L10n.text("This game needs a valid title ID to manage saves.") + case .unsafePath: return L10n.text("This save location contains a link or an unsafe path. Manage it in Finder.") + case .customSavePath: return L10n.text("This game uses a custom save location. Manage it outside this panel.") + case .unreadableConfiguration: return L10n.text("The game's save configuration could not be read.") + case .cloudEnabled(let origin): return L10n.format("Cloud saves are enabled in %@. Turn them off there before importing or restoring.", [String(describing: (origin))]) + case .running: return L10n.text("Close the game before managing its saves. Keep it closed until this operation finishes.") + case .busy: return L10n.text("A save operation or app installation is in progress for this game.") + case .changed: return L10n.text("The files changed since the preview. Choose the folder again.") + case .emptyImport: return L10n.text("Choose a folder containing this game's actual save files.") + case .invalidSnapshot: return L10n.text("This backup is incomplete, changed, or belongs to another game.") + case .recoveryRequired: return L10n.text("Save recovery is required before Play. Your backup has been kept.") + case .cloudPullPending: return L10n.text("A cloud save download did not finish. Play the game once so it can put this Mac's save back, then try again.") case .io(let message): return message } } @@ -35,6 +36,9 @@ public struct SaveLocation: Equatable { public let titleID: String public let root: URL public func directory(_ slot: SaveSlot) -> URL { root.appendingPathComponent(slot == .account ? "save" : "save_device") } + /// nxnative's journal of a cloud-save pull (dm/host/save_sync.c, `<DM_SAVE>.pull`, nxnative e6048dea): while it is + /// there, the game's next start puts this device's save back, so nothing may replace the save folders first. + public var cloudPullJournal: URL { root.appendingPathComponent("save.pull") } public static func validID(_ value: String) -> Bool { value.utf8.count == 16 && value.utf8.allSatisfy { (48...57).contains($0) || (65...70).contains($0) || (97...102).contains($0) } } diff --git a/Sources/NXLibraryCore/Models/SavePresentation.swift b/Sources/NXLibraryCore/Models/SavePresentation.swift index 259c0d2..597ad3e 100644 --- a/Sources/NXLibraryCore/Models/SavePresentation.swift +++ b/Sources/NXLibraryCore/Models/SavePresentation.swift @@ -7,26 +7,32 @@ public enum SavePresentation { guard available, !busy, !running, !recovery else { return .unavailable } return cloud ? .cloudSettings : .chooseFolder } - public static let closedNotice = "Keep the game closed, including launches from Finder, until the operation finishes." - public static let cloudNotice = "Backups capture the local committed save, which may differ from a save not yet received from the cloud." - public static let importWarning = "A save from a newer game version may not load. Cloud saves may replace this local save if you turn sync on later." - public static func slotName(_ slot: SaveSlot) -> String { slot == .account ? "User save" : "Device save" } - public static func size(_ bytes: Int64) -> String { ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file) } + public static var closedNotice: String { L10n.text("Keep the game closed, including launches from Finder, until the operation finishes.") } + public static var cloudNotice: String { L10n.text("Backups capture the local committed save, which may differ from a save not yet received from the cloud.") } + public static var importWarning: String { L10n.text("A save from a newer game version may not load. Cloud saves may replace this local save if you turn sync on later.") } + public static func slotName(_ slot: SaveSlot) -> String { slot == .account ? L10n.text("User save") : L10n.text("Device save") } + public static func size(_ bytes: Int64) -> String { bytes.formatted(.byteCount(style: .file).locale(L10n.context.formatLocale)) } public static func date(_ date: Date) -> String { - let formatter = DateFormatter(); formatter.dateStyle = .medium; formatter.timeStyle = .short + let formatter = DateFormatter() + formatter.locale = L10n.context.formatLocale + formatter.dateStyle = .medium; formatter.timeStyle = .short return formatter.string(from: date) } public static func restoreMessage(_ snapshot: SaveSnapshot, installedVersion: String?) -> String { - let saved = snapshot.identity.version ?? "unknown" - let current = installedVersion ?? "unknown" - return "Restore the backup from \(date(snapshot.created))? The current save will be backed up first.\n\nBackup game version: \(saved) · Installed: \(current). Different versions may be incompatible.\n\nCloud saves may replace the restored local save if sync is enabled later." + let saved = snapshot.identity.version ?? L10n.text("Unknown") + let current = installedVersion ?? L10n.text("Unknown") + return L10n.format("Restore the backup from %@? The current save will be backed up first.\n\nBackup game version: %@ · Installed: %@. Different versions may be incompatible.\n\nCloud saves may replace the restored local save if sync is enabled later.", [date(snapshot.created), saved, current]) } public static func importSummary(_ preview: SaveImportPreview, version: String?) -> String { - let files = preview.fileCount == 1 ? "1 file" : "\(preview.fileCount) files" - return [files, size(preview.byteCount), version.map { "Game v" + $0 }] + let files = L10n.count("%lld files", preview.fileCount) + return [files, size(preview.byteCount), version.map { L10n.format("Game v%@", [$0]) }] .compactMap { $0 }.joined(separator: " · ") } public static func reason(_ reason: SaveSnapshotReason) -> String { - switch reason { case .manual: return "Manual backup"; case .beforeImport: return "Before import"; case .beforeRestore: return "Before restore" } + switch reason { + case .manual: return L10n.text("Manual backup") + case .beforeImport: return L10n.text("Before import") + case .beforeRestore: return L10n.text("Before restore") + } } } diff --git a/Sources/NXLibraryCore/Models/SettingsCatalog.swift b/Sources/NXLibraryCore/Models/SettingsCatalog.swift index 1389169..e9fa9da 100644 --- a/Sources/NXLibraryCore/Models/SettingsCatalog.swift +++ b/Sources/NXLibraryCore/Models/SettingsCatalog.swift @@ -36,6 +36,8 @@ public enum SettingsCatalog { public static let all: [SettingDescriptor] = [ // Start-up + row("appLanguage", .startUp, "App language", "Menus, genres and available game descriptions. The games’ own language stays separate.") { $0.appLanguage }, + row("appRegion", .startUp, "Regional formats", "Dates, numbers and sizes only. Game descriptions follow the app language.") { $0.appRegion }, row("startOn", .startUp, "Start on", "The screen NX Library opens on: Spotlight, Collection, or where you left it.") { $0.startOn }, row("restoreLastGame", .startUp, "Restore last game", @@ -155,8 +157,8 @@ public enum SettingsCatalog { "A notification when a porting finishes while NX Library is in the background.") { $0.notifyPortingEnds }, // Storage - action("storageUsage", .storage, "Storage usage", "Recalculate apps, caches, logs and free space."), - action("buildCaches", .storage, "Build caches", "Delete extracted build caches after the active porting."), + action("storageUsage", .storage, "Storage usage", "See per-game app, cache and log sizes and free disk space."), + action("buildCaches", .storage, "Build caches", "Delete a game's build cache or clear all caches after confirmation; rebuilds may take longer."), action("logs", .storage, "Logs", "Delete build and launch logs after confirmation."), action("metadataCache", .storage, "Metadata cache", "Clear cached store descriptions and links."), action("artworkCache", .storage, "Artwork cache", "Clear downloaded images; manual picks are kept."), @@ -189,6 +191,8 @@ public enum SettingsCatalog { let options: String.CompareOptions = [.caseInsensitive, .diacriticInsensitive] return all.filter { $0.title.range(of: needle, options: options) != nil + || L10n.text($0.title).range(of: needle, options: options) != nil + || L10n.text($0.about).range(of: needle, options: options) != nil || $0.about.range(of: needle, options: options) != nil } } diff --git a/Sources/NXLibraryCore/Models/SettingsSection.swift b/Sources/NXLibraryCore/Models/SettingsSection.swift index 3c01c54..097c3c4 100644 --- a/Sources/NXLibraryCore/Models/SettingsSection.swift +++ b/Sources/NXLibraryCore/Models/SettingsSection.swift @@ -8,7 +8,13 @@ public enum SettingsSection: String, CaseIterable, Identifiable, Equatable { public var id: String { rawValue } - public var title: String { + public var title: String { title(in: L10n.context) } + + public func title(in context: AppLocale) -> String { + L10n.text(titleKey, context: context) + } + + private var titleKey: String { switch self { case .search: return "Search" case .changed: return "Changed" @@ -28,19 +34,19 @@ public enum SettingsSection: String, CaseIterable, Identifiable, Equatable { public var subtitle: String { switch self { - case .search: return "Find any setting by name or description." - case .changed: return "Settings that differ from the defaults." - case .startUp: return "Startup, setup and readiness." - case .navigation: return "Button hints, button names and the View button." - case .spotlight: return "Which cards show on Spotlight and where it opens." - case .soundMusic: return "Interface sounds, volume and the launch screen." - case .look: return "How NX Library looks and moves." - case .games: return "Launch screen, hidden games and defaults for new games." - case .controllers: return "Library controls and new-game rumble." - case .library: return "Folders, imports and artwork caches." - case .storage: return "Caches, logs, saves and the console." - case .engine: return "Engine pin, toolchain gate and keys." - case .about: return "Version, credits and licenses." + case .search: return L10n.text("Find any setting by name or description.") + case .changed: return L10n.text("Settings that differ from the defaults.") + case .startUp: return L10n.text("Startup, setup and readiness.") + case .navigation: return L10n.text("Button hints, button names and the View button.") + case .spotlight: return L10n.text("Which cards show on Spotlight and where it opens.") + case .soundMusic: return L10n.text("Interface sounds, volume and the launch screen.") + case .look: return L10n.text("How NX Library looks and moves.") + case .games: return L10n.text("Launch screen, hidden games and defaults for new games.") + case .controllers: return L10n.text("Library controls and new-game rumble.") + case .library: return L10n.text("Folders, imports and artwork caches.") + case .storage: return L10n.text("Caches, logs, saves and the console.") + case .engine: return L10n.text("Engine pin, toolchain gate and keys.") + case .about: return L10n.text("Version, credits and licenses.") } } diff --git a/Sources/NXLibraryCore/Models/SetupCheck.swift b/Sources/NXLibraryCore/Models/SetupCheck.swift index fc8e005..5a91bf6 100644 --- a/Sources/NXLibraryCore/Models/SetupCheck.swift +++ b/Sources/NXLibraryCore/Models/SetupCheck.swift @@ -3,7 +3,7 @@ import Foundation /// The Setup check's cards, in screen order: the player's folders first, /// then prerequisites for building games. public enum SetupItemID: String, CaseIterable, Codable { - case library, sources, xcode, metal, dotnet, engine, runtime, keys + case library, sources, xcode, metal, engine, runtime, keys } public enum SetupState: String, Codable, Equatable { @@ -40,7 +40,6 @@ public struct SetupFacts: Equatable { public var metalVersion: String? public var certifiedMetal: String public var allowUntested: Bool - public var dotnetSDKs: [String] public var engine: EngineFacts public var engineFolder: URL public var pinnedCommit: String @@ -62,8 +61,6 @@ public struct SetupFacts: Equatable { public var extractedFoldersSupported: Bool = false /// "27.1"; nil when xcodebuild did not say. public var xcodeVersion: String? - /// The newest SDK of the required major Microsoft publishes; nil until known. - public var dotnetLatest: String? public var requirements: ToolchainRequirements /// The checkout's HEAD is an ancestor of the pin (an NX Library update brought a newer engine); false when it /// is ahead or elsewhere; nil when git cannot tell (the pin is not fetched yet, so it is newer). @@ -72,15 +69,14 @@ public struct SetupFacts: Equatable { public var newestEngine: NewestEngineChoice? = nil public init(xcodeStatus: Int32?, metalVersion: String?, certifiedMetal: String, allowUntested: Bool, - dotnetSDKs: [String], engine: EngineFacts, engineFolder: URL, pinnedCommit: String, + engine: EngineFacts, engineFolder: URL, pinnedCommit: String, runtimeProblem: String?, keys: KeySummary?, keysError: String?, libraryReadable: Bool?, - xcodeVersion: String? = nil, dotnetLatest: String? = nil, + xcodeVersion: String? = nil, requirements: ToolchainRequirements = .current, engineBehind: Bool? = nil) { self.xcodeStatus = xcodeStatus self.metalVersion = metalVersion self.certifiedMetal = certifiedMetal self.allowUntested = allowUntested - self.dotnetSDKs = dotnetSDKs self.engine = engine self.engineFolder = engineFolder self.pinnedCommit = pinnedCommit @@ -89,7 +85,6 @@ public struct SetupFacts: Equatable { self.keysError = keysError self.libraryReadable = libraryReadable self.xcodeVersion = xcodeVersion - self.dotnetLatest = dotnetLatest self.requirements = requirements self.engineBehind = engineBehind } @@ -135,9 +130,9 @@ public enum SetupCheck { } /// The cards NX Library fixes by itself, in card order: anything missing it can - /// install, the certified Metal compiler, the pinned engine commit, and (when - /// asked) a newer SDK of the .NET major the engine needs. - public static func automatic(_ items: [SetupItem], includeUpdates: Bool) -> [SetupItemID] { + /// install, the certified Metal compiler and the pinned engine commit. Since nxnative 641636e2 the + /// engine's tools are native executables, so there is no .NET to install or update. + public static func automatic(_ items: [SetupItem]) -> [SetupItemID] { let xcodeReady = items.first { $0.id == .xcode }?.state == .ready return items.compactMap { item in guard item.state != .working, item.actionTitle != nil else { return nil } @@ -147,8 +142,6 @@ public enum SetupCheck { return xcodeReady && (item.state == .missing || item.state == .warning) ? item.id : nil case .engine: return item.state == .missing || item.state == .warning ? item.id : nil - case .dotnet: - return item.state == .missing || (includeUpdates && item.state == .ready) ? item.id : nil default: return nil } @@ -167,157 +160,143 @@ public enum SetupCheck { private static func item(_ id: SetupItemID, _ f: SetupFacts) -> SetupItem { switch id { case .xcode: - let about = "Apple's developer tools. Every game is compiled with them." + let about = L10n.text("Apple's developer tools. Every game is compiled with them.") switch f.xcodeStatus { case nil: - return SetupItem(id: id, title: "Xcode", state: .missing, chip: "NOT INSTALLED", about: about, - detail: "Get it from the App Store, then open it once.", actionTitle: "Get Xcode") + return SetupItem(id: id, title: L10n.text("Xcode"), state: .missing, chip: "NOT INSTALLED", about: about, + detail: L10n.text("Get it from the App Store, then open it once."), actionTitle: L10n.text("Get Xcode")) case 69?: - return SetupItem(id: id, title: "Xcode", state: .needsYou, chip: "OPEN XCODE ONCE", about: about, - detail: "Open Xcode and accept its licence; it then installs its components.", - actionTitle: "Open Xcode") + return SetupItem(id: id, title: L10n.text("Xcode"), state: .needsYou, chip: "OPEN XCODE ONCE", about: about, + detail: L10n.text("Open Xcode and accept its licence; it then installs its components."), + actionTitle: L10n.text("Open Xcode")) default: let minimum = f.requirements.minimumXcode if let version = f.xcodeVersion, VersionNumber.isOlder(version, than: minimum) { - return SetupItem(id: id, title: "Xcode", state: .needsYou, chip: "UPDATE XCODE", about: about, - detail: "Xcode \(version) is installed; the engine needs \(minimum) or newer. Update it from the App Store.", - actionTitle: "Open App Store") + return SetupItem(id: id, title: L10n.text("Xcode"), state: .needsYou, chip: "UPDATE XCODE", about: about, + detail: L10n.format("Xcode %@ is installed; the engine needs %@ or newer. Update it from the App Store.", [String(describing: (version)), String(describing: (minimum))]), + actionTitle: L10n.text("Open App Store")) } - return ready(id, "Xcode", about, f.xcodeVersion.map { "Xcode \($0), installed and set up." } ?? "Installed and set up.") + return ready(id, "Xcode", about, f.xcodeVersion.map { L10n.format("Xcode %@, installed and set up.", [String(describing: ($0))]) } ?? L10n.text("Installed and set up.")) } case .metal: - let about = "Compiles each game's shaders for this Mac's GPU." + let about = L10n.text("Compiles each game's shaders for this Mac's GPU.") guard let version = f.metalVersion else { - return SetupItem(id: id, title: "Metal compiler", state: .missing, chip: "NOT INSTALLED", - about: about, detail: "NX Library downloads it through Xcode.", actionTitle: "Install") + return SetupItem(id: id, title: L10n.text("Metal compiler"), state: .missing, chip: "NOT INSTALLED", + about: about, detail: L10n.text("NX Library downloads it through Xcode."), actionTitle: L10n.text("Install")) } if version == f.certifiedMetal { - return ready(id, "Metal compiler", about, "Version \(version), the one this engine is tested with.") + return ready(id, "Metal compiler", about, L10n.format("Version %@, the one this engine is tested with.", [String(describing: (version))])) } if f.allowUntested { return ready(id, "Metal compiler", about, - "Version \(version); the untested toolchain override is on, so builds may differ.") + L10n.format("Version %@; the untested toolchain override is on, so builds may differ.", [String(describing: (version))])) } if f.requirements.metalDownload(for: f.certifiedMetal) != nil { - return SetupItem(id: id, title: "Metal compiler", state: .warning, chip: "NOT CERTIFIED", about: about, - detail: "Version \(version); the engine is tested with \(f.certifiedMetal). NX Library installs \(f.certifiedMetal) through Xcode.", - actionTitle: "Install \(f.certifiedMetal)") + return SetupItem(id: id, title: L10n.text("Metal compiler"), state: .warning, chip: "NOT CERTIFIED", about: about, + detail: L10n.format("Version %@; the engine is tested with %@. NX Library installs %@ through Xcode.", [String(describing: (version)), String(describing: (f.certifiedMetal)), String(describing: (f.certifiedMetal))]), + actionTitle: L10n.format("Install %@", [String(describing: (f.certifiedMetal))])) } - return SetupItem(id: id, title: "Metal compiler", state: .warning, chip: "WAITING FOR ENGINE", about: about, - detail: "Version \(version) came with this Xcode; the engine is tested with \(f.certifiedMetal). New builds wait for an NX Library update tested with it. Games you built still play.", + return SetupItem(id: id, title: L10n.text("Metal compiler"), state: .warning, chip: "WAITING FOR ENGINE", about: about, + detail: L10n.format("Version %@ came with this Xcode; the engine is tested with %@. New builds wait for an NX Library update tested with it. Games you built still play.", [String(describing: (version)), String(describing: (f.certifiedMetal))]), actionTitle: nil) - case .dotnet: - let about = "Builds the engine's tools that read each game's package." - if let installed = ToolchainRequirements.newestSDK(major: f.requirements.dotnetMajor, in: f.dotnetSDKs) { - if let latest = f.dotnetLatest, VersionNumber.isOlder(installed, than: latest) { - var card = ready(id, ".NET 10 SDK", about, - "\(installed) installed. Microsoft has \(latest); A installs it, and macOS asks for your password.") - card.actionTitle = "Update to \(latest)" - return card - } - return ready(id, ".NET 10 SDK", about, "\(installed) installed.") - } - return SetupItem(id: id, title: ".NET 10 SDK", state: .missing, chip: "NOT INSTALLED", about: about, - detail: "NX Library downloads Microsoft's installer; macOS asks for your password.", - actionTitle: "Install") case .engine: - let about = "nxnative, used as it is upstream, at the commit this NX Library is tested with." + let about = L10n.text("nxnative, used as it is upstream, at the commit this NX Library is tested with.") let short = String(f.pinnedCommit.prefix(7)) switch f.engine { case .missing: - return SetupItem(id: id, title: "nxnative engine", state: .missing, chip: "NOT INSTALLED", about: about, - detail: "NX Library clones it into \(f.engineFolder.path) at \(short).", actionTitle: "Clone") + return SetupItem(id: id, title: L10n.text("nxnative engine"), state: .missing, chip: "NOT INSTALLED", about: about, + detail: L10n.format("NX Library clones it into %@ at %@.", [String(describing: (f.engineFolder.path)), String(describing: (short))]), actionTitle: L10n.text("Clone")) case .notACheckout: - return SetupItem(id: id, title: "nxnative engine", state: .warning, chip: "NOT A CHECKOUT", about: about, - detail: "\(f.engineFolder.path) is not a git checkout of nxnative. Move it away, then check again.", + return SetupItem(id: id, title: L10n.text("nxnative engine"), state: .warning, chip: "NOT A CHECKOUT", about: about, + detail: L10n.format("%@ is not a git checkout of nxnative. Move it away, then check again.", [String(describing: (f.engineFolder.path))]), actionTitle: nil) case .checkout(_, true): - return SetupItem(id: id, title: "nxnative engine", state: .warning, chip: "LOCAL CHANGES", about: about, - detail: "\(f.engineFolder.path) has changed files. NX Library never touches them: put them aside, then check again.", + return SetupItem(id: id, title: L10n.text("nxnative engine"), state: .warning, chip: "LOCAL CHANGES", about: about, + detail: L10n.format("%@ has changed files. NX Library never touches them: put them aside, then check again.", [String(describing: (f.engineFolder.path))]), actionTitle: nil) case .checkout(let head, false) where head == f.newestEngine?.commit && head != f.pinnedCommit: // The player's own choice: ready, never "other version", and A goes back to the tested engine. - var card = ready(id, "nxnative engine", NewestEngineCopy.note, + var card = ready(id, L10n.text("nxnative engine"), NewestEngineCopy.note, NewestEngineCopy.setupDetail(choice: f.newestEngine!, tested: f.pinnedCommit)) card.chip = "NEWEST ENGINE" card.isUntested = true - card.actionTitle = "Back to tested" + card.actionTitle = L10n.text("Back to tested") return card case .checkout(let head, false) where head != f.pinnedCommit: // Warning either way: Setup reopens for it and Install all runs it. if f.engineBehind == false { - return SetupItem(id: id, title: "nxnative engine", state: .warning, chip: "OTHER VERSION", about: about, - detail: "The engine here is at \(head.prefix(7)), not the \(short) this NX Library is tested with. Switch moves it to \(short). Games you built keep playing.", - actionTitle: "Switch") + return SetupItem(id: id, title: L10n.text("nxnative engine"), state: .warning, chip: "OTHER VERSION", about: about, + detail: L10n.format("The engine here is at %@, not the %@ this NX Library is tested with. Switch moves it to %@. Games you built keep playing.", [String(describing: (head.prefix(7))), String(describing: (short)), String(describing: (short))]), + actionTitle: L10n.text("Switch")) } - return SetupItem(id: id, title: "nxnative engine", state: .warning, chip: "UPDATE READY", about: about, - detail: "This NX Library uses nxnative \(short); the engine here is at \(head.prefix(7)). Update downloads it and switches to it. Games you built keep playing.", - actionTitle: "Update") + return SetupItem(id: id, title: L10n.text("nxnative engine"), state: .warning, chip: "UPDATE READY", about: about, + detail: L10n.format("This NX Library uses nxnative %@; the engine here is at %@. Update downloads it and switches to it. Games you built keep playing.", [String(describing: (short)), String(describing: (head.prefix(7)))]), + actionTitle: L10n.text("Update")) case .checkout: - return ready(id, "nxnative engine", about, "At \(short) in \(f.engineFolder.path).") + return ready(id, L10n.text("nxnative engine"), about, "At \(short) in \(f.engineFolder.path).") } case .runtime: - let about = "Python 3.14 with its modules, zstd and glslang, inside NX Library." + let about = L10n.text("Python 3.14 with its modules, zstd and glslang, inside NX Library.") if let problem = f.runtimeProblem { - return SetupItem(id: id, title: "Python and tools", state: .failed, chip: "REINSTALL APP", about: about, - detail: "\(problem). Reinstall NX Library from its DMG.", actionTitle: nil) + return SetupItem(id: id, title: L10n.text("Python and tools"), state: .failed, chip: "REINSTALL APP", about: about, + detail: L10n.format("%@. Reinstall NX Library from its DMG.", [String(describing: (problem))]), actionTitle: nil) } - return SetupItem(id: id, title: "Python and tools", state: .ready, chip: "✓ INCLUDED", about: about, - detail: "Nothing to install.", actionTitle: nil) + return SetupItem(id: id, title: L10n.text("Python and tools"), state: .ready, chip: "✓ INCLUDED", about: about, + detail: L10n.text("Nothing to install."), actionTitle: nil) case .keys: - let about = "Your console's prod.keys, which unlock your own games. They stay on this Mac." + let about = L10n.text("Your console's prod.keys, which unlock your own games. They stay on this Mac.") if let keys = f.keys { - return SetupItem(id: id, title: "Keys", state: .ready, chip: "✓ IMPORTED", about: about, - detail: "Header key and \(keys.applicationKeyGenerations.count) application key generations.", - actionTitle: "Replace") + return SetupItem(id: id, title: L10n.text("Keys"), state: .ready, chip: "✓ IMPORTED", about: about, + detail: L10n.format("Header key and %@ application key generations.", [String(describing: (keys.applicationKeyGenerations.count))]), + actionTitle: L10n.text("Replace")) } if f.keysOptional { - return SetupItem(id: id, title: "Keys", state: .ready, chip: "OPTIONAL", about: about, - detail: "Needed for NSP/XCI packages. Extracted folders do not use keys.", actionTitle: "Choose file") + return SetupItem(id: id, title: L10n.text("Keys"), state: .ready, chip: "OPTIONAL", about: about, + detail: L10n.text("Needed for NSP/XCI packages. Extracted folders do not use keys."), actionTitle: L10n.text("Choose file")) } - return SetupItem(id: id, title: "Keys", state: .missing, chip: "NOT IMPORTED", about: about, - detail: f.keysError ?? "Choose your prod.keys file.", actionTitle: "Choose file") + return SetupItem(id: id, title: L10n.text("Keys"), state: .missing, chip: "NOT IMPORTED", about: about, + detail: f.keysError ?? L10n.text("Choose your prod.keys file."), actionTitle: L10n.text("Choose file")) case .library: let about = GameFoldersCopy.appsAbout if f.libraryNeedsConfirmation { return SetupItem(id: id, title: GameFoldersCopy.appsTitle, state: .needsYou, chip: "CHOOSE FOLDER", about: about, - detail: f.libraryFolder.map { "Choose this folder or another: \($0.path)" } - ?? "Choose where to keep your game apps. An empty folder is fine.", - actionTitle: "Choose folder") + detail: f.libraryFolder.map { L10n.format("Choose this folder or another: %@", [String(describing: ($0.path))]) } + ?? L10n.text("Choose where to keep your game apps. An empty folder is fine."), + actionTitle: L10n.text("Choose folder")) } switch f.libraryReadable { case nil: return SetupItem(id: id, title: GameFoldersCopy.appsTitle, state: .missing, chip: "NOT CHOSEN", about: about, - detail: "Choose the folder containing your game apps.", actionTitle: "Choose folder") + detail: L10n.text("Choose the folder containing your game apps."), actionTitle: L10n.text("Choose folder")) case false?: return SetupItem(id: id, title: GameFoldersCopy.appsTitle, state: .warning, chip: "NOT READABLE", about: about, - detail: f.libraryFolder.map { "Cannot read \($0.path). Reconnect it or choose another." } - ?? "NX Library cannot read this folder. Reconnect it or choose another.", actionTitle: "Choose folder") + detail: f.libraryFolder.map { L10n.format("Cannot read %@. Reconnect it or choose another.", [String(describing: ($0.path))]) } + ?? L10n.text("NX Library cannot read this folder. Reconnect it or choose another."), actionTitle: L10n.text("Choose folder")) case true?: if f.libraryWritable == false { return SetupItem(id: id, title: GameFoldersCopy.appsTitle, state: .warning, chip: "READ ONLY", about: about, - detail: "Choose a writable folder for new builds. Existing apps still play.", - actionTitle: "Change") + detail: L10n.text("Choose a writable folder for new builds. Existing apps still play."), + actionTitle: L10n.text("Change")) } return SetupItem(id: id, title: GameFoldersCopy.appsTitle, state: .ready, chip: "✓ READY", about: about, detail: [GameFoldersCopy.appsContents, f.libraryFolder?.path].compactMap { $0 }.joined(separator: "\n"), - actionTitle: "Change") + actionTitle: L10n.text("Change")) } case .sources: let about = GameFoldersCopy.sourcesAbout(extractedFolders: f.extractedFoldersSupported) switch f.sourcesReadable { case nil: return SetupItem(id: id, title: GameFoldersCopy.sourcesTitle, state: .ready, chip: "OPTIONAL", about: about, - detail: "Existing game apps play without sources. Choose later in Settings → Library.", - actionTitle: "Choose folder", isRequired: false) + detail: L10n.text("Existing game apps play without sources. Choose later in Settings → Library."), + actionTitle: L10n.text("Choose folder"), isRequired: false) case false?: return SetupItem(id: id, title: GameFoldersCopy.sourcesTitle, state: .warning, chip: "FOLDER UNAVAILABLE", about: about, - detail: f.sourcesFolder.map { "Unavailable: \($0.path). Existing game apps still play." } - ?? "Reconnect this folder or choose another. Existing game apps still play.", - actionTitle: "Change", isRequired: false) + detail: f.sourcesFolder.map { L10n.format("Unavailable: %@. Existing game apps still play.", [String(describing: ($0.path))]) } + ?? L10n.text("Reconnect this folder or choose another. Existing game apps still play."), + actionTitle: L10n.text("Change"), isRequired: false) case true?: return SetupItem(id: id, title: GameFoldersCopy.sourcesTitle, state: .ready, chip: "✓ CHOSEN", about: about, - detail: f.sourcesFolder?.path ?? "Ready to find game sources.", actionTitle: "Change", isRequired: false) + detail: f.sourcesFolder?.path ?? L10n.text("Ready to find game sources."), actionTitle: L10n.text("Change"), isRequired: false) } } } diff --git a/Sources/NXLibraryCore/Models/SpotlightRow.swift b/Sources/NXLibraryCore/Models/SpotlightRow.swift index 46d2661..dd595a2 100644 --- a/Sources/NXLibraryCore/Models/SpotlightRow.swift +++ b/Sources/NXLibraryCore/Models/SpotlightRow.swift @@ -5,11 +5,11 @@ public enum SpotlightCard: String, Codable, CaseIterable { public var title: String { switch self { - case .collection: return "Collection" - case .surprise: return "Surprise me" - case .activity: return "Activity" - case .mods: return "Mods" - case .addGame: return "Add Game" + case .collection: return L10n.text("Collection") + case .surprise: return L10n.text("Surprise me") + case .activity: return L10n.text("Activity") + case .mods: return L10n.text("Mods") + case .addGame: return L10n.text("Add Game") } } diff --git a/Sources/NXLibraryCore/Models/TargetPlatform.swift b/Sources/NXLibraryCore/Models/TargetPlatform.swift index 38d6698..bc0266f 100644 --- a/Sources/NXLibraryCore/Models/TargetPlatform.swift +++ b/Sources/NXLibraryCore/Models/TargetPlatform.swift @@ -4,7 +4,7 @@ public enum TargetPlatform: String, Codable, CaseIterable, Identifiable { case macApp = "mac-app" public var id: String { rawValue } - public var title: String { "Mac App (.app)" } + public var title: String { L10n.text("Mac App (.app)") } public var iconName: String { "macbook.gen2" } - public var subtitle: String { "Double-clickable native macOS app for Apple Silicon." } + public var subtitle: String { L10n.text("Double-clickable native macOS app for Apple Silicon.") } } diff --git a/Sources/NXLibraryCore/Models/ToolchainGateCopy.swift b/Sources/NXLibraryCore/Models/ToolchainGateCopy.swift index cd135b1..c0ffde1 100644 --- a/Sources/NXLibraryCore/Models/ToolchainGateCopy.swift +++ b/Sources/NXLibraryCore/Models/ToolchainGateCopy.swift @@ -35,31 +35,31 @@ public enum ToolchainGateCopy { public static func summary(report: CompatibilityReport, overrideOn: Bool, newest: Bool = false) -> Summary { if newest && report.ok { // The Newest engine row right below says the sentence; the panel only names the state. - return Summary(label: "Newest engine", tone: .newest, message: nil, buildHint: "") + return Summary(label: L10n.text("Newest engine"), tone: .newest, message: nil, buildHint: "") } - if report.ok { return Summary(label: "Certified", tone: .certified, message: nil, buildHint: "") } + if report.ok { return Summary(label: L10n.text("Certified"), tone: .certified, message: nil, buildHint: "") } let blockers = report.checks.filter { $0.status == .block } if blockers.allSatisfy({ $0.id == "metal" }) { switch report.metal { case .newer(let installed, let required): - let head = "Your Metal compiler (\(installed)) is newer than the one nxnative is tested with (\(required))." - return Summary(label: overrideOn ? "Newer Metal · override on" : "Newer Metal", tone: .info, - message: head + (overrideOn ? " There should be no problems; builds are recorded as untested." - : " There should be no problems: turn on Untested toolchain override to build."), - buildHint: "Newer Metal: turn on the override in Settings → Engine") + let head = L10n.format("Your Metal compiler (%@) is newer than the one nxnative is tested with (%@).", [String(describing: (installed)), String(describing: (required))]) + return Summary(label: overrideOn ? L10n.text("Newer Metal · override on") : L10n.text("Newer Metal"), tone: .info, + message: head + (overrideOn ? L10n.text(" There should be no problems; builds are recorded as untested.") + : L10n.text(" There should be no problems: turn on Untested toolchain override to build.")), + buildHint: L10n.text("Newer Metal: turn on the override in Settings → Engine")) case .older(let installed, let required): - let head = "Your Metal compiler (\(installed)) is older than the one nxnative needs (\(required)). " - + "Updating to Metal \(required) is strongly recommended." - return Summary(label: overrideOn ? "Older Metal · override on" : "Update recommended", tone: .warning, - message: head + (overrideOn ? " Builds are recorded as untested." - : " Untested toolchain override builds anyway, at your own risk."), - buildHint: "Older Metal: update it, or use the override in Settings → Engine") + let head = L10n.format("Your Metal compiler (%@) is older than the one nxnative needs (%@). ", [String(describing: (installed)), String(describing: (required))]) + + L10n.format("Updating to Metal %@ is strongly recommended.", [String(describing: (required))]) + return Summary(label: overrideOn ? L10n.text("Older Metal · override on") : L10n.text("Update recommended"), tone: .warning, + message: head + (overrideOn ? L10n.text(" Builds are recorded as untested.") + : L10n.text(" Untested toolchain override builds anyway, at your own risk.")), + buildHint: L10n.text("Older Metal: update it, or use the override in Settings → Engine")) case .same, .unknown: break } } return overrideOn - ? Summary(label: "Override enabled", tone: .override, message: nil, buildHint: "") - : Summary(label: "Blocked", tone: .blocked, message: nil, buildHint: "Build blocked — Settings → Engine") + ? Summary(label: L10n.text("Override enabled"), tone: .override, message: nil, buildHint: "") + : Summary(label: L10n.text("Blocked"), tone: .blocked, message: nil, buildHint: L10n.text("Build blocked — Settings → Engine")) } } diff --git a/Sources/NXLibraryCore/Models/ToolchainRequirements.swift b/Sources/NXLibraryCore/Models/ToolchainRequirements.swift index c935cdc..e8227d4 100644 --- a/Sources/NXLibraryCore/Models/ToolchainRequirements.swift +++ b/Sources/NXLibraryCore/Models/ToolchainRequirements.swift @@ -5,24 +5,20 @@ import Foundation public struct ToolchainRequirements: Equatable { /// The oldest Xcode the engine's Metal toolchain runs with. public var minimumXcode: String - /// The .NET SDK major the engine's extractor builds with; any patch of it works. - public var dotnetMajor: Int /// The certified Metal compiler before an engine exists to name one in its lock. public var fallbackMetal: String /// Metal compiler version → Apple's MetalToolchain asset build, so NX Library can /// download exactly the compiler the engine certifies, not just the newest one. public var metalAssets: [String: String] - public init(minimumXcode: String, dotnetMajor: Int, fallbackMetal: String, metalAssets: [String: String]) { + public init(minimumXcode: String, fallbackMetal: String, metalAssets: [String: String]) { self.minimumXcode = minimumXcode - self.dotnetMajor = dotnetMajor self.fallbackMetal = fallbackMetal self.metalAssets = metalAssets } public static let current = ToolchainRequirements( minimumXcode: "27.2", - dotnetMajor: 10, fallbackMetal: "32023.922", metalAssets: ["32023.921": "27A266a", "32023.922": "27B47"]) @@ -39,13 +35,6 @@ public struct ToolchainRequirements: Equatable { guard let line = text.split(separator: "\n").first, line.hasPrefix("Xcode ") else { return nil } return String(line.dropFirst("Xcode ".count)).trimmingCharacters(in: .whitespaces) } - - /// The newest SDK of `major` in `dotnet --list-sdks` output. - public static func newestSDK(major: Int, in lines: [String]) -> String? { - lines.compactMap { $0.split(separator: " ").first.map(String.init) } - .filter { $0.hasPrefix("\(major).") } - .max { VersionNumber.isOlder($0, than: $1) } - } } public enum VersionNumber { diff --git a/Sources/NXLibraryCore/Models/UpstreamCopy.swift b/Sources/NXLibraryCore/Models/UpstreamCopy.swift index 3e2c536..b7f5872 100644 --- a/Sources/NXLibraryCore/Models/UpstreamCopy.swift +++ b/Sources/NXLibraryCore/Models/UpstreamCopy.swift @@ -4,7 +4,7 @@ import Foundation /// updates, once tested. The Newest engine row is the only way to use them earlier, by the player's own choice. public enum UpstreamCopy { public static func subtitle(upToDate: Bool, newCommits: Int) -> String { - if upToDate { return "This NX Library uses the newest nxnative." } - return "nxnative has \(newCommits) newer commit\(newCommits == 1 ? "" : "s"). Engine updates reach you with NX Library updates, once tested." + if upToDate { return L10n.text("This NX Library uses the newest nxnative.") } + return L10n.count("nxnative has %lld newer commits. Engine updates reach you with NX Library updates, once tested.", newCommits, arguments: []) } } diff --git a/Sources/NXLibraryCore/Models/WallpaperCatalog.swift b/Sources/NXLibraryCore/Models/WallpaperCatalog.swift index 825e928..088b5b6 100644 --- a/Sources/NXLibraryCore/Models/WallpaperCatalog.swift +++ b/Sources/NXLibraryCore/Models/WallpaperCatalog.swift @@ -18,7 +18,7 @@ public struct Wallpaper: Equatable, Identifiable { let name = url.deletingPathExtension().lastPathComponent let words = name.split(separator: "-").drop { $0.allSatisfy(\.isNumber) }.joined(separator: " ") let themeName = theme == WallpaperCatalog.custom - ? "Your own" : theme.split(separator: "-").map { $0.capitalized }.joined(separator: " ") + ? L10n.text("Your own") : theme.split(separator: "-").map { $0.capitalized }.joined(separator: " ") return words.isEmpty ? themeName : "\(themeName) · \(words.prefix(1).uppercased())\(words.dropFirst())" } } diff --git a/Sources/NXLibraryCore/Models/WallpaperPlacement.swift b/Sources/NXLibraryCore/Models/WallpaperPlacement.swift index 2882ca6..4d38644 100644 --- a/Sources/NXLibraryCore/Models/WallpaperPlacement.swift +++ b/Sources/NXLibraryCore/Models/WallpaperPlacement.swift @@ -12,9 +12,9 @@ public enum WallpaperPlacement: CaseIterable, Sendable { public var title: String { switch self { - case .notNow: return "Not now" - case .settingsOnly: return "Settings only" - case .everywhere: return "App & Settings" + case .notNow: return L10n.text("Not now") + case .settingsOnly: return L10n.text("Settings only") + case .everywhere: return L10n.text("App & Settings") } } diff --git a/Sources/NXLibraryCore/Resources/Localizable.xcstrings b/Sources/NXLibraryCore/Resources/Localizable.xcstrings new file mode 100644 index 0000000..de61c48 --- /dev/null +++ b/Sources/NXLibraryCore/Resources/Localizable.xcstrings @@ -0,0 +1,68924 @@ +{ + "sourceLanguage": "en", + "strings": { + "\n\n--- Build log: %@ ---\n": { + "comment": "ConsoleView.swift:481", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "\n\n--- Build log: %@ ---\n" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "\n\n--- Log di compilazione: %@ ---\n" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\n\n--- Journal de compilation : %@ ---\n" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "\n\n--- Build-Protokoll: %@ ---\n" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "\n\n--- Registro de compilación: %@ ---\n" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "\n\n--- Log de compilação: %@ ---\n" + } + } + } + }, + "\nCache shared with %@.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "\nCache shared with %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "\nCache condivisa con %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\nCache partagé avec %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "\nCache gemeinsam mit %@ genutzt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "\nCaché compartida con %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "\nCache compartilhado com %@." + } + } + } + }, + "\nYou can retry installation.": { + "comment": "AppShellView.swift:667", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "\nYou can retry installation." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "\nPuoi riprovare l’installazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\nVous pouvez réessayer l’installation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "\nDu kannst die Installation erneut versuchen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "\nPuedes reintentar la instalación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "\nVocê pode tentar instalar novamente." + } + } + } + }, + "\nYour current app is still available.": { + "comment": "AppShellView.swift:667", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "\nYour current app is still available." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "\nL’app attuale è ancora disponibile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "\nVotre app actuelle reste disponible." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "\nDeine bisherige App ist weiterhin verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "\nTu app actual sigue disponible." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "\nSeu app atual continua disponível." + } + } + } + }, + " · light": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " · light" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " · chiaro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " · clair" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " · hell" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " · claro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " · claro" + } + } + } + }, + " (rewrite stopped at 0x%@)": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " (rewrite stopped at 0x%@)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " (riscrittura interrotta a 0x%@)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " (réécriture arrêtée à 0x%@)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " (Umschreiben bei 0x%@ gestoppt)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " (reescritura detenida en 0x%@)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " (reescrita interrompida em 0x%@)" + } + } + } + }, + " A large game takes about a minute.": { + "comment": "ChangeArtworkView.swift:193", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " A large game takes about a minute." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Per un gioco di grandi dimensioni serve circa un minuto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Cela prend environ une minute pour un jeu volumineux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Bei großen Spielen dauert das etwa eine Minute." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " Un juego grande tarda aproximadamente un minuto." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " Um jogo grande leva cerca de um minuto." + } + } + } + }, + " Builds are recorded as untested.": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " Builds are recorded as untested." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Le compilazioni sono registrate come non testate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Les compilations sont enregistrées comme non testées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Builds werden als ungetestet erfasst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " Las compilaciones se registran como no probadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " As compilações são registradas como não testadas." + } + } + } + }, + " It changes when the game quits.": { + "comment": "ChangeArtworkView.swift:193", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " It changes when the game quits." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Cambia alla chiusura del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Elle sera modifiée à la fermeture du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Wird geändert, sobald das Spiel beendet wird." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " Se cambia cuando se cierra el juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " A alteração será aplicada quando o jogo for encerrado." + } + } + } + }, + " Rebuild to use cheats.": { + "comment": "CheatsPanel.swift:109", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " Rebuild to use cheats." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Ricompila per usare i trucchi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Recompilez pour utiliser les codes de triche." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Erstelle die App erneut, um Cheats zu verwenden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " Recompila para usar trucos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " Recompile para usar trapaças." + } + } + } + }, + " The Game apps folder's drive can't hold build files (it needs APFS or Mac OS Extended), so the build uses this Mac's disk.": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " The Game apps folder's drive can't hold build files (it needs APFS or Mac OS Extended), so the build uses this Mac's disk." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " L’unità della cartella App dei giochi non può contenere i file di build (richiede APFS o Mac OS esteso), quindi la build usa il disco di questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Le disque du dossier des apps de jeux ne peut pas héberger les fichiers de compilation (APFS ou Mac OS étendu requis). La compilation utilise donc le disque de ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Das Laufwerk des Spiele-App-Ordners kann keine Build-Dateien speichern (APFS oder Mac OS Extended erforderlich). Daher nutzt der Build die Festplatte dieses Macs." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " La unidad de la carpeta de apps de juegos no puede guardar archivos de compilación (requiere APFS o Mac OS Plus), por lo que se usa el disco de este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " A unidade da pasta de apps de jogos não pode armazenar arquivos de compilação (requer APFS ou Mac OS Expandido), então a compilação usa o disco deste Mac." + } + } + } + }, + " The log has the details.": { + "comment": "BuildWarningCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " The log has the details." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Il log contiene i dettagli." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Le journal contient les détails." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Einzelheiten stehen im Protokoll." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " El registro contiene los detalles." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " O log contém os detalhes." + } + } + } + }, + " There should be no problems: turn on Untested toolchain override to build.": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " There should be no problems: turn on Untested toolchain override to build." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Non dovrebbero esserci problemi: attiva Ignora controllo della toolchain non testata per compilare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Aucun problème n’est attendu : activez Dérogation pour chaîne d’outils non testée pour compiler." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Es sollten keine Probleme auftreten: Aktiviere „Ungetestete Toolchain zulassen“, um Apps zu erstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " No debería haber problemas: activa Permitir cadena de herramientas no probada para compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " Não deve haver problemas: ative Permitir conjunto de ferramentas não testado para compilar." + } + } + } + }, + " There should be no problems; builds are recorded as untested.": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " There should be no problems; builds are recorded as untested." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Non dovrebbero esserci problemi; le compilazioni sono registrate come non testate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Aucun problème n’est attendu ; les compilations sont enregistrées comme non testées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Es sollten keine Probleme auftreten; Builds werden als ungetestet erfasst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " No debería haber problemas; las compilaciones se registran como no probadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " Não deve haver problemas; as compilações são registradas como não testadas." + } + } + } + }, + " This cache is also used by %@; their next rebuilds may take longer too.": { + "comment": "SettingsStorageSection.swift:168", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " This cache is also used by %@; their next rebuilds may take longer too." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Questa cache viene usata anche da %@; anche le relative ricompilazioni potrebbero richiedere più tempo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " Ce cache est aussi utilisé par %@ ; leurs prochaines recompilations peuvent également prendre plus de temps." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Dieser Cache wird auch von %@ verwendet; die nächsten Neuerstellungen dieser Spiele können ebenfalls länger dauern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " Esta caché también se usa para %@; sus próximas recompilaciones también podrían tardar más." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " Este cache também é usado por %@; as próximas recompilações desses jogos também podem demorar mais." + } + } + } + }, + " Untested toolchain override builds anyway, at your own risk.": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " Untested toolchain override builds anyway, at your own risk." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Ignora controllo della toolchain non testata consente di compilare comunque, a tuo rischio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " La dérogation pour chaîne d’outils non testée permet tout de même de compiler, à vos risques et périls." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " „Ungetestete Toolchain zulassen“ ermöglicht Builds trotzdem, auf eigenes Risiko." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " Permitir cadena de herramientas no probada deja compilar de todos modos, bajo tu responsabilidad." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " Permitir conjunto de ferramentas não testado compila mesmo assim, por sua conta e risco." + } + } + } + }, + " Your theme’s own background takes its place.": { + "comment": "ThemePickerView.swift:834", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " Your theme’s own background takes its place." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " Al suo posto viene usato lo sfondo del tema." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " L’arrière-plan du thème prend sa place." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " Stattdessen wird der eigene Hintergrund deines Designs verwendet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " El fondo propio de tu tema lo reemplaza." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " O fundo do próprio tema toma seu lugar." + } + } + } + }, + " · new portings need Xcode %@": { + "comment": "SettingsAboutSection.swift:226", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": " · new portings need Xcode %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": " · le nuove creazioni di app per Mac richiedono Xcode %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": " · les nouvelles créations d’apps Mac nécessitent Xcode %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": " · neue App-Erstellungen benötigen Xcode %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": " · las nuevas compilaciones necesitan Xcode %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": " · novas compilações precisam do Xcode %@" + } + } + } + }, + "%@ (recommended)": { + "comment": "ModDetailPanel.swift:297", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ (recommended)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ (scelta consigliata)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ (choix recommandé)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ (empfohlen)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ (recomendado)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ (recomendado)" + } + } + } + }, + "%@ (requires 3.12+)": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ (requires 3.12+)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ (richiede 3.12+)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ (nécessite 3.12+)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ (3.12+ erforderlich)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ (requiere 3.12+)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ (requer 3.12+)" + } + } + } + }, + "%@ IS UNAVAILABLE RIGHT NOW": { + "comment": "OnlineBrowsePanel.swift:144", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ IS UNAVAILABLE RIGHT NOW" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ NON È AL MOMENTO DISPONIBILE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ EST INDISPONIBLE POUR LE MOMENT" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ IST DERZEIT NICHT VERFÜGBAR" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ NO ESTÁ DISPONIBLE AHORA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ ESTÁ INDISPONÍVEL NO MOMENTO" + } + } + } + }, + "%@ In use: %@, ": { + "comment": "SettingsEngineSection.swift:319", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ In use: %@, " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ In uso: %@, " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ En cours d’utilisation : %@, " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ In Verwendung: %@, " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ En uso: %@, " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ Em uso: %@, " + } + } + } + }, + "%@ MB": { + "comment": "InstallProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ MB" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ MB" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ Mo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ MB" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ MB" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ MB" + } + } + } + }, + "%@ Some items were kept: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ Some items were kept: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ Alcuni elementi sono stati conservati: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ Certains éléments ont été conservés : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Einige Objekte bleiben erhalten: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ Se han conservado algunos elementos: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ Alguns itens foram mantidos: %@" + } + } + } + }, + "%@ The installed app and saves were kept.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ The installed app and saves were kept." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ L’app installata e i salvataggi sono stati conservati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ L’app installée et les sauvegardes ont été conservées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Die installierte App und die Spielstände bleiben erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ Se han conservado la app instalada y las partidas guardadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ O app instalado e os dados salvos foram mantidos." + } + } + } + }, + "%@ The installed app and saves were kept. Rebuilds stay disabled until you attach the sources again.": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ The installed app and saves were kept. Rebuilds stay disabled until you attach the sources again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ L’app installata e i salvataggi sono stati conservati. Non potrai ricompilare finché non colleghi di nuovo le sorgenti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ L’app installée et les sauvegardes ont été conservées. La recompilation reste désactivée jusqu’à ce que vous rattachiez les sources." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Die installierte App und Spielstände wurden behalten. Neue Builds sind erst wieder möglich, wenn du die Quelldateien erneut verknüpfst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ Se conservaron la app instalada y las partidas guardadas. No podrás recompilar hasta que vuelvas a vincular los archivos de origen." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ O app instalado e os saves foram mantidos. Novas compilações ficam desativadas até você vincular os arquivos de origem novamente." + } + } + } + }, + "%@ and %@ change the same files: the upper one wins.": { + "comment": "ModStore.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ and %@ change the same files: the upper one wins." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ e %@ modificano gli stessi file: prevale quella più in alto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ et %@ modifient les mêmes fichiers : celui du dessus est prioritaire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ und %@ ändern dieselben Dateien: Der obere Mod hat Vorrang." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ y %@ modifican los mismos archivos: el de arriba tiene prioridad." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ e %@ alteram os mesmos arquivos: o de cima tem prioridade." + } + } + } + }, + "%@ finished with a warning": { + "comment": "BuildWarningCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ finished with a warning" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ completato con un avviso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ s’est terminé avec un avertissement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ mit Warnung abgeschlossen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ ha terminado con una advertencia" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ terminou com um aviso" + } + } + } + }, + "%@ games": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ games" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ jogos" + } + } + } + }, + "%@ goes to the Trash, so you can bring it back from there, or download it again whenever you like.": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ goes to the Trash, so you can bring it back from there, or download it again whenever you like." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ viene spostato nel Cestino: puoi ripristinarlo da lì o scaricarlo di nuovo quando vuoi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est placé dans la corbeille ; vous pourrez l’y récupérer ou le télécharger à nouveau quand vous le souhaitez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ wird in den Papierkorb gelegt. Du kannst es von dort zurückholen oder jederzeit erneut laden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ se mueve a la Papelera; puedes recuperarlo desde allí o volver a descargarlo cuando quieras." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ vai para o Lixo. Você poderá restaurá-lo de lá ou baixá-lo novamente quando quiser." + } + } + } + }, + "%@ h %@ min": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Std. %@ Min." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + } + } + }, + "%@ has an unsupported internal table size.": { + "comment": "SourceCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ has an unsupported internal table size." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ ha una dimensione della tabella interna non supportata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ contient une table interne dont la taille n’est pas prise en charge." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ hat eine nicht unterstützte interne Tabellengröße." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ tiene un tamaño de tabla interna no compatible." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ tem um tamanho de tabela interna incompatível." + } + } + } + }, + "%@ has changed files. NX Library never touches them: put them aside, then check again.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ has changed files. NX Library never touches them: put them aside, then check again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ contiene file modificati. NX Library non li tocca mai: mettili da parte, poi ricontrolla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ contient des fichiers modifiés. NX Library n’y touche jamais : mettez-les de côté, puis vérifiez à nouveau." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ enthält geänderte Dateien. NX Library verändert sie niemals: Lege sie beiseite und prüfe erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ contiene archivos modificados. NX Library nunca los modifica: apártalos y vuelve a comprobar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ tem arquivos alterados. O NX Library nunca os modifica: guarde-os em outro local e verifique novamente." + } + } + } + }, + "%@ has no %@ called “%@”": { + "comment": "ChangeArtworkView.swift:297", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ has no %@ called “%@”" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@: nessun risultato di tipo %@ con il nome “%@”" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ ne propose aucun résultat de type %@ nommé « %@ »" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@: %@ mit dem Namen „%@“ nicht gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@: no se encuentra %@ con el nombre «%@»" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ não tem %@ com o nome “%@”" + } + } + } + }, + "%@ has no %@ for “%@”": { + "comment": "ChangeArtworkView.swift:301", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ has no %@ for “%@”" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@: %@ non disponibile per “%@”" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ ne propose aucun élément de type %@ pour « %@ »" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@: %@ für „%@“ nicht gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ no tiene %@ para «%@»" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ não tem %@ para “%@”" + } + } + } + }, + "%@ has no %@, the language set in its Game options.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ has no %@, the language set in its Game options." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ non include %@, la lingua impostata nelle sue Opzioni del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ ne propose pas %@, la langue définie dans ses Options du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ bietet %@ nicht an, die in seinen Spieloptionen gewählte Sprache." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ no incluye %@, el idioma seleccionado en sus opciones del juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ não tem %@, o idioma definido nas Opções do jogo." + } + } + } + }, + "%@ has no verified nxnative game profile%@; the pinned engine does not support it yet.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ has no verified nxnative game profile%@; the pinned engine does not support it yet." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ non ha un profilo di gioco nxnative verificato%@; il motore fissato non lo supporta ancora." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ n’a pas de profil de jeu nxnative vérifié%@ ; le moteur épinglé ne le prend pas encore en charge." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für %@ gibt es kein verifiziertes nxnative-Spielprofil%@; die festgelegte Engine unterstützt es noch nicht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ no tiene un perfil de juego de nxnative verificado%@; la versión fijada del motor aún no es compatible." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ não tem um perfil de jogo nxnative verificado%@; a engine fixada ainda não é compatível com ele." + } + } + } + }, + "%@ hr": { + "comment": "PortingPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ hr" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ h" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ h" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Std." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ h" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ h" + } + } + } + }, + "%@ hr %@ min": { + "comment": "PortingPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ hr %@ min" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Std. %@ Min." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + } + } + }, + "%@ installed. Microsoft has %@; A installs it, and macOS asks for your password.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ installed. Microsoft has %@; A installs it, and macOS asks for your password." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ installato. Microsoft offre %@; A lo installa e macOS chiede la tua password." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ installé. Microsoft propose %@ ; A l’installe, et macOS vous demande votre mot de passe." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ installiert. Microsoft bietet %@; A installiert es und macOS fragt nach deinem Passwort." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ instalado. Microsoft ofrece %@; A lo instala y macOS pide tu contraseña." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ instalado. A Microsoft tem %@; A inicia a instalação, e o macOS pede sua senha." + } + } + } + }, + "%@ is a 32-bit ARM program, and nxnative runs only 64-bit games.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is a 32-bit ARM program, and nxnative runs only 64-bit games." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ è un programma ARM a 32 bit e nxnative esegue solo giochi a 64 bit." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est un programme ARM 32 bits et nxnative exécute uniquement les jeux 64 bits." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ ist ein 32-Bit-ARM-Programm; nxnative führt nur 64-Bit-Spiele aus." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ es un programa ARM de 32 bits y nxnative solo ejecuta juegos de 64 bits." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ é um programa ARM de 32 bits, e o nxnative executa apenas jogos de 64 bits." + } + } + } + }, + "%@ is a folder": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is a folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ è una cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est un dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ ist ein Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ es una carpeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ é uma pasta" + } + } + } + }, + "%@ is also used by another library entry": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is also used by another library entry" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ è usato anche da un’altra voce della libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est aussi utilisé par une autre entrée de la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ wird auch von einem anderen Bibliothekseintrag verwendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ también lo usa otra entrada de la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ também é usado por outra entrada da biblioteca" + } + } + } + }, + "%@ is available": { + "comment": "SettingsAboutSection.swift:227", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is available" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disponibile: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est disponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ ist verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ disponível" + } + } + } + }, + "%@ is available · %@": { + "comment": "SettingsAboutSection.swift:227", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is available · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disponibile: %@ · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est disponible · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ ist verfügbar · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ disponible · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ disponível · %@" + } + } + } + }, + "%@ is inside the app": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is inside the app" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ si trova dentro l’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ se trouve dans l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ befindet sich innerhalb der App" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ está dentro de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ está dentro do app" + } + } + } + }, + "%@ is not a git checkout of nxnative. Move it away, then check again.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is not a git checkout of nxnative. Move it away, then check again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ non è un checkout git di nxnative. Spostalo altrove, poi ricontrolla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ n’est pas une copie de travail git de nxnative. Déplacez-le ailleurs, puis vérifiez à nouveau." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ ist kein git-Checkout von nxnative. Verschiebe es an einen anderen Ort und prüfe erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ no es una copia local git de nxnative. Muévela a otra ubicación y vuelve a comprobar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ não é um checkout git do nxnative. Mova-o para outro local e verifique novamente." + } + } + } + }, + "%@ is open, so the new app waits beside it and replaces it as soon as you quit the game.": { + "comment": "PendingInstall.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is open, so the new app waits beside it and replaces it as soon as you quit the game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ è aperto, quindi la nuova app resta accanto a quella attuale e la sostituisce appena chiudi il gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est ouvert ; la nouvelle app attend donc à côté et la remplacera dès que vous quitterez le jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ ist geöffnet. Die neue App wartet daneben und ersetzt sie, sobald du das Spiel beendest." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ está abierto, así que la nueva app espera junto a él y lo reemplaza en cuanto cierres el juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ está aberto, então o novo app aguarda ao lado dele e o substitui assim que você encerrar o jogo." + } + } + } + }, + "%@ is running. Quit it to change its mods.": { + "comment": "ModsPanel.swift:393", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is running. Quit it to change its mods." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ è in esecuzione. Chiudilo per modificarne le mod." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est en cours d’exécution. Quittez-le pour modifier ses mods." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ läuft. Beende das Spiel, um seine Mods zu ändern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ está en ejecución. Ciérralo para cambiar sus mods." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ está em execução. Encerre o jogo para alterar seus mods." + } + } + } + }, + "%@ is running: change cheats in its Cheats panel (⌘⇧C).": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ is running: change cheats in its Cheats panel (⌘⇧C)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ è in esecuzione: modifica i trucchi nel suo pannello Trucchi (⌘⇧C)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ est en cours d’exécution : modifiez les codes de triche dans son panneau Codes de triche (⌘⇧C)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ läuft: Ändere Cheats im Cheats-Bereich des Spiels (⌘⇧C)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ está en ejecución: cambia los trucos en su panel de trucos (⌘⇧C)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ está em execução: altere as trapaças no painel Trapaças do jogo (⌘⇧C)." + } + } + } + }, + "%@ left": { + "comment": "SetupCheckView.swift:225", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ left" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Da completare: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reste : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ übrig" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pendiente: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pendentes: %@" + } + } + } + }, + "%@ likes · %@ downloads": { + "comment": "ModDetailPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ likes · %@ downloads" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ Mi piace · %@ download" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ mentions J’aime · %@ téléchargements" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Likes · %@ Downloads" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ me gusta · %@ descargas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ curtidas · %@ downloads" + } + } + } + }, + "%@ lines": { + "comment": "BuildLiveLogsDrawer.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ lines" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ righe" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ lignes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Zeilen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ líneas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ linhas" + } + } + } + }, + "%@ min": { + "comment": "PortingPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Min." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + } + } + }, + "%@ mods": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ mods" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ mods" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Mods" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ mods" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ mods" + } + } + } + }, + "%@ mods, all off": { + "comment": "ModsPanel.swift:388", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ mods, all off" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ mod, tutte disattivate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ mods, tous désactivés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Mods, alle deaktiviert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ mods, todos desactivados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ mods, todos desativados" + } + } + } + }, + "%@ needs keys newer than the ones you imported.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ needs keys newer than the ones you imported." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ richiede chiavi più recenti di quelle importate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ nécessite des clés plus récentes que celles importées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ benötigt neuere Schlüssel als die importierten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ necesita claves más recientes que las importadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ precisa de chaves mais recentes que as importadas." + } + } + } + }, + "%@ of %@ MB": { + "comment": "InstallProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ of %@ MB" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ di %@ MB" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ sur %@ Mo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ von %@ MB" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ de %@ MB" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ de %@ MB" + } + } + } + }, + "%@ of %@ on · the upper mod wins when two change the same file": { + "comment": "ModsPanel.swift:389", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ of %@ on · the upper mod wins when two change the same file" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ di %@ attive · se due mod modificano lo stesso file, prevale quella più in alto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ sur %@ activés · le mod du dessus est prioritaire si deux mods modifient le même fichier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ von %@ aktiv · Wenn zwei Mods dieselbe Datei ändern, hat der obere Vorrang" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ de %@ activados · el mod de arriba tiene prioridad si dos modifican el mismo archivo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ de %@ ativados · o mod de cima tem prioridade quando dois alteram o mesmo arquivo" + } + } + } + }, + "%@ of %@ ready": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ of %@ ready" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ di %@ pronti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ sur %@ prêts" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ von %@ bereit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ de %@ listos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ de %@ prontos" + } + } + } + }, + "%@ of the tested %@.": { + "comment": "SettingsEngineSection.swift:320", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ of the tested %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ rispetto al commit testato %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ par rapport au commit testé %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ (getestet: %@)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ de la versión probada %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ em relação à versão testada %@." + } + } + } + }, + "%@ showed no window within 60 s; launch screen closed.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ showed no window within 60 s; launch screen closed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ non ha mostrato alcuna finestra entro 60 s; schermata di avvio chiusa." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ n’a affiché aucune fenêtre en 60 s ; l’écran de lancement a été fermé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ hat innerhalb von 60 s kein Fenster angezeigt; der Startbildschirm wurde geschlossen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ no ha mostrado ninguna ventana en 60 s; se ha cerrado la pantalla de inicio." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ não exibiu nenhuma janela em 60 s; a tela de abertura foi fechada." + } + } + } + }, + "%@ to go": { + "comment": "SetupCheckView.swift:213", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ to go" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Da completare: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reste : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Por completar: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Falta concluir: %@" + } + } + } + }, + "%@ · BROWSE ONLINE": { + "comment": "ChangeArtworkView.swift:122", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ · BROWSE ONLINE" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ · SFOGLIA ONLINE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · PARCOURIR EN LIGNE" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ · ONLINE STÖBERN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ · EXPLORAR EN LÍNEA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ · BUSCAR ON-LINE" + } + } + } + }, + "%@ · Open log": { + "comment": "SpotlightInfoView.swift:266", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ · Open log" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ · Apri log" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · Ouvrir le journal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ · Protokoll öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ · Abrir registro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ · Abrir log" + } + } + } + }, + "%@ · “%@” · full size downloads when you choose": { + "comment": "ChangeArtworkView.swift:303", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@ · “%@” · full size downloads when you choose" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ · “%@” · l’immagine completa viene scaricata quando scegli" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ · « %@ » · téléchargement en taille réelle à la sélection" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ · „%@“ · Vollgröße wird bei der Auswahl geladen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ · «%@» · la imagen a tamaño completo se descarga al elegirla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ · “%@” · a imagem em tamanho original será baixada ao escolher" + } + } + } + }, + "%@%% · Download continues if you close this dialog.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@%% · Download continues if you close this dialog." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@%% · Il download continua anche se chiudi questa finestra." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@%% · Le téléchargement continue si vous fermez cette boîte de dialogue." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@%% · Der Download läuft weiter, wenn du diesen Dialog schließt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@%% · La descarga continúa si cierras este diálogo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@%% · O download continua se você fechar esta caixa de diálogo." + } + } + } + }, + "%@'s files are on %@ that is not connected.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@'s files are on %@ that is not connected." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file di %@ si trovano su %@, che non è collegato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers de %@ se trouvent sur %@, qui n’est pas connecté." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Dateien von %@ liegen auf %@, das nicht verbunden ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos de %@ están en %@, que no está conectado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos de %@ estão em %@, que não está conectado." + } + } + } + }, + "%@'s package is cut off or its own tables fail their hash check: an incomplete copy or a broken download.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@'s package is cut off or its own tables fail their hash check: an incomplete copy or a broken download." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il pacchetto di %@ è troncato o le sue tabelle non superano la verifica dell’hash: una copia incompleta o un download danneggiato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le paquet de %@ est tronqué ou ses tables internes échouent à la vérification d’empreinte : copie incomplète ou téléchargement endommagé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Paket von %@ ist abgeschnitten oder seine eigenen Tabellen bestehen die Hash-Prüfung nicht: eine unvollständige Kopie oder ein fehlerhafter Download." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El paquete de %@ está truncado o sus tablas internas no superan la comprobación de hash: una copia incompleta o una descarga dañada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O pacote de %@ está truncado ou suas próprias tabelas falham na verificação de hash: uma cópia incompleta ou um download corrompido." + } + } + } + }, + "%@, %@ by %@": { + "comment": "OnlineBrowsePanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@, %@ by %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@, %@ di %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@, %@ par %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@, %@ von %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@, %@ de %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@, %@ por %@" + } + } + } + }, + "%@, by %@%@": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@, by %@%@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@, di %@%@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@, par %@%@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@, von %@%@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@, de %@%@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@, por %@%@" + } + } + } + }, + "%@, for %@, by %@": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@, for %@, by %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@, per %@, di %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@, pour %@, par %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@, für %@, von %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@, para %@, de %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@, para %@, por %@" + } + } + } + }, + "%@. Reinstall NX Library from its DMG.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@. Reinstall NX Library from its DMG." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@. Reinstalla NX Library dal suo DMG." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@. Réinstallez NX Library depuis son DMG." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@. Installiere NX Library erneut aus der DMG-Datei." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@. Reinstala NX Library desde su DMG." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@. Reinstale o NX Library a partir do DMG." + } + } + } + }, + "%@. You can change both wallpapers any time in Settings → Look.": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@. You can change both wallpapers any time in Settings → Look." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@. Puoi cambiare entrambi gli sfondi in qualsiasi momento in Impostazioni → Aspetto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@. Vous pouvez changer les deux fonds d’écran à tout moment dans Réglages → Apparence." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@. Du kannst beide Hintergrundbilder jederzeit unter Einstellungen → Erscheinungsbild ändern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@. Puedes cambiar ambos fondos cuando quieras en Ajustes → Apariencia." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@. Você pode alterar ambos os papéis de parede a qualquer momento em Ajustes → Aparência." + } + } + } + }, + "%@d ago": { + "comment": "LibraryPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@d ago" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ g fa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %@ j" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "vor %@ Tagen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "hace %@ d" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "há %@ d" + } + } + } + }, + "%@h %@m": { + "comment": "Compact playtime units, metadata heading or accessible duration.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@h %@m" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Std. %@ Min." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ h %@ min" + } + } + } + }, + "%@h ago": { + "comment": "LibraryPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@h ago" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ h fa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %@ h" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "vor %@ Std." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "hace %@ h" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "há %@ h" + } + } + } + }, + "%@m": { + "comment": "Compact playtime units, metadata heading or accessible duration.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@m" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Min." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ min" + } + } + } + }, + "%@m ago": { + "comment": "LibraryPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "%@m ago" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ min fa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "il y a %@ min" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "vor %@ Min." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "hace %@ min" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "há %@ min" + } + } + } + }, + "%lld commits ahead": { + "comment": "Quantity-sensitive UI message; preserve %lld count and other placeholders.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld commit ahead" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld commits ahead" + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld commit avanti" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld commit avanti" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld commit d’avance" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld commits d’avance" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Commit voraus" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Commits voraus" + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld commit por delante" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld commits por delante" + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld commit à frente" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld commits à frente" + } + } + } + } + } + } + }, + "%lld files": { + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld file" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld files" + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld file" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld file" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld fichier" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld fichiers" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Datei" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Dateien" + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld archivo" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld archivos" + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld arquivo" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld arquivos" + } + } + } + } + } + } + }, + "%lld games found.": { + "comment": "Quantity-sensitive UI message; preserve %lld count and other placeholders.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld game found." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld games found." + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld gioco trovato." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld giochi trovati." + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld jeu trouvé." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld jeux trouvés." + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Spiel gefunden." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Spiele gefunden." + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Se ha encontrado %lld juego." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Se han encontrado %lld juegos." + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld jogo encontrado." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld jogos encontrados." + } + } + } + } + } + } + }, + "%lld lines": { + "comment": "Actual quantity. Preserve %lld; native catalog supplies singular/plural.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld line" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld lines" + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld riga" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld righe" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld ligne" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld lignes" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Zeile" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Zeilen" + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld línea" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld líneas" + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld linha" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld linhas" + } + } + } + } + } + } + }, + "%lld original source files (%@)": { + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld original source file (%@)" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld original source files (%@)" + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld file sorgente originale (%@)" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld file sorgenti originali (%@)" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld fichier source d’origine (%@)" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld fichiers sources d’origine (%@)" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld ursprüngliche Quelldatei (%@)" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld ursprüngliche Quelldateien (%@)" + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld archivo de origen original (%@)" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld archivos de origen originales (%@)" + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld arquivo de origem original (%@)" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld arquivos de origem originais (%@)" + } + } + } + } + } + } + }, + "%lld sources": { + "comment": "Actual quantity. Preserve %lld; native catalog supplies singular/plural.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld source" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld sources" + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld sorgente" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld sorgenti" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld source" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld sources" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Quelle" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Quellen" + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld archivo de origen" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld archivos de origen" + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld arquivo de origem" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld arquivos de origem" + } + } + } + } + } + } + }, + "+ %@ more commits upstream": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "+ %@ more commits upstream" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "+ altri %@ commit upstream" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "+ %@ autres commits dans le dépôt amont" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "+ %@ weitere Commits im Upstream" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "+ %@ commits más en el proyecto original" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "+ %@ commits no repositório original" + } + } + } + }, + ", %lld commits newer": { + "comment": "Quantity-sensitive UI message; preserve %lld count and other placeholders.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": ", %lld commit newer" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": ", %lld commits newer" + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": ", %lld commit più recente" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": ", %lld commit più recenti" + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": ", %lld commit plus récent" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": ", %lld commits plus récents" + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": ", %lld Commit neuer" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": ", %lld Commits neuer" + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": ", %lld commit más reciente" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": ", %lld commits más recientes" + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": ", %lld commit mais recente" + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": ", %lld commits mais recentes" + } + } + } + } + } + } + }, + ", or Switch 2 NUP files and extracted folders": { + "comment": "GameFoldersCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": ", or Switch 2 NUP files and extracted folders" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": ", oppure file NUP di Switch 2 e cartelle estratte" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": ", ou des fichiers NUP Switch 2 et des dossiers extraits" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": ", oder Switch 2-NUP-Dateien und entpackte Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": ", o archivos NUP de Switch 2 y carpetas extraídas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": ", ou arquivos NUP de Switch 2 e pastas extraídas" + } + } + } + }, + ".NET %@ SDK installer opened.": { + "comment": "SetupCheckModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": ".NET %@ SDK installer opened." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Programma di installazione dell’SDK .NET %@ aperto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le programme d’installation du SDK .NET %@ a été ouvert." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installationsprogramm für .NET %@ SDK geöffnet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha abierto el instalador del SDK de .NET %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalador do SDK do .NET %@ aberto." + } + } + } + }, + ".NET 10 SDK": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": ".NET 10 SDK" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "SDK .NET 10" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SDK .NET 10" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": ".NET 10 SDK" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "SDK de .NET 10" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "SDK do .NET 10" + } + } + } + }, + ".NET SDK: %@": { + "comment": "SetupCheckModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": ".NET SDK: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "SDK .NET: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SDK .NET : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": ".NET SDK: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "SDK de .NET: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "SDK do .NET: %@" + } + } + } + }, + "1 game": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "1 game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "1 gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "1 Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "1 juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "1 jogo" + } + } + } + }, + "1 mod": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "1 mod" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "1 mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "1 mod" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "1 Mod" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "1 mod" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "1 mod" + } + } + } + }, + "120 fps": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "120 fps" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "120 fps" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "120 FPS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "120 FPS" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "120 fps" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "120 fps" + } + } + } + }, + "16:9 (Switch)": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "16:9 (Switch)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "16:9 (Switch)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "16:9 (Switch)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "16:9 (Switch)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "16:9 (Switch)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "16:9 (Switch)" + } + } + } + }, + "30 fps": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "30 fps" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "30 fps" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "30 FPS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "30 FPS" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "30 fps" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "30 fps" + } + } + } + }, + "3D render scale": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "3D render scale" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scala di rendering 3D" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échelle de rendu 3D" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "3D-Render-Skalierung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Escala de renderizado 3D" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escala de renderização 3D" + } + } + } + }, + "4K": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "4K" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "4K" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "4K" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "4K" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "4K" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "4K" + } + } + } + }, + "60 FPS": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "60 FPS" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "60 FPS" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "60 FPS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "60 FPS" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "60 FPS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "60 FPS" + } + } + } + }, + "60 fps": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "60 fps" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "60 fps" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "60 FPS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "60 FPS" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "60 fps" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "60 fps" + } + } + } + }, + "; %@ not found in that folder.": { + "comment": "SettingsSoundSection.swift:76", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "; %@ not found in that folder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "; elementi non trovati nella cartella: %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "; %@ introuvables dans ce dossier." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "; %@ in diesem Ordner nicht gefunden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "; no se ha encontrado %@ en esa carpeta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "; faltando nessa pasta: %@." + } + } + } + }, + "A .txt cheat file, or a folder in SD-card layout. You can also drop them here.": { + "comment": "CheatsPanel.swift:291", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A .txt cheat file, or a folder in SD-card layout. You can also drop them here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un file di trucchi .txt oppure una cartella con la struttura di una scheda SD. Puoi anche trascinarli qui." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un fichier de codes de triche .txt, ou un dossier organisé comme une carte SD. Vous pouvez aussi les déposer ici." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eine .txt-Cheat-Datei oder ein Ordner mit SD-Kartenstruktur. Du kannst sie auch hier ablegen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Un archivo de trucos .txt o una carpeta con la estructura de una tarjeta SD. También puedes arrastrarlos aquí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Um arquivo de trapaças .txt ou uma pasta com a estrutura de um cartão SD. Você também pode soltá-los aqui." + } + } + } + }, + "A Python package is missing": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A Python package is missing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Manca un pacchetto Python" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un paquet Python est manquant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Python-Paket fehlt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta un paquete de Python" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Falta um pacote Python" + } + } + } + }, + "A build is already running.": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A build is already running." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "È già in corso una compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une compilation est déjà en cours." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Build läuft bereits." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ya hay una compilación en curso." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Já há uma compilação em andamento." + } + } + } + }, + "A cloud save download did not finish. Play the game once so it can put this Mac's save back, then try again.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A cloud save download did not finish. Play the game once so it can put this Mac's save back, then try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un download del salvataggio dal cloud non è terminato. Avvia il gioco una volta perché rimetta a posto il salvataggio di questo Mac, poi riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un téléchargement de la sauvegarde depuis le cloud ne s’est pas terminé. Lancez le jeu une fois pour qu’il remette en place la sauvegarde de ce Mac, puis réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Download des Cloud-Spielstands wurde nicht abgeschlossen. Starte das Spiel einmal, damit es den Spielstand dieses Macs wiederherstellt, und versuche es dann erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Una descarga de la partida guardada desde la nube no terminó. Inicia el juego una vez para que restaure la partida de este Mac y vuelve a intentarlo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Um download do salvamento da nuvem não terminou. Abra o jogo uma vez para que ele restaure o salvamento deste Mac e tente de novo." + } + } + } + }, + "A drive is not connected": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A drive is not connected" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un’unità non è collegata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un disque n’est pas connecté" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Laufwerk ist nicht verbunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Hay una unidad desconectada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Uma unidade não está conectada" + } + } + } + }, + "A notification when a porting finishes while NX Library is in the background.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A notification when a porting finishes while NX Library is in the background." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Una notifica quando la creazione dell’app per Mac termina mentre NX Library è in background." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une notification quand la création d’une app Mac se termine alors que NX Library est en arrière-plan." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eine Mitteilung, wenn die App-Erstellung endet, während NX Library im Hintergrund ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Envía una notificación cuando termina una compilación mientras NX Library está en segundo plano." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Uma notificação quando uma compilação terminar enquanto o NX Library estiver em segundo plano." + } + } + } + }, + "A personal macOS front-end for building and launching your own Switch games with nxnative.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A personal macOS front-end for building and launching your own Switch games with nxnative." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un’interfaccia personale per macOS per creare e avviare i tuoi giochi Switch con nxnative." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une interface macOS personnelle pour compiler et lancer vos propres jeux Switch avec nxnative." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eine persönliche macOS-Oberfläche zum Erstellen und Starten deiner eigenen Switch-Spiele als Apps mit nxnative." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Una interfaz personal para macOS que permite compilar y ejecutar tus propios juegos de Switch con nxnative." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Uma interface pessoal para macOS que compila e executa seus próprios jogos de Switch com o nxnative." + } + } + } + }, + "A report for an issue: versions, setup and the last porting. No keys.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A report for an issue: versions, setup and the last porting. No keys." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un rapporto per segnalare un problema: versioni, configurazione e ultima creazione dell’app per Mac. Nessuna chiave." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un rapport pour signaler un problème : versions, configuration et dernière création d’app Mac. Aucune clé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Bericht zur Fehlerbehebung: Versionen, Einrichtung und letzte App-Erstellung. Keine Schlüssel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Un informe para comunicar un problema: versiones, configuración y última compilación. Sin claves." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Um relatório para relatar um problema: versões, configuração e a última compilação. Sem chaves." + } + } + } + }, + "A save from a newer game version may not load. Cloud saves may replace this local save if you turn sync on later.": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A save from a newer game version may not load. Cloud saves may replace this local save if you turn sync on later." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un salvataggio creato con una versione più recente del gioco potrebbe non caricarsi. Se attivi la sincronizzazione in seguito, i salvataggi cloud potrebbero sostituire quello locale." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une sauvegarde créée avec une version plus récente du jeu peut ne pas se charger. Les sauvegardes cloud peuvent remplacer ces données locales si vous activez la synchronisation plus tard." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Spielstand aus einer neueren Spielversion lässt sich möglicherweise nicht laden. Wenn du die Synchronisierung später aktivierst, können Cloud-Spielstände diesen lokalen Spielstand ersetzen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Una partida de una versión más reciente del juego podría no cargarse. Si activas la sincronización más adelante, las partidas de la nube podrían reemplazar esta partida local." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Um salvamento de uma versão mais recente do jogo pode não carregar. Se você ativar a sincronização depois, os salvamentos na nuvem poderão substituir este salvamento local." + } + } + } + }, + "A save operation or app installation is in progress for this game.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A save operation or app installation is in progress for this game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "È in corso un’operazione sui salvataggi o un’installazione dell’app per questo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une opération de sauvegarde ou une installation d’app est en cours pour ce jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für dieses Spiel läuft gerade ein Spielstandvorgang oder eine App-Installation." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Hay una operación de partidas guardadas o una instalación de la app en curso para este juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Há uma operação de dados salvos ou uma instalação do app em andamento para este jogo." + } + } + } + }, + "A slow drift of the art while the game starts.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "A slow drift of the art while the game starts." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un lento movimento dell’immagine durante l’avvio del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un lent mouvement de l’illustration pendant le démarrage du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Langsame Bewegung des Artworks während des Spielstarts." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Un movimiento lento de las imágenes mientras se inicia el juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Um movimento lento da arte enquanto o jogo inicia." + } + } + } + }, + "ADD A GAME": { + "comment": "AddGameView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ADD A GAME" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "AGGIUNGI UN GIOCO" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "AJOUTER UN JEU" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SPIEL HINZUFÜGEN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "AÑADIR UN JUEGO" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ADICIONAR JOGO" + } + } + } + }, + "API key": { + "comment": "SteamGridDBKeyDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "API key" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiave API" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé API" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "API-Schlüssel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clave de API" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chave de API" + } + } + } + }, + "Aa": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Aa" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aa" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aa" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aa" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aa" + } + } + } + }, + "About": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "About" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Informazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À propos" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Über" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Acerca de" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sobre" + } + } + } + }, + "About this game": { + "comment": "SpotlightPageSections.swift:78", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "About this game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Informazioni sul gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À propos de ce jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Über dieses Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Acerca de este juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sobre este jogo" + } + } + } + }, + "Abstract": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Abstract" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Astratti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Abstrait" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Abstrakt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abstracto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abstrato" + } + } + } + }, + "Activity": { + "comment": "SpotlightCardInfoView.swift:50", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Activity" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attività" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activité" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktivität" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actividad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atividade" + } + } + } + }, + "Add": { + "comment": "CheatsPanel.swift:408", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar" + } + } + } + }, + "Add Game": { + "comment": "SpotlightRow.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add Game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar jogo" + } + } + } + }, + "Add Game…": { + "comment": "NXLibraryApp.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add Game…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi gioco…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un jeu…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel hinzufügen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir juego…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar jogo…" + } + } + } + }, + "Add a game": { + "comment": "SpotlightCardInfoView.swift:52", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add a game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi un gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir un juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar um jogo" + } + } + } + }, + "Add a game to see its storage here.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add a game to see its storage here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi un gioco per vederne qui lo spazio occupato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoutez un jeu pour voir son stockage ici." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Füge ein Spiel hinzu, um seinen Speicherbedarf hier zu sehen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade un juego para ver aquí su almacenamiento." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicione um jogo para ver seu armazenamento aqui." + } + } + } + }, + "Add a game to start your collection.": { + "comment": "SpotlightRowView.swift:196", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add a game to start your collection." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi un gioco per iniziare la tua raccolta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoutez un jeu pour commencer votre collection." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Füge ein Spiel hinzu, um deine Sammlung zu beginnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade un juego para empezar tu colección." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicione um jogo para começar sua coleção." + } + } + } + }, + "Add and port": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add and port" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi e crea app per Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter et créer l’app Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hinzufügen und als App erstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir y compilar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar e compilar" + } + } + } + }, + "Add cheats for %@": { + "comment": "CheatsPanel.swift:407", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add cheats for %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi trucchi per %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter des codes de triche pour %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cheats für %@ hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir trucos para %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar trapaças para %@" + } + } + } + }, + "Add cheats…": { + "comment": "CheatsPanel.swift:291", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add cheats…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi trucchi…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter des codes de triche…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cheats hinzufügen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir trucos…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar trapaças…" + } + } + } + }, + "Add game": { + "comment": "CollectionView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter un jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar jogo" + } + } + } + }, + "Add key": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add key" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi chiave" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter une clé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir clave" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar chave" + } + } + } + }, + "Add mods": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add mods" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter des mods" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mods hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir mods" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar mods" + } + } + } + }, + "Add to favorites": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add to favorites" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi ai preferiti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter aux favoris" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zu Favoriten hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir a favoritos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar aos favoritos" + } + } + } + }, + "Add to library only": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add to library only" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi solo alla libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter uniquement à la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur zur Bibliothek hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo añadir a la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apenas adicionar à biblioteca" + } + } + } + }, + "Add your API key to find covers, backgrounds, logos and icons in Change artwork.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add your API key to find covers, backgrounds, logos and icons in Change artwork." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi la tua chiave API per trovare copertine, sfondi, loghi e icone in Cambia grafica." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoutez votre clé API pour trouver des jaquettes, arrière-plans, logos et icônes dans Changer l’illustration." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Füge deinen API-Schlüssel hinzu, um unter „Artwork ändern“ Cover, Hintergründe, Logos und Symbole zu finden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade tu clave de API para buscar portadas, fondos, logotipos e iconos en Cambiar imágenes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicione sua chave de API para encontrar capas, fundos, logotipos e ícones em Alterar arte." + } + } + } + }, + "Add your first game": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add your first game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi il tuo primo gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoutez votre premier jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein erstes Spiel hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade tu primer juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicione seu primeiro jogo" + } + } + } + }, + "Add your free API key to find art in Change artwork": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add your free API key to find art in Change artwork" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi la tua chiave API gratuita per trovare immagini in Cambia grafica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoutez votre clé API gratuite pour trouver des images dans Changer l’illustration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Füge deinen kostenlosen API-Schlüssel hinzu, um unter „Artwork ändern“ Bilder zu finden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade tu clave de API gratuita para buscar imágenes en Cambiar imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicione sua chave de API gratuita para encontrar artes em Alterar arte" + } + } + } + }, + "Add your own": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add your own" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi il tuo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter le vôtre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigenes hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir el tuyo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar imagem própria" + } + } + } + }, + "Add your own…": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add your own…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi il tuo…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter le vôtre…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigenes hinzufügen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir el tuyo…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar imagem própria…" + } + } + } + }, + "Add your video": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add your video" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi il tuo video" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter votre vidéo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigenes Video hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir tu vídeo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar seu vídeo" + } + } + } + }, + "Add your video…": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Add your video…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi il tuo video…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter votre vidéo…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigenes Video hinzufügen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añadir tu vídeo…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar seu vídeo…" + } + } + } + }, + "Added %@ to the library.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Added %@ to the library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ aggiunto alla libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ a été ajouté à la bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ zur Bibliothek hinzugefügt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha añadido %@ a la biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ foi adicionado à biblioteca." + } + } + } + }, + "Adds a soft shadow under text so it reads over bright art.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Adds a soft shadow under text so it reads over bright art." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiunge un’ombra morbida sotto il testo per renderlo leggibile sulle immagini chiare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoute une ombre douce sous le texte pour le rendre lisible sur les illustrations claires." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fügt einen weichen Textschatten hinzu, damit Text auf hellen Bildern lesbar bleibt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade una sombra suave bajo el texto para facilitar su lectura sobre imágenes claras." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adiciona uma sombra suave sob o texto para facilitar a leitura sobre artes claras." + } + } + } + }, + "Adjusts minimum thumbstick deflection needed to trigger menu navigation. Increase if your analog stick has slight drift causing unintended scrolling.": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Adjusts minimum thumbstick deflection needed to trigger menu navigation. Increase if your analog stick has slight drift causing unintended scrolling." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Regola lo spostamento minimo dello stick necessario per navigare nei menu. Aumentalo se una lieve deriva dello stick causa scorrimenti involontari." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Règle l’inclinaison minimale du stick nécessaire pour naviguer dans les menus. Augmentez-la si une légère dérive du stick analogique provoque un défilement involontaire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Legt fest, wie weit du den Stick mindestens bewegen musst, um durch Menüs zu navigieren. Erhöhe den Wert, wenn leichter Stick-Drift unbeabsichtigtes Scrollen verursacht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajusta cuánto debes mover el stick para navegar por los menús. Auméntalo si el stick tiene una ligera deriva que provoca desplazamientos involuntarios." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajusta o movimento mínimo do analógico necessário para navegar pelos menus. Aumente se o analógico apresentar um leve desvio que cause rolagem involuntária." + } + } + } + }, + "After storage cleanup": { + "comment": "AppShellView.swift:515", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "After storage cleanup" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dopo la pulizia dello spazio di archiviazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Après le nettoyage du stockage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach der Speicherbereinigung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Después de la limpieza del almacenamiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Após a limpeza do armazenamento" + } + } + } + }, + "After the game starts": { + "comment": "AppShellView.swift:516", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "After the game starts" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dopo l’avvio del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Après le démarrage du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach dem Spielstart" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Después de iniciar el juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Após o jogo iniciar" + } + } + } + }, + "After the porting": { + "comment": "AppShellView.swift:514", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "After the porting" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dopo la creazione dell’app per Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Après la création de l’app Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach der App-Erstellung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Después de la compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Após a compilação" + } + } + } + }, + "After the save operation": { + "comment": "AppShellView.swift:517", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "After the save operation" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dopo l’operazione sui salvataggi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Après l’opération de sauvegarde" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach dem Spielstandvorgang" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Después de la operación de partidas guardadas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Após a operação de dados salvos" + } + } + } + }, + "After you quit %@": { + "comment": "AppShellView.swift:520", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "After you quit %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dopo la chiusura di %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Après la fermeture de %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nachdem du %@ beendet hast" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Después de cerrar %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Após encerrar %@" + } + } + } + }, + "Ag": { + "comment": "ModDetailPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ag" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ag" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ag" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ag" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ag" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ag" + } + } + } + }, + "All": { + "comment": "ConsoleView.swift:15", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Todos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Todos" + } + } + } + }, + "All %@ mods": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All %@ mods" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutte le %@ mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les %@ mods" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle %@ Mods" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los %@ mods" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Todos os %@ mods" + } + } + } + }, + "All games": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All games" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutti i giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tous les jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Todos los juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Todos os jogos" + } + } + } + }, + "All games · Next launch": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All games · Next launch" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutti i giochi · Prossimo avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tous les jeux · Prochain lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Spiele · Nächster Start" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Todos los juegos · Próximo inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Todos os jogos · Próxima abertura" + } + } + } + }, + "All installed": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All installed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutto installato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout est installé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alles installiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Todo instalado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tudo instalado" + } + } + } + }, + "All set": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "All set" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutto pronto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout est prêt" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alles bereit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Todo listo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tudo pronto" + } + } + } + }, + "Always on": { + "comment": "CheatsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Always on" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sempre attivo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Toujours activé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Immer aktiv" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Siempre activado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sempre ativado" + } + } + } + }, + "Animated profile backgrounds, made by the games' studios for Steam.": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Animated profile backgrounds, made by the games' studios for Steam." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondi animati per il profilo, creati dagli studi dei giochi per Steam." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrière-plans de profil animés, créés par les studios des jeux pour Steam." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Animierte Profilhintergründe, von den Spielestudios für Steam erstellt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondos de perfil animados, creados por los estudios de los juegos para Steam." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fundos de perfil animados, criados pelos estúdios dos jogos para o Steam." + } + } + } + }, + "Anime": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Anime" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Anime" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Anime" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anime" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Anime" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Anime" + } + } + } + }, + "Anime scenery": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Anime scenery" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Paesaggi anime" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Paysages anime" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anime-Landschaften" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Paisajes de anime" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cenários de anime" + } + } + } + }, + "App": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "App" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "App" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "App" + } + } + } + }, + "App & Settings": { + "comment": "WallpaperPlacement.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App & Settings" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App e impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "App et Réglages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App & Einstellungen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "App y Ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "App e Ajustes" + } + } + } + }, + "App background": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App background" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondo dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrière-plan de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Hintergrund" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondo de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fundo do app" + } + } + } + }, + "App folder unavailable": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App folder unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cartella delle app non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossier de l’app indisponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Ordner nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carpeta de apps no disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pasta do app indisponível" + } + } + } + }, + "App icon": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App icon" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Icona dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Icône de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Symbol" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Icono de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ícone do app" + } + } + } + }, + "App icon updated in Finder and the Dock": { + "comment": "AppIconProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App icon updated in Finder and the Dock" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Icona dell’app aggiornata nel Finder e nel Dock" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Icône de l’app mise à jour dans le Finder et le Dock" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Symbol im Finder und im Dock aktualisiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Icono de la app actualizado en el Finder y el Dock" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ícone do app atualizado no Finder e no Dock" + } + } + } + }, + "App language": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App language" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lingua dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Langue de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Sprache" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Idioma de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Idioma do app" + } + } + } + }, + "App name": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App name" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nome dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Name" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nombre de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nome do app" + } + } + } + }, + "App not found": { + "comment": "GameResourceState.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App not found" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App non trovata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "App introuvable" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App nicht gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "App no encontrada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "App não encontrado" + } + } + } + }, + "App updates": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App updates" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiornamenti dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mises à jour de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Updates" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizaciones de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizações do app" + } + } + } + }, + "App: %@ · Cache: %@ · Logs: %@": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "App: %@ · Cache: %@ · Logs: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App: %@ · Cache: %@ · Log: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "App : %@ · Cache : %@ · Journaux : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App: %@ · Cache: %@ · Protokolle: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "App: %@ · Caché: %@ · Registros: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "App: %@ · Cache: %@ · Logs: %@" + } + } + } + }, + "Apple's developer tools. Every game is compiled with them.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Apple's developer tools. Every game is compiled with them." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gli strumenti di sviluppo di Apple. Ogni gioco viene compilato con questi strumenti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les outils de développement d’Apple. Chaque jeu est compilé avec eux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Apples Entwicklerwerkzeuge. Jedes Spiel wird damit kompiliert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las herramientas de desarrollo de Apple. Todos los juegos se compilan con ellas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "As ferramentas de desenvolvimento da Apple. Todos os jogos são compilados com elas." + } + } + } + }, + "Applied before each game starts, including Off. Reset group keeps cloud sync and its folder. Custom save-sync overrides are reported in Saves.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Applied before each game starts, including Off. Reset group keeps cloud sync and its folder. Custom save-sync overrides are reported in Saves." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Applicato prima di ogni avvio, anche se disattivato. Reimposta gruppo conserva la sincronizzazione cloud e la relativa cartella. Le impostazioni personalizzate che prevalgono sulla sincronizzazione dei salvataggi sono indicate in Salvataggi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appliqué avant chaque démarrage de jeu, y compris Désactivé. Réinitialiser le groupe conserve la synchronisation cloud et son dossier. Les réglages personnalisés de synchronisation des sauvegardes sont signalés dans Sauvegardes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird vor jedem Spielstart angewendet, auch „Aus“. „Gruppe zurücksetzen“ behält die Cloud-Synchronisierung und ihren Ordner bei. Eigene Überschreibungen der Spielstandsynchronisierung werden unter „Spielstände“ angezeigt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se aplica antes de iniciar cada juego, también al desactivarlo. Restablecer el grupo conserva la sincronización en la nube y su carpeta. Las personalizaciones de sincronización de partidas se indican en Partidas guardadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aplicado antes de cada jogo iniciar, inclusive Desativado. Redefinir o grupo mantém a sincronização na nuvem e sua pasta. Substituições personalizadas da sincronização são informadas em Dados salvos." + } + } + } + }, + "Applies at the next launch.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Applies at the next launch." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Si applica al prossimo avvio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "S’applique au prochain lancement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gilt beim nächsten Start." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se aplica en el próximo inicio." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aplicado na próxima abertura." + } + } + } + }, + "Apps, build caches and logs for each game, including hidden games.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Apps, build caches and logs for each game, including hidden games." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App, cache di compilazione e log di ogni gioco, inclusi quelli nascosti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apps, caches de compilation et journaux de chaque jeu, y compris les jeux masqués." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Apps, Build-Caches und Protokolle pro Spiel, einschließlich ausgeblendeter Spiele." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Apps, cachés de compilación y registros de cada juego, incluidos los ocultos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apps, caches de compilação e logs de cada jogo, incluindo jogos ocultos." + } + } + } + }, + "Arrow keys": { + "comment": "ControlsView.swift:30", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Arrow keys" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tasti freccia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Touches fléchées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Pfeiltasten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Teclas de dirección" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Teclas de seta" + } + } + } + }, + "Arrows · D-pad or left stick": { + "comment": "ControlsView.swift:46", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Arrows · D-pad or left stick" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Frecce · Croce direzionale o stick sinistro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Flèches · Croix directionnelle ou stick gauche" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Pfeiltasten · Steuerkreuz oder linker Stick" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Flechas · Cruceta o stick izquierdo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Setas · Direcional ou analógico esquerdo" + } + } + } + }, + "Art and logo": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Art and logo" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Immagine e logo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Illustration et logo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Artwork und Logo" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Imagen y logotipo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arte e logotipo" + } + } + } + }, + "Art only": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Art only" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Solo immagine" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Illustration seule" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Artwork" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo imagen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Só arte" + } + } + } + }, + "Artwork": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Artwork" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Immagini" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Illustrations" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Artwork" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arte" + } + } + } + }, + "Artwork cache": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Artwork cache" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cache delle immagini" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cache des illustrations" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Artwork-Cache" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Caché de imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cache de artes" + } + } + } + }, + "Artwork drift": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Artwork drift" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Movimento delle immagini" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mouvement de l’illustration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Artwork-Bewegung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desplazamiento de las imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Movimento da arte" + } + } + } + }, + "Artwork layout": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Artwork layout" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disposizione delle immagini" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Disposition de l’illustration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Artwork-Anordnung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Distribución de imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Disposição da arte" + } + } + } + }, + "Artwork, metadata, store page, sources and the build log.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Artwork, metadata, store page, sources and the build log." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Immagini, metadati, pagina nello store, sorgenti e log di compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Illustrations, métadonnées, page de la boutique, sources et journal de compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Artwork, Metadaten, Shop-Seite, Quellen und Build-Protokoll." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Imágenes, metadatos, página de la tienda, archivos de origen y registro de compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arte, metadados, página na loja, arquivos de origem e log de compilação." + } + } + } + }, + "Ask GameBanana again": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ask GameBanana again" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Interroga di nuovo GameBanana" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Interroger à nouveau GameBanana" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameBanana erneut abfragen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Consultar GameBanana de nuevo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Consultar o GameBanana novamente" + } + } + } + }, + "Asking Apple for Metal %@…": { + "comment": "SetupCheckModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Asking Apple for Metal %@…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Richiesta di Metal %@ ad Apple…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Demande de Metal %@ auprès d’Apple…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Metal %@ wird bei Apple angefragt …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solicitando Metal %@ a Apple…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Solicitando o Metal %@ à Apple…" + } + } + } + }, + "Asking GitHub for the latest commits...": { + "comment": "SettingsEngineSection.swift:258", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Asking GitHub for the latest commits..." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Richiesta degli ultimi commit a GitHub..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Demande des derniers commits auprès de GitHub..." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueste Commits werden bei GitHub angefragt..." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solicitando los últimos commits a GitHub..." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Solicitando os commits mais recentes ao GitHub..." + } + } + } + }, + "Asking Microsoft for the newest .NET %@ SDK…": { + "comment": "SetupCheckModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Asking Microsoft for the newest .NET %@ SDK…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Richiesta dell’SDK .NET %@ più recente a Microsoft…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Demande du dernier SDK .NET %@ auprès de Microsoft…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das neueste .NET %@ SDK wird bei Microsoft angefragt …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solicitando a Microsoft el SDK de .NET %@ más reciente…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Solicitando à Microsoft o SDK do .NET %@ mais recente…" + } + } + } + }, + "At %@, %@ of the tested %@, chosen in Settings → Engine.": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "At %@, %@ of the tested %@, chosen in Settings → Engine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Al commit %@, %@ rispetto al commit testato %@, scelto in Impostazioni → Motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Au commit %@, %@ par rapport au commit testé %@, choisi dans Réglages → Moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Stand %@, %@ (getestet: %@), gewählt unter Einstellungen → Engine." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "En %@, %@ de la versión probada %@, elegido en Ajustes → Motor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Em %@, %@ em relação à versão testada %@, escolhido em Ajustes → Engine." + } + } + } + }, + "Attach sources…": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Attach sources…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Collega sorgenti…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Associer les sources…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen zuordnen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vincular archivos de origen…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Vincular arquivos de origem…" + } + } + } + }, + "Audits the local engine commit, working tree cleanliness, installed Metal compiler, and required CLI flags against the certified toolchain lock.": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Audits the local engine commit, working tree cleanliness, installed Metal compiler, and required CLI flags against the certified toolchain lock." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica il commit locale del motore, l’assenza di modifiche nell’albero di lavoro, il compilatore Metal installato e i flag CLI richiesti rispetto al file di vincoli della toolchain certificata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifie le commit local du moteur, l’absence de modifications dans la copie de travail, le compilateur Metal installé et les options CLI requises par rapport au fichier de verrouillage de la chaîne d’outils certifiée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Prüft den lokalen Engine-Commit, unveränderte Arbeitsdateien, den installierten Metal-Compiler und erforderliche CLI-Optionen anhand der zertifizierten Toolchain-Lockdatei." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprueba el commit del motor local, que no haya cambios en el árbol de trabajo, el compilador Metal instalado y las opciones de CLI necesarias frente al archivo de certificación de la cadena de herramientas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifica o commit local da engine, a ausência de alterações na árvore de trabalho, o compilador Metal instalado e as opções de linha de comando obrigatórias em relação ao arquivo de versões certificadas do conjunto de ferramentas." + } + } + } + }, + "Auto (System)": { + "comment": "AppIconAppearanceMode.swift:4", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Auto (System)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Automatico (sistema)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Automatique (système)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Automatisch (System)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Automático (sistema)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Automático (Sistema)" + } + } + } + }, + "Automatic": { + "comment": "ChangeArtworkView.swift:435", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Automatic" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Automatico" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Automatique" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Automatisch" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Automático" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Automático" + } + } + } + }, + "Automatic (Mac)": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Automatic (Mac)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Automatico (Mac)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Automatique (Mac)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Automatisch (Mac)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Automático (Mac)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Automático (Mac)" + } + } + } + }, + "Automatic follows the connected controller.": { + "comment": "SettingsNavigationSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Automatic follows the connected controller." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Automatico segue il controller connesso." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Automatique suit la manette connectée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Automatisch“ richtet sich nach dem verbundenen Controller." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Automático sigue al mando conectado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Automático segue o controle conectado." + } + } + } + }, + "Available after the active build finishes or is cancelled.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Available after the active build finishes or is cancelled." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disponibile quando la compilazione in corso termina o viene annullata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Disponible une fois la compilation en cours terminée ou annulée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verfügbar, sobald der aktive Build abgeschlossen oder abgebrochen ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Disponible cuando termine o se cancele la compilación activa." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Disponível após o término ou cancelamento da compilação em andamento." + } + } + } + }, + "Available when the compatibility gate blocks a build.": { + "comment": "SettingsEngineSection.swift:228", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Available when the compatibility gate blocks a build." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disponibile quando il controllo di compatibilità blocca una compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Disponible lorsque le contrôle de compatibilité bloque une compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verfügbar, wenn die Kompatibilitätsprüfung einen Build sperrt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Disponible cuando el control de compatibilidad bloquea una compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Disponível quando a verificação de compatibilidade bloqueia uma compilação." + } + } + } + }, + "Back": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Back" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Indietro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Atrás" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Voltar" + } + } + } + }, + "Back to review": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to review" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Torna al riepilogo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour à la vérification" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück zur Überprüfung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volver a la revisión" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Voltar à revisão" + } + } + } + }, + "Back to tested": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to tested" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Torna alla versione testata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour au moteur testé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zur getesteten Version" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volver al probado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Voltar à versão testada" + } + } + } + }, + "Back to tested returns to the tested engine; games you built keep playing.": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to tested returns to the tested engine; games you built keep playing." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Con Torna alla versione testata ripristini il motore testato; i giochi già compilati continuano a funzionare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour au moteur testé rétablit le moteur testé ; les jeux déjà compilés restent jouables." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Zur getesteten Version“ kehrt zur getesteten Engine zurück; bereits erstellte Spiele bleiben spielbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volver al probado restaura el motor probado; los juegos ya compilados siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Voltar à versão testada restaura a engine testada; seus jogos compilados continuam funcionando." + } + } + } + }, + "Back to tested: %@": { + "comment": "AppViewModel+NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Back to tested: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ritorno alla versione testata: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour à la version testée : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück zur getesteten Version: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelta a la versión probada: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "De volta à versão testada: %@" + } + } + } + }, + "Back up now": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Back up now" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea backup ora" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Créer une copie de sauvegarde" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt sichern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Crear copia ahora" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fazer backup agora" + } + } + } + }, + "Back up, import and restore progress.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Back up, import and restore progress." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea backup, importa e ripristina i progressi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Créer une copie de votre progression, l’importer et la restaurer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielfortschritt sichern, importieren und wiederherstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Crea copias de seguridad, importa y restaura el progreso." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Faça backup, importe e restaure seu progresso." + } + } + } + }, + "Background": { + "comment": "ArtworkPicker.swift:12", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Background" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrière-plan" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrund" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fundo" + } + } + } + }, + "Background motion": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Background motion" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Movimento dello sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Animation de l’arrière-plan" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundbewegung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Movimiento del fondo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Movimento do fundo" + } + } + } + }, + "Backup folder": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Backup folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cartella dei backup" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossier des copies de sauvegarde" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Backup-Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carpeta de copias de seguridad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pasta de backups" + } + } + } + }, + "Backups capture the local committed save, which may differ from a save not yet received from the cloud.": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Backups capture the local committed save, which may differ from a save not yet received from the cloud." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I backup copiano il salvataggio locale completato, che può essere diverso da uno non ancora ricevuto dal cloud." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les sauvegardes copient les données locales enregistrées, qui peuvent différer de celles pas encore reçues du cloud." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Backups erfassen den lokal abgeschlossenen Spielstand. Dieser kann von einem noch nicht aus der Cloud empfangenen Spielstand abweichen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las copias de seguridad guardan la partida local confirmada, que puede diferir de otra aún no recibida de la nube." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os backups copiam os dados salvos e confirmados localmente, que podem ser diferentes dos ainda não recebidos da nuvem." + } + } + } + }, + "Balanced": { + "comment": "Background brightness/shade choice, not a stored identifier.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Balanced" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Bilanciato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Équilibré" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausgewogen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Equilibrado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Equilibrado" + } + } + } + }, + "Base Game": { + "comment": "GameFileItem.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Base Game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioco base" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu de base" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Basisspiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juego base" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo base" + } + } + } + }, + "Base game required": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Base game required" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioco base richiesto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu de base requis" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Basisspiel erforderlich" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se requiere el juego base" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo base necessário" + } + } + } + }, + "Before import": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Before import" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prima dell’importazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avant l’importation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vor dem Import" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Antes de importar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Antes da importação" + } + } + } + }, + "Before restore": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Before restore" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prima del ripristino" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Avant la restauration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vor der Wiederherstellung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Antes de restaurar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Antes da restauração" + } + } + } + }, + "Behind library text": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Behind library text" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dietro al testo della libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Derrière le texte de la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hinter Bibliothekstext" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Detrás del texto de la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atrás do texto da biblioteca" + } + } + } + }, + "Behind the menus and the home when no game is selected.": { + "comment": "ThemePickerView.swift:261", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Behind the menus and the home when no game is selected." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dietro ai menu e alla schermata iniziale quando nessun gioco è selezionato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Derrière les menus et l’accueil quand aucun jeu n’est sélectionné." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hinter den Menüs und der Startseite, wenn kein Spiel ausgewählt ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Detrás de los menús y la pantalla principal cuando no hay ningún juego seleccionado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atrás dos menus e da tela inicial quando nenhum jogo está selecionado." + } + } + } + }, + "Blocked": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Blocked" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Bloccato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bloqué" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gesperrt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Bloqueado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Bloqueado" + } + } + } + }, + "Blocked by the Metal gate. See Settings → Engine.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Blocked by the Metal gate. See Settings → Engine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Bloccato dal controllo Metal. Vedi Impostazioni → Motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bloqué par le contrôle Metal. Consultez Réglages → Moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Durch die Metal-Prüfung gesperrt. Siehe Einstellungen → Engine." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Bloqueado por el control de Metal. Consulta Ajustes → Motor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Bloqueado pela verificação do Metal. Veja Ajustes → Engine." + } + } + } + }, + "Box covers": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Box covers" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copertine delle confezioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jaquettes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiele-Cover" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portadas de cajas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capas de caixas" + } + } + } + }, + "Box covers, logos, backgrounds and square art in Change artwork": { + "comment": "SettingsAboutSection.swift:22", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Box covers, logos, backgrounds and square art in Change artwork" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copertine delle confezioni, loghi, sfondi e immagini quadrate in Cambia grafica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jaquettes, logos, arrière-plans et illustrations carrées dans Changer l’illustration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cover, Logos, Hintergründe und quadratische Bilder unter „Artwork ändern“" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portadas de cajas, logotipos, fondos e imágenes cuadradas en Cambiar imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capas de caixas, logotipos, fundos e artes quadradas em Alterar arte" + } + } + } + }, + "Bright": { + "comment": "AppSettings.swift:57", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Bright" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Luminoso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clair" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hell" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Brillante" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Claro" + } + } + } + }, + "Bright on the right": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Bright on the right" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Luminoso a destra" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clair à droite" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Rechts hell" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Brillo a la derecha" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Claro à direita" + } + } + } + }, + "Bring it back in Settings": { + "comment": "SpotlightCardInfoView.swift:189", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Bring it back in Settings" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristinala in Impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La réafficher dans les Réglages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In den Einstellungen wieder einblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Recupérala en Ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Traga de volta nos Ajustes" + } + } + } + }, + "Browse": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Browse" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfoglia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Parcourir" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Stöbern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Explorar" + } + } + } + }, + "Browse %@": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Browse %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfoglia %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Parcourir %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ durchsuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorar %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Explorar %@" + } + } + } + }, + "Browse online": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Browse online" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfoglia online" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Parcourir en ligne" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Online stöbern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorar en línea" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar on-line" + } + } + } + }, + "Browse online · add your key": { + "comment": "ChangeArtworkView.swift:448", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Browse online · add your key" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfoglia online · aggiungi la tua chiave" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Parcourir en ligne · ajoutez votre clé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Online stöbern · Schlüssel hinzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Explorar en línea · añade tu clave" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar on-line · adicione sua chave" + } + } + } + }, + "Build": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compila" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compiler" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilar" + } + } + } + }, + "Build %@ — not used by this version": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build %@ — not used by this version" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Build %@ — non usata da questa versione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Build %@ — inutilisé par cette version" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build %@ — von dieser Version nicht verwendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación %@ — no usada por esta versión" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Build %@ — não usada por esta versão" + } + } + } + }, + "Build & Sources": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build & Sources" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilazione e sorgenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilation et sources" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build & Quellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación y archivos de origen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilação e arquivos de origem" + } + } + } + }, + "Build a game and it shows up here.": { + "comment": "SpotlightRowView.swift:192", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build a game and it shows up here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea l’app di un gioco e apparirà qui." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilez un jeu pour le voir apparaître ici." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstelle eine Spiele-App, damit sie hier erscheint." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila un juego y aparecerá aquí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compile um jogo e ele aparecerá aqui." + } + } + } + }, + "Build a game from your own dumps.": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build a game from your own dumps." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea l’app di un gioco dai tuoi dump." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compiler un jeu à partir de vos propres copies extraites." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstelle eine Spiele-App aus deinen eigenen Dumps." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila un juego a partir de tus propios volcados." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compile um jogo a partir das suas próprias cópias extraídas." + } + } + } + }, + "Build a new app with this engine; the old app stays if the build fails.": { + "comment": "CheatsPanel.swift:282", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build a new app with this engine; the old app stays if the build fails." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea una nuova app con questo motore; quella precedente viene conservata se la compilazione non riesce." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compiler une nouvelle app avec ce moteur ; l’ancienne est conservée en cas d’échec." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstellt eine neue App mit dieser Engine; schlägt der Build fehl, bleibt die alte App erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila una nueva app con este motor; la anterior se conserva si la compilación falla." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compile um novo app com esta engine; o app anterior será mantido se a compilação falhar." + } + } + } + }, + "Build a new app; the old app stays if the build fails.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build a new app; the old app stays if the build fails." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea una nuova app; quella precedente viene conservata se la compilazione non riesce." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compiler une nouvelle app ; l’ancienne est conservée en cas d’échec." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstellt eine neue App; schlägt der Build fehl, bleibt die alte App erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila una nueva app; la anterior se conserva si la compilación falla." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compile um novo app; o app anterior será mantido se a compilação falhar." + } + } + } + }, + "Build and launch logs. Apps, sources and saves are untouched.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build and launch logs. Apps, sources and saves are untouched." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Log di compilazione e avvio. App, sorgenti e salvataggi restano invariati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Journaux de compilation et de lancement. Les apps, sources et sauvegardes restent intacts." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build- und Startprotokolle. Apps, Quellen und Spielstände bleiben unverändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Registros de compilación e inicio. Las apps, los archivos de origen y las partidas guardadas siguen intactos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Logs de compilação e execução. Apps, arquivos de origem e dados salvos não são alterados." + } + } + } + }, + "Build and launch output": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build and launch output" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Output di compilazione e avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sortie de compilation et de lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build- und Startausgabe" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Salida de compilación e inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Saída de compilação e execução" + } + } + } + }, + "Build anyway, recorded as untested. Corrupted graphics are a known risk.": { + "comment": "SettingsEngineSection.swift:236", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build anyway, recorded as untested. Corrupted graphics are a known risk." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compila comunque, registrando la compilazione come non testata. Gli errori grafici sono un rischio noto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compiler quand même, avec enregistrement comme non testé. Des erreurs graphiques sont un risque connu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Trotzdem erstellen, als ungetestet erfasst. Fehlerhafte Grafik ist ein bekanntes Risiko." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila de todos modos; la compilación se registra como no probada. Existe un riesgo conocido de fallos gráficos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compile mesmo assim; a compilação será registrada como não testada. Gráficos corrompidos são um risco conhecido." + } + } + } + }, + "Build blocked — Settings → Engine": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build blocked — Settings → Engine" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilazione bloccata — Impostazioni → Motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilation bloquée — Réglages → Moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build gesperrt — Einstellungen → Engine" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación bloqueada — Ajustes → Motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilação bloqueada — Ajustes → Engine" + } + } + } + }, + "Build cache (%@)": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build cache (%@)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cache di build (%@)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cache de compilation (%@)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build-Cache (%@)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Caché de compilación (%@)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cache de compilação (%@)" + } + } + } + }, + "Build cache: %@": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build cache: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cache di build: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cache de compilation : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build-Cache: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Caché de compilación: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cache de compilação: %@" + } + } + } + }, + "Build caches": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build caches" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cache di compilazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Caches de compilation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build-Caches" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cachés de compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Caches de compilação" + } + } + } + }, + "Build cancelled.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build cancelled." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilazione annullata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilation annulée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build abgebrochen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación cancelada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilação cancelada." + } + } + } + }, + "Build failed (exit code %@).": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build failed (exit code %@)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilazione non riuscita (codice di uscita %@)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de la compilation (code de sortie %@)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build fehlgeschlagen (Exit-Code %@)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La compilación ha fallado (código de salida %@)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Falha na compilação (código de saída %@)." + } + } + } + }, + "Build setup can wait. Existing game apps still play.": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build setup can wait. Existing game apps still play." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La configurazione per compilare può aspettare. Le app dei giochi esistenti funzionano comunque." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La configuration de compilation peut attendre. Les apps de jeux existantes restent jouables." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Build-Einrichtung kann warten. Vorhandene Spiele-Apps lassen sich weiterhin spielen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La configuración para compilar puede esperar. Las apps de juegos existentes siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A configuração de compilação pode esperar. Apps de jogos existentes continuam funcionando." + } + } + } + }, + "Build stages": { + "comment": "BuildStageStepperView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build stages" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Fasi di compilazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étapes de compilation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build-Phasen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Etapas de compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Etapas da compilação" + } + } + } + }, + "Build started for %@ (%@).": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build started for %@ (%@)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilazione avviata per %@ (%@)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilation de %@ démarrée (%@)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build für %@ (%@) gestartet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación de %@ iniciada (%@)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilação de %@ (%@) iniciada." + } + } + } + }, + "Build succeeded while %@ is open: it installs when the game quits.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build succeeded while %@ is open: it installs when the game quits." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilazione riuscita mentre %@ è aperto: l’installazione avverrà alla chiusura del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilation réussie alors que %@ est ouvert : elle sera installée à la fermeture du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build erfolgreich, während %@ geöffnet ist: Die Installation erfolgt, sobald das Spiel beendet wird." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La compilación ha terminado mientras %@ está abierto: se instalará cuando se cierre el juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A compilação foi concluída enquanto %@ está aberto: ela será instalada quando o jogo for encerrado." + } + } + } + }, + "Build succeeded: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build succeeded: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilazione riuscita: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilation réussie : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build erfolgreich: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación completada: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilação concluída: %@" + } + } + } + }, + "Build with nxnative main (%@, %@) before NX Library has tested it.": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build with nxnative main (%@, %@) before NX Library has tested it." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compila con nxnative main (%@, %@) prima che NX Library l’abbia testato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compiler avec nxnative main (%@, %@) avant qu’il soit testé par NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit nxnative main (%@, %@) erstellen, bevor NX Library es getestet hat." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila con main de nxnative (%@, %@) antes de que NX Library lo haya probado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compile com o main do nxnative (%@, %@) antes de o NX Library testá-lo." + } + } + } + }, + "Build with nxnative main before NX Library has tested it, or go back to the tested engine.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Build with nxnative main before NX Library has tested it, or go back to the tested engine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compila con nxnative main prima che NX Library l’abbia testato oppure torna al motore testato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compiler avec nxnative main avant qu’il soit testé par NX Library, ou revenir au moteur testé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit nxnative main erstellen, bevor NX Library es getestet hat, oder zur getesteten Engine zurückkehren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila con main de nxnative antes de que NX Library lo haya probado o vuelve al motor probado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compile com o main do nxnative antes de o NX Library testá-lo ou volte à engine testada." + } + } + } + }, + "Builds are recorded as untested and the untested-toolchain override stays on. ": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Builds are recorded as untested and the untested-toolchain override stays on. " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le compilazioni sono registrate come non testate e il controllo della toolchain non testata resta ignorato. " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les compilations sont enregistrées comme non testées et la dérogation pour chaîne d’outils non testée reste activée. " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Builds werden als ungetestet erfasst und die Ausnahme für ungetestete Toolchains bleibt aktiv. " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las compilaciones se registran como no probadas y la excepción para cadenas de herramientas no probadas se mantiene activada. " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "As compilações são registradas como não testadas e a permissão para conjunto de ferramentas não testado permanece ativada. " + } + } + } + }, + "Builds games with nxnative main instead of the engine this NX Library is tested with. ": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Builds games with nxnative main instead of the engine this NX Library is tested with. " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compila i giochi con nxnative main anziché con il motore con cui è testata questa NX Library. " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compile les jeux avec nxnative main au lieu du moteur avec lequel cette version de NX Library a été testée. " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstellt Spiele-Apps mit nxnative main statt mit der Engine, mit der diese NX Library-Version getestet wurde. " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila juegos con main de nxnative en lugar del motor con el que se ha probado este NX Library. " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compila jogos com o main do nxnative em vez da engine com a qual este NX Library foi testado. " + } + } + } + }, + "Builds the Mac app now; you can keep using the library meanwhile.": { + "comment": "AddGameView+Rows.swift:172", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Builds the Mac app now; you can keep using the library meanwhile." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea subito l’app per Mac; nel frattempo puoi continuare a usare la libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compile l’app Mac maintenant ; vous pouvez continuer à utiliser la bibliothèque pendant ce temps." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstellt die Mac-App jetzt; du kannst die Bibliothek währenddessen weiter nutzen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila la app de Mac ahora; puedes seguir usando la biblioteca mientras tanto." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compila o app para Mac agora; você pode continuar usando a biblioteca enquanto isso." + } + } + } + }, + "Builds the engine's tools that read each game's package.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Builds the engine's tools that read each game's package." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compila gli strumenti del motore che leggono il pacchetto di ogni gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compile les outils du moteur qui lisent les paquets de chaque jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstellt die Engine-Werkzeuge zum Lesen der Spielpakete." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila las herramientas del motor que leen los paquetes de cada juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compila as ferramentas da engine que leem o pacote de cada jogo." + } + } + } + }, + "Bundle identifier": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Bundle identifier" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Identificatore del bundle" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Identifiant du paquet" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bundle-ID" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Identificador del paquete" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Identificador do pacote" + } + } + } + }, + "Button hints": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Button hints" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Suggerimenti dei pulsanti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Indications des boutons" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Tastenhinweise" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Indicaciones de botones" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dicas de botões" + } + } + } + }, + "Button hints, button names and the View button.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Button hints, button names and the View button." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Suggerimenti, nomi dei pulsanti e pulsante View." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Indications des boutons, noms des boutons et bouton View." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Tastenhinweise, Tastennamen und die Ansicht-Taste." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Indicaciones, nombres de botones y botón View." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dicas de botões, nomes dos botões e o botão View." + } + } + } + }, + "Button layout": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Button layout" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disposizione dei pulsanti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Disposition des boutons" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Tastenbelegung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Disposición de botones" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Disposição dos botões" + } + } + } + }, + "Button names": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Button names" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nomi dei pulsanti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Noms des boutons" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Tastennamen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nombres de botones" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nomes dos botões" + } + } + } + }, + "By game": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "By game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Per gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Por juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Por jogo" + } + } + } + }, + "CHANGE ARTWORK": { + "comment": "ChangeArtworkView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "CHANGE ARTWORK" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "CAMBIA GRAFICA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CHANGER L’ILLUSTRATION" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ARTWORK ÄNDERN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "CAMBIAR IMÁGENES" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ALTERAR ARTE" + } + } + } + }, + "CHANGE METADATA": { + "comment": "ChangeMetadataView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "CHANGE METADATA" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "CAMBIA METADATI" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MODIFIER LES MÉTADONNÉES" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "METADATEN ÄNDERN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "CAMBIAR METADATOS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ALTERAR METADADOS" + } + } + } + }, + "CHEATS": { + "comment": "CheatsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "CHEATS" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "TRUCCHI" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CODES DE TRICHE" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "CHEATS" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "TRUCOS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "TRAPAÇAS" + } + } + } + }, + "CONSOLE": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "CONSOLE" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "CONSOLE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CONSOLE" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "KONSOLE" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "CONSOLA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "CONSOLE" + } + } + } + }, + "Cache changed": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cache changed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cache modificata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cache modifié" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cache geändert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La caché ha cambiado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cache alterado" + } + } + } + }, + "Caches, logs, saves and the console.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Caches, logs, saves and the console." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cache, log, salvataggi e console." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Caches, journaux, sauvegardes et console." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Caches, Protokolle, Spielstände und Konsole." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cachés, registros, partidas guardadas y consola." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Caches, logs, dados salvos e console." + } + } + } + }, + "Calculating storage...": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Calculating storage..." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Calcolo dello spazio occupato..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calcul de l’espace de stockage..." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Speicherbedarf wird berechnet..." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Calculando almacenamiento..." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Calculando armazenamento..." + } + } + } + }, + "Calculating…": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Calculating…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Calcolo…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calcul…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird berechnet …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Calculando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Calculando…" + } + } + } + }, + "Can't decide? Pick a game for me.": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Can't decide? Pick a game for me." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non sai deciderti? Scegli un gioco per me." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous hésitez ? Choisissez un jeu pour moi." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Du kannst dich nicht entscheiden? Wähle ein Spiel für mich." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿No te decides? Elige un juego por mí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não consegue decidir? Escolha um jogo para mim." + } + } + } + }, + "Cancel": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Annulla" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Abbrechen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cancelar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cancelar" + } + } + } + }, + "Cancel porting": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancel porting" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Annulla creazione dell’app per Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annuler la création de l’app Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Erstellung abbrechen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cancelar compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cancelar compilação" + } + } + } + }, + "Cancelling build...": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancelling build..." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Annullamento della compilazione..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annulation de la compilation..." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build wird abgebrochen..." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cancelando la compilación..." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cancelando compilação..." + } + } + } + }, + "Cancelling…": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cancelling…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Annullamento…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Annulation…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird abgebrochen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cancelando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cancelando…" + } + } + } + }, + "Cannot read %@. Reconnect it or choose another.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cannot read %@. Reconnect it or choose another." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile leggere %@. Ricollegala o scegline un’altra." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de lire %@. Reconnectez-le ou choisissez-en un autre." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ kann nicht gelesen werden. Verbinde den Ordner erneut oder wähle einen anderen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se puede leer %@. Vuelve a conectarla o elige otra." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não é possível ler %@. Reconecte ou escolha outra pasta." + } + } + } + }, + "Card options": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Card options" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Opzioni della scheda" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Options de la carte" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kartenoptionen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opciones de la tarjeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opções do cartão" + } + } + } + }, + "Cards": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cards" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schede" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cartes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Karten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tarjetas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cartões" + } + } + } + }, + "Certified": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Certified" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Certificata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certifiée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zertifiziert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Certificada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Certificado" + } + } + } + }, + "Change": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ändern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alterar" + } + } + } + }, + "Change artwork": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change artwork" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia grafica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer l’illustration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Artwork ändern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alterar arte" + } + } + } + }, + "Change folder": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer de dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner ändern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar carpeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alterar pasta" + } + } + } + }, + "Change its place on Spotlight": { + "comment": "SpotlightCardInfoView.swift:188", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change its place on Spotlight" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia la sua posizione in Spotlight" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer sa position dans À la une" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Position in Spotlight ändern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambia su posición en Destacados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Altere sua posição em Destaques" + } + } + } + }, + "Change its place on Spotlight.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change its place on Spotlight." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia la sua posizione in Spotlight." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer sa position dans À la une." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Position in Spotlight ändern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambia su posición en Destacados." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Altere sua posição em Destaques." + } + } + } + }, + "Change keys after the porting finishes.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change keys after the porting finishes." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia le chiavi al termine della creazione dell’app per Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifiez les clés une fois la création de l’app Mac terminée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ändere die Schlüssel, nachdem die App erstellt wurde." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambia las claves cuando termine la compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Altere as chaves após o término da compilação." + } + } + } + }, + "Change metadata": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change metadata" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia metadati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les métadonnées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Metadaten ändern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar metadatos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alterar metadados" + } + } + } + }, + "Change sources": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change sources" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia sorgenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les sources" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen ändern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar archivos de origen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alterar arquivos de origem" + } + } + } + }, + "Changed": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Changed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Modificate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifiés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Geändert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Modificados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alterados" + } + } + } + }, + "Changes apply at the next launch. If a mod changes nothing, rebuild the game.": { + "comment": "ModsPanel.swift:400", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Changes apply at the next launch. If a mod changes nothing, rebuild the game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le modifiche si applicano al prossimo avvio. Se una mod non produce cambiamenti, ricompila il gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les modifications s’appliquent au prochain lancement. Si un mod n’a aucun effet, recompilez le jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Änderungen gelten beim nächsten Spielstart. Falls ein Mod nichts ändert, erstelle die Spiele-App erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los cambios se aplican en el próximo inicio. Si un mod no cambia nada, recompila el juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "As alterações são aplicadas na próxima abertura. Se um mod não mudar nada, recompile o jogo." + } + } + } + }, + "Changes apply immediately.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Changes apply immediately." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le modifiche si applicano subito." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les modifications s’appliquent immédiatement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Änderungen gelten sofort." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los cambios se aplican de inmediato." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "As alterações são aplicadas imediatamente." + } + } + } + }, + "Change…": { + "comment": "DropZoneView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Change…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ändern …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alterar…" + } + } + } + }, + "Characters & skins": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Characters & skins" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Personaggi e skin" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Personnages et apparences" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Charaktere & Skins" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Personajes y aspectos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Personagens e visuais" + } + } + } + }, + "Cheats": { + "comment": "CheatsPanel.swift:284", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cheats" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Trucchi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Codes de triche" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cheats" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Trucos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Trapaças" + } + } + } + }, + "Cheats change values while you play, for this version only. New items, looks or gameplay are in Mods.": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cheats change values while you play, for this version only. New items, looks or gameplay are in Mods." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I trucchi modificano i valori mentre giochi, solo per questa versione. Nuovi oggetti, aspetti o modalità di gioco si trovano in Mod." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les codes de triche modifient des valeurs pendant le jeu, pour cette version uniquement. Les nouveaux objets, apparences ou mécanismes de jeu se trouvent dans Mods." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cheats ändern Werte während des Spielens und gelten nur für diese Version. Neue Gegenstände, Designs oder Spielmechaniken findest du unter „Mods“." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los trucos cambian valores mientras juegas, solo para esta versión. Los objetos, aspectos y funciones de juego nuevos están en Mods." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "As trapaças alteram valores enquanto você joga, apenas nesta versão. Novos itens, visuais ou mecânicas ficam em Mods." + } + } + } + }, + "Cheats that were on and keep their name stay on; the rest start off.": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cheats that were on and keep their name stay on; the rest start off." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I trucchi attivi che mantengono il proprio nome restano attivi; gli altri sono inizialmente disattivati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les codes de triche activés dont le nom ne change pas restent activés ; les autres sont désactivés au départ." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktive Cheats mit unverändertem Namen bleiben aktiv; alle anderen sind zunächst deaktiviert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los trucos que estaban activados y conservan su nombre siguen activados; los demás empiezan desactivados." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "As trapaças ativadas que mantiverem o nome continuarão ativadas; as demais começam desativadas." + } + } + } + }, + "Check": { + "comment": "SettingsEngineSection.swift:353", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Prüfen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificar" + } + } + } + }, + "Check engine at launch": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check engine at launch" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controlla il motore all’avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifier le moteur au lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine beim Start prüfen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobar el motor al abrir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificar engine ao iniciar" + } + } + } + }, + "Check for Updates…": { + "comment": "NXLibraryApp.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check for Updates…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica aggiornamenti…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher les mises à jour…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach Updates suchen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar actualizaciones…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar atualizações…" + } + } + } + }, + "Check for app updates": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check for app updates" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica aggiornamenti dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher les mises à jour de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach App-Updates suchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar actualizaciones de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar atualizações do app" + } + } + } + }, + "Check newer engine commits without installing them.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check newer engine commits without installing them." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controlla i commit più recenti del motore senza installarli." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifier les nouveaux commits du moteur sans les installer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach neueren Engine-Commits suchen, ohne sie zu installieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Consulta commits más recientes del motor sin instalarlos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifique commits mais recentes da engine sem instalá-los." + } + } + } + }, + "Check now": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check now" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica ora" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifier maintenant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt prüfen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobar ahora" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificar agora" + } + } + } + }, + "Check now, update or read release notes.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check now, update or read release notes." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica ora, aggiorna o leggi le note di rilascio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifier maintenant, mettre à jour ou lire les notes de version." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt prüfen, aktualisieren oder Versionshinweise lesen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprueba ahora, actualiza o lee las notas de la versión." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifique agora, atualize ou leia as notas da versão." + } + } + } + }, + "Check saves and backups again.": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check saves and backups again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controlla di nuovo salvataggi e backup." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifier à nouveau les sauvegardes et leurs copies." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstände und Backups erneut prüfen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a comprobar las partidas y las copias de seguridad." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifique os dados salvos e os backups novamente." + } + } + } + }, + "Check that the files are complete NSP, NSZ or XCI dumps.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check that the files are complete NSP, NSZ or XCI dumps." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica che i file siano dump NSP, NSZ o XCI completi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifiez que les fichiers sont des copies extraites NSP, NSZ ou XCI complètes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Prüfe, ob die Dateien vollständige NSP-, NSZ- oder XCI-Dumps sind." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprueba que los archivos sean volcados NSP, NSZ o XCI completos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifique se os arquivos são cópias extraídas completas em NSP, NSZ ou XCI." + } + } + } + }, + "Check what you need to build. Your games come next.": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Check what you need to build. Your games come next." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controlla ciò che serve per compilare. Poi passerai ai tuoi giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifiez les éléments nécessaires à la compilation. Vous ajouterez vos jeux ensuite." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Prüfe die Voraussetzungen zum Erstellen von Apps. Danach kommen deine Spiele." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprueba qué necesitas para compilar. Tus juegos vienen después." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifique o que você precisa para compilar. Seus jogos vêm a seguir." + } + } + } + }, + "Checking": { + "comment": "SettingsEngineSection.swift:240", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird geprüft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobando" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificando" + } + } + } + }, + "Checking Microsoft's hash…": { + "comment": "SetupCheckModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking Microsoft's hash…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica dell’hash di Microsoft…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification de l’empreinte fournie par Microsoft…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hash von Microsoft wird geprüft …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobando el hash de Microsoft…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificando o hash da Microsoft…" + } + } + } + }, + "Checking download…": { + "comment": "SettingsAboutSection.swift:229", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking download…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica del file scaricato…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification du téléchargement…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Download wird überprüft …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobando la descarga…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificando download…" + } + } + } + }, + "Checking environment": { + "comment": "GameOptionsView.swift:94", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking environment" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica dell’ambiente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification de l’environnement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Umgebung wird geprüft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobando el entorno" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificando ambiente" + } + } + } + }, + "Checking for updates": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking for updates" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica degli aggiornamenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche de mises à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach Updates wird gesucht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando actualizaciones" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando atualizações" + } + } + } + }, + "Checking for updates…": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking for updates…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica degli aggiornamenti…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche de mises à jour…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach Updates wird gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando actualizaciones…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando atualizações…" + } + } + } + }, + "Checking the download before installing.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking the download before installing." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica del file scaricato prima dell’installazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification du téléchargement avant l’installation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Download wird vor der Installation überprüft." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobando la descarga antes de instalar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificando o download antes de instalar." + } + } + } + }, + "Checking...": { + "comment": "SettingsEngineSection.swift:352", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking..." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification..." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird geprüft..." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobando..." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificando..." + } + } + } + }, + "Checking…": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checking…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird geprüft …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificando…" + } + } + } + }, + "Checks Game apps and its subfolders. Existing library entries are kept; no apps are moved. Source files are optional for Play.": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checks Game apps and its subfolders. Existing library entries are kept; no apps are moved. Source files are optional for Play." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controlla App dei giochi e le sue sottocartelle. Le voci esistenti della libreria vengono conservate; nessuna app viene spostata. I file sorgente non sono necessari per giocare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifie Apps de jeux et ses sous-dossiers. Les entrées existantes de la bibliothèque sont conservées ; aucune app n’est déplacée. Les fichiers sources sont facultatifs pour Jouer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Prüft den Spiele-App-Ordner und seine Unterordner. Vorhandene Bibliothekseinträge bleiben erhalten; Apps werden nicht verschoben. Quelldateien sind zum Spielen optional." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Revisa la carpeta Apps de juegos y sus subcarpetas. Conserva las entradas existentes de la biblioteca; no se mueve ninguna app. Los archivos de origen son opcionales para jugar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifica Apps de jogos e suas subpastas. As entradas existentes da biblioteca são mantidas; nenhum app é movido. Os arquivos de origem são opcionais para Jogar." + } + } + } + }, + "Checks the engine and toolchain each time NX Library starts.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checks the engine and toolchain each time NX Library starts." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controlla il motore e la toolchain a ogni avvio di NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifie le moteur et la chaîne d’outils à chaque démarrage de NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Prüft Engine und Toolchain bei jedem Start von NX Library." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprueba el motor y la cadena de herramientas cada vez que se inicia NX Library." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifica a engine e o conjunto de ferramentas sempre que o NX Library inicia." + } + } + } + }, + "Checks the nxnative checkout against the certified engine commit and confirms your installed Metal compiler matches the lock file before builds are attempted.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Checks the nxnative checkout against the certified engine commit and confirms your installed Metal compiler matches the lock file before builds are attempted." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controlla il checkout di nxnative rispetto al commit certificato del motore e verifica che il compilatore Metal installato corrisponda al file di vincoli prima di tentare una compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifie la copie de travail de nxnative par rapport au commit certifié du moteur et confirme que le compilateur Metal installé correspond au fichier de verrouillage avant toute tentative de compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Prüft den nxnative-Checkout anhand des zertifizierten Engine-Commits und bestätigt vor Build-Versuchen, dass der installierte Metal-Compiler der Lockdatei entspricht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compara la copia local de nxnative con el commit certificado del motor y confirma que el compilador Metal instalado coincide con el archivo de certificación antes de intentar compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifica o checkout do nxnative em relação ao commit certificado da engine e confirma se o compilador Metal instalado corresponde ao arquivo de versões fixadas antes de tentar compilar." + } + } + } + }, + "Choose": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher" + } + } + } + }, + "Choose a %@ for %@.": { + "comment": "ChangeArtworkView.swift:661", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a %@ for %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli %@ per %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez une image (%@) pour %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ für %@ auswählen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige %@ para %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha %@ para %@." + } + } + } + }, + "Choose a folder containing this game's actual save files.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a folder containing this game's actual save files." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli una cartella contenente i file di salvataggio effettivi di questo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un dossier contenant les véritables fichiers de sauvegarde de ce jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle einen Ordner mit den tatsächlichen Spielstanddateien dieses Spiels." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige una carpeta que contenga los archivos reales de la partida guardada de este juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha uma pasta com os arquivos reais de dados salvos deste jogo." + } + } + } + }, + "Choose a folder instead": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a folder instead" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli invece una cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir plutôt un dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Stattdessen einen Ordner auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir una carpeta en su lugar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher uma pasta" + } + } + } + }, + "Choose a folder with one game's base, update and DLC.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a folder with one game's base, update and DLC." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli una cartella con gioco base, aggiornamento e DLC di un solo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un dossier contenant le jeu de base, la mise à jour et les DLC d’un seul jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle einen Ordner mit Basisspiel, Update und DLC eines einzelnen Spiels." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige una carpeta con el juego base, la actualización y los DLC de un solo juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha uma pasta com o jogo base, a atualização e os DLCs de um único jogo." + } + } + } + }, + "Choose a folder with your %@. Existing apps play without these files. Change it later in Settings → Library.": { + "comment": "GameFoldersCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a folder with your %@. Existing apps play without these files. Change it later in Settings → Library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli una cartella con i tuoi %@. Le app esistenti funzionano senza questi file. Puoi cambiarla in seguito in Impostazioni → Libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un dossier contenant vos %@. Les apps existantes fonctionnent sans ces fichiers. Vous pourrez modifier ce choix dans Réglages → Bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle einen Ordner mit deinen %@. Vorhandene Apps lassen sich ohne diese Dateien spielen. Du kannst den Ordner später unter Einstellungen → Bibliothek ändern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige una carpeta con tus %@. Las apps existentes se pueden ejecutar sin estos archivos. Puedes cambiarlo más tarde en Ajustes → Biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha uma pasta com seus %@. Apps existentes funcionam sem esses arquivos. Altere depois em Ajustes → Biblioteca." + } + } + } + }, + "Choose a game": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli un gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir un juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher um jogo" + } + } + } + }, + "Choose a game file, or add one later.": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a game file, or add one later." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli un file di gioco oppure aggiungilo più tardi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un fichier de jeu, ou ajoutez-en un plus tard." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle eine Spieldatei aus oder füge später eine hinzu." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige un archivo de juego o añade uno más tarde." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha um arquivo de jogo ou adicione um depois." + } + } + } + }, + "Choose a game folder": { + "comment": "DropZoneView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a game folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli una cartella di gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un dossier de jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielordner auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir una carpeta de juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher uma pasta de jogo" + } + } + } + }, + "Choose a picture for your background. NX Library keeps its own copy.": { + "comment": "ThemePickerView.swift:766", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a picture for your background. NX Library keeps its own copy." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli un’immagine per lo sfondo. NX Library ne conserva una copia." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez une image pour votre arrière-plan. NX Library en conserve une copie." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle ein Bild für deinen Hintergrund. NX Library behält eine eigene Kopie." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige una imagen para tu fondo. NX Library conserva su propia copia." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha uma imagem para seu fundo. O NX Library mantém uma cópia própria." + } + } + } + }, + "Choose a save folder to import.": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a save folder to import." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli una cartella di salvataggio da importare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un dossier de sauvegarde à importer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle einen Spielstandordner zum Importieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige una carpeta de partida guardada para importar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha uma pasta de dados salvos para importar." + } + } + } + }, + "Choose a video for your live background (MP4, MOV or M4V). NX Library keeps its own copy and plays it muted.": { + "comment": "ThemePickerView.swift:765", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a video for your live background (MP4, MOV or M4V). NX Library keeps its own copy and plays it muted." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli un video per lo sfondo animato (MP4, MOV o M4V). NX Library ne conserva una copia e lo riproduce senza audio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez une vidéo pour votre arrière-plan animé (MP4, MOV ou M4V). NX Library en conserve une copie et la lit sans le son." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle ein Video für deinen Live-Hintergrund (MP4, MOV oder M4V). NX Library behält eine eigene Kopie und spielt es ohne Ton ab." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige un vídeo para tu fondo animado (MP4, MOV o M4V). NX Library conserva su propia copia y lo reproduce sin sonido." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha um vídeo para seu fundo animado (MP4, MOV ou M4V). O NX Library mantém uma cópia própria e o reproduz sem som." + } + } + } + }, + "Choose a writable folder for new builds. Existing apps still play.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose a writable folder for new builds. Existing apps still play." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli una cartella scrivibile per le nuove compilazioni. Le app esistenti funzionano comunque." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un dossier accessible en écriture pour les nouvelles compilations. Les apps existantes restent jouables." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle einen beschreibbaren Ordner für neue Builds. Vorhandene Apps bleiben spielbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige una carpeta con permiso de escritura para nuevas compilaciones. Las apps existentes siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha uma pasta com permissão de gravação para novas compilações. Apps existentes continuam funcionando." + } + } + } + }, + "Choose app language": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose app language" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli la lingua dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir la langue de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Sprache wählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir el idioma de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher o idioma do app" + } + } + } + }, + "Choose file": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose file" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli file" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un fichier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir archivo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher arquivo" + } + } + } + }, + "Choose folder": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir carpeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher pasta" + } + } + } + }, + "Choose game file": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose game file" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli file di gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un fichier de jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldatei auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir archivo de juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher arquivo de jogo" + } + } + } + }, + "Choose game files": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose game files" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli i file del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir les fichiers du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldateien auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir archivos del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher arquivos do jogo" + } + } + } + }, + "Choose game files or find your existing apps.": { + "comment": "CollectionView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose game files or find your existing apps." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli i file dei giochi o trova le app esistenti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez les fichiers du jeu ou recherchez vos apps existantes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle Spieldateien aus oder suche deine vorhandenen Apps." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige archivos de juegos o busca tus apps existentes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha arquivos de jogos ou encontre seus apps existentes." + } + } + } + }, + "Choose how dates and numbers are displayed.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose how dates and numbers are displayed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli come visualizzare date e numeri." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir le format d’affichage des dates et des nombres." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle, wie Datumsangaben und Zahlen angezeigt werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige cómo se muestran las fechas y los números." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha como as datas e os números são exibidos." + } + } + } + }, + "Choose one NUP file on its own: it holds the whole game.": { + "comment": "EnginePackageReader.swift:39", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose one NUP file on its own: it holds the whole game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli un solo file NUP, senza altri file: contiene il gioco completo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez un seul fichier NUP, sans autre fichier : il contient le jeu complet." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle nur eine einzelne NUP-Datei aus: Sie enthält das gesamte Spiel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige un solo archivo NUP: contiene el juego completo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha um único arquivo NUP: ele contém o jogo completo." + } + } + } + }, + "Choose region": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose region" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli zona" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir une région" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Region auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir región" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher região" + } + } + } + }, + "Choose the folder containing your game apps.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose the folder containing your game apps." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli la cartella che contiene le app dei giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez le dossier contenant vos apps de jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle den Ordner mit deinen Spiele-Apps." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige la carpeta que contiene tus apps de juegos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha a pasta que contém os apps dos seus jogos." + } + } + } + }, + "Choose the heart on a game to add it to your favorites.": { + "comment": "SpotlightRowView.swift:194", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose the heart on a game to add it to your favorites." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Seleziona il cuore su un gioco per aggiungerlo ai preferiti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sélectionnez le cœur d’un jeu pour l’ajouter à vos favoris." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle das Herz bei einem Spiel, um es zu deinen Favoriten hinzuzufügen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Selecciona el corazón de un juego para añadirlo a tus favoritos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Selecione o coração de um jogo para adicioná-lo aos favoritos." + } + } + } + }, + "Choose this folder or another: %@": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose this folder or another: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli questa cartella o un’altra: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez ce dossier ou un autre : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle diesen oder einen anderen Ordner: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige esta carpeta u otra: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha esta pasta ou outra: %@" + } + } + } + }, + "Choose where to find existing nxnative game apps and install new builds.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose where to find existing nxnative game apps and install new builds." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli dove trovare le app dei giochi nxnative esistenti e installare le nuove compilazioni." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir où rechercher les apps de jeux nxnative existantes et installer les nouvelles compilations." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle, wo vorhandene nxnative-Spiele-Apps gesucht und neue Builds installiert werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige dónde buscar apps de juegos de nxnative existentes e instalar nuevas compilaciones." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha onde encontrar apps de jogos existentes do nxnative e instalar novas compilações." + } + } + } + }, + "Choose where to keep your game apps.": { + "comment": "GameFoldersCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose where to keep your game apps." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli dove conservare le app dei giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez où conserver vos apps de jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle, wo deine Spiele-Apps gespeichert werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige dónde guardar tus apps de juegos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha onde manter os apps dos seus jogos." + } + } + } + }, + "Choose where to keep your game apps. An empty folder is fine.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose where to keep your game apps. An empty folder is fine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli dove conservare le app dei giochi. Va bene anche una cartella vuota." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez où conserver vos apps de jeux. Un dossier vide convient." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle, wo deine Spiele-Apps gespeichert werden. Ein leerer Ordner genügt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige dónde guardar tus apps de juegos. Puede ser una carpeta vacía." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha onde manter os apps dos seus jogos. Pode ser uma pasta vazia." + } + } + } + }, + "Choose where to keep your game apps. Put any existing nxnative games (.app) here. Change it later in Settings → Library.": { + "comment": "GameFoldersCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose where to keep your game apps. Put any existing nxnative games (.app) here. Change it later in Settings → Library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli dove conservare le app dei giochi. Inserisci qui eventuali giochi nxnative (.app) esistenti. Puoi cambiarla in seguito in Impostazioni → Libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez où conserver vos apps de jeux. Placez-y vos jeux nxnative existants (.app). Vous pourrez modifier ce choix dans Réglages → Bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle, wo deine Spiele-Apps gespeichert werden. Lege hier auch vorhandene nxnative-Spiele (.app) ab. Du kannst den Ordner später unter Einstellungen → Bibliothek ändern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige dónde guardar tus apps de juegos. Coloca aquí tus juegos existentes de nxnative (.app). Puedes cambiarlo más tarde en Ajustes → Biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha onde manter os apps dos seus jogos. Coloque aqui os jogos existentes do nxnative (.app). Altere depois em Ajustes → Biblioteca." + } + } + } + }, + "Choose where to keep your game apps. Put any existing nxnative games (.app) here. Changing folders does not move apps; rebuilds update them where they are.": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose where to keep your game apps. Put any existing nxnative games (.app) here. Changing folders does not move apps; rebuilds update them where they are." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli dove conservare le app dei giochi. Inserisci qui eventuali giochi nxnative (.app) esistenti. Cambiare cartella non sposta le app; le ricompilazioni le aggiornano dove si trovano." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez où conserver vos apps de jeux. Placez-y vos jeux nxnative existants (.app). Changer de dossier ne déplace pas les apps ; les recompilations les mettent à jour à leur emplacement actuel." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle, wo deine Spiele-Apps gespeichert werden. Lege hier auch vorhandene nxnative-Spiele (.app) ab. Ein Ordnerwechsel verschiebt keine Apps; beim erneuten Erstellen werden sie an ihrem bisherigen Ort aktualisiert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige dónde guardar tus apps de juegos. Coloca aquí tus juegos existentes de nxnative (.app). Cambiar de carpeta no mueve las apps; al recompilar, se actualizan en su ubicación actual." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha onde manter os apps dos seus jogos. Coloque aqui os jogos existentes do nxnative (.app). Alterar a pasta não move os apps; as recompilações os atualizam onde estão." + } + } + } + }, + "Choose your Selene folder (the one that contains theme/theme.yaml).": { + "comment": "SettingsSoundSection.swift:69", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose your Selene folder (the one that contains theme/theme.yaml)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli la tua cartella Selene (quella che contiene theme/theme.yaml)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez votre dossier Selene (celui qui contient theme/theme.yaml)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle deinen Selene-Ordner (den Ordner mit theme/theme.yaml)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige tu carpeta de Selene (la que contiene theme/theme.yaml)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha sua pasta do Selene (a que contém theme/theme.yaml)." + } + } + } + }, + "Choose your background": { + "comment": "ThemePickerView.swift:261", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose your background" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli il tuo sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez votre arrière-plan" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle deinen Hintergrund" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige tu fondo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha seu fundo" + } + } + } + }, + "Choose your file once": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose your file once" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli il tuo file una sola volta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez votre fichier une seule fois" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle deine Datei einmal aus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige tu archivo una sola vez" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha seu arquivo uma vez" + } + } + } + }, + "Choose your prod.keys file.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose your prod.keys file." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli il tuo file prod.keys." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez votre fichier prod.keys." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle deine prod.keys-Datei." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige tu archivo prod.keys." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha seu arquivo prod.keys." + } + } + } + }, + "Choose your theme": { + "comment": "ThemePickerView.swift:158", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose your theme" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli il tuo tema" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisissez votre thème" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle dein Design" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elige tu tema" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolha seu tema" + } + } + } + }, + "Choose…": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Choose…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auswählen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher…" + } + } + } + }, + "City": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "City" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Città" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ville" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Stadt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ciudad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cidade" + } + } + } + }, + "Cleanup unavailable": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cleanup unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pulizia non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nettoyage indisponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bereinigung nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Limpieza no disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Limpeza indisponível" + } + } + } + }, + "Clear": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Svuota" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effacer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Leeren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Borrar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Limpar" + } + } + } + }, + "Clear cached store descriptions and links.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear cached store descriptions and links." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Svuota la cache di descrizioni e link dello store." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effacer les descriptions et liens de la boutique mis en cache." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zwischengespeicherte Shop-Beschreibungen und Links löschen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Borra las descripciones y los enlaces de la tienda guardados en caché." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Limpe as descrições e os links da loja em cache." + } + } + } + }, + "Clear downloaded images; manual picks are kept.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear downloaded images; manual picks are kept." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovi le immagini scaricate; le scelte manuali vengono conservate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effacer les images téléchargées ; les choix manuels sont conservés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Geladene Bilder löschen; manuell gewählte Bilder bleiben erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Borra las imágenes descargadas; se conservan las elegidas manualmente." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Limpe as imagens baixadas; as escolhas manuais são mantidas." + } + } + } + }, + "Clear the console?": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clear the console?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Svuotare la console?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Effacer la console ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Konsole leeren?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Borrar la consola?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Limpar o console?" + } + } + } + }, + "Click and drag · Handheld mode": { + "comment": "ControlsView.swift:38", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Click and drag · Handheld mode" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Fai clic e trascina · Modalità portatile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cliquer et faire glisser · Mode portable" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Klicken und ziehen · Handheld-Modus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Haz clic y arrastra · Modo portátil" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Clique e arraste · Modo portátil" + } + } + } + }, + "Clock": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clock" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Orologio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Horloge" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Uhr" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reloj" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Relógio" + } + } + } + }, + "Clone": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Clone" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Clona" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cloner" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Klonen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clonar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Clonar" + } + } + } + }, + "Close": { + "comment": "MenuOverlayView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Close" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiudi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schließen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cerrar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fechar" + } + } + } + }, + "Close %@?": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Close %@?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiudere %@?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer %@ ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ beenden?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Cerrar %@?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encerrar %@?" + } + } + } + }, + "Close Game": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Close Game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiudi gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermer le jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel beenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cerrar juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encerrar jogo" + } + } + } + }, + "Close the game before managing its saves. Keep it closed until this operation finishes.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Close the game before managing its saves. Keep it closed until this operation finishes." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiudi il gioco prima di gestirne i salvataggi. Tienilo chiuso fino al termine dell’operazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fermez le jeu avant de gérer ses sauvegardes. Gardez-le fermé jusqu’à la fin de cette opération." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beende das Spiel, bevor du seine Spielstände verwaltest. Lass es geschlossen, bis der Vorgang abgeschlossen ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cierra el juego antes de gestionar sus partidas guardadas. Mantenlo cerrado hasta que termine esta operación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encerre o jogo antes de gerenciar seus dados salvos. Mantenha-o fechado até o término desta operação." + } + } + } + }, + "Cloud on": { + "comment": "SavesPanel.swift:136", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud on" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cloud attivo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cloud activé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud aktiviert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nube activada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nuvem ativada" + } + } + } + }, + "Cloud saves": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud saves" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salvataggi cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegardes dans le cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Spielstände" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Partidas en la nube" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dados salvos na nuvem" + } + } + } + }, + "Cloud saves are enabled in %@. Turn them off there before importing or restoring.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud saves are enabled in %@. Turn them off there before importing or restoring." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I salvataggi cloud sono attivi in %@. Disattivali lì prima di importare o ripristinare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les sauvegardes dans le cloud sont activées dans %@. Désactivez-les à cet endroit avant d’importer ou de restaurer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Spielstände sind in %@ aktiviert. Deaktiviere sie dort vor dem Importieren oder Wiederherstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las partidas en la nube están activadas en %@. Desactívalas allí antes de importar o restaurar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os dados salvos na nuvem estão ativados em %@. Desative-os lá antes de importar ou restaurar." + } + } + } + }, + "Cloud saves folder": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud saves folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cartella dei salvataggi cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossier des sauvegardes dans le cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Spielstandordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carpeta de partidas en la nube" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pasta de dados salvos na nuvem" + } + } + } + }, + "Cloud saves on": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud saves on" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salvataggi cloud attivi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegardes dans le cloud activées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Spielstände aktiviert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Partidas en la nube activadas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dados salvos na nuvem ativados" + } + } + } + }, + "Cloud saves: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud saves: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salvataggi cloud: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegardes dans le cloud : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Spielstände: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Partidas en la nube: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dados salvos na nuvem: %@" + } + } + } + }, + "Cloud sync could not be updated for this launch. Check Games → Cloud saves.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud sync could not be updated for this launch. Check Games → Cloud saves." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile aggiornare la sincronizzazione cloud per questo avvio. Controlla Giochi → Salvataggi cloud." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La synchronisation cloud n’a pas pu être actualisée pour ce lancement. Vérifiez Jeux → Sauvegardes dans le cloud." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Cloud-Synchronisierung konnte für diesen Start nicht aktualisiert werden. Prüfe Spiele → Cloud-Spielstände." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido actualizar la sincronización en la nube para este inicio. Revisa Juegos → Partidas en la nube." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível atualizar a sincronização na nuvem para esta abertura. Verifique Jogos → Dados salvos na nuvem." + } + } + } + }, + "Cloud sync kept": { + "comment": "SettingsView.swift:265", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud sync kept" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sincronizzazione cloud conservata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Synchronisation cloud conservée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud-Synchronisierung beibehalten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sincronización en la nube conservada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sincronização na nuvem mantida" + } + } + } + }, + "Cloud unavailable": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cloud unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cloud non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cloud indisponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cloud nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nube no disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nuvem indisponível" + } + } + } + }, + "Collection": { + "comment": "SpotlightRow.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Collection" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Raccolta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Collection" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sammlung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Colección" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Coleção" + } + } + } + }, + "Collection · Suggestions": { + "comment": "ChangeArtworkView.swift:308", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Collection · Suggestions" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Raccolta · Suggerimenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Collection · Suggestions" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sammlung · Vorschläge" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Colección · Sugerencias" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Coleção · Sugestões" + } + } + } + }, + "Color": { + "comment": "AppIconAppearanceMode.swift:13", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Color" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Farbe" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Color" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cor" + } + } + } + }, + "Color theme": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Color theme" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tema cromatico" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Thème de couleurs" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Farbdesign" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tema de color" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tema de cores" + } + } + } + }, + "Colors for all of NX Library. Move through them to see each one in the preview.": { + "comment": "ThemePickerView.swift:158", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Colors for all of NX Library. Move through them to see each one in the preview." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colori per tutta NX Library. Scorri le opzioni per vederle in anteprima." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Des couleurs pour toute l’interface de NX Library. Parcourez-les pour voir chaque thème dans l’aperçu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Farben für ganz NX Library. Wechsle zwischen ihnen, um jedes Design in der Vorschau zu sehen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Colores para todo NX Library. Recorre los temas para ver cada uno en la vista previa." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cores para todo o NX Library. Navegue entre elas para ver cada uma na prévia." + } + } + } + }, + "Community covers, backgrounds, logos and icons, with your API key": { + "comment": "SettingsAboutSection.swift:23", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Community covers, backgrounds, logos and icons, with your API key" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copertine, sfondi, loghi e icone della community, con la tua chiave API" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jaquettes, arrière-plans, logos et icônes de la communauté, avec votre clé API" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cover, Hintergründe, Logos und Symbole aus der Community mit deinem API-Schlüssel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portadas, fondos, logotipos e iconos de la comunidad, con tu clave de API" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capas, fundos, logotipos e ícones da comunidade, com sua chave de API" + } + } + } + }, + "Compare": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Compare" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Confronta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comparer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vergleichen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comparar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Comparar" + } + } + } + }, + "Compatibility": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Compatibility" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compatibilità" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compatibilité" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kompatibilität" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compatibilidad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compatibilidade" + } + } + } + }, + "Compiles each game's shaders for this Mac's GPU.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Compiles each game's shaders for this Mac's GPU." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compila gli shader di ogni gioco per la GPU di questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compile les shaders de chaque jeu pour le GPU de ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kompiliert die Shader jedes Spiels für die GPU dieses Macs." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila los shaders de cada juego para la GPU de este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compila os shaders de cada jogo para a GPU deste Mac." + } + } + } + }, + "Connect %@, then try again.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connect %@, then try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Collega %@, poi riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez %@, puis réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbinde %@ und versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conecta %@ e inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Conecte %@ e tente novamente." + } + } + } + }, + "Connect a controller and press Detect.": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connect a controller and press Detect." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Collega un controller e premi Rileva." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectez une manette et appuyez sur Détecter." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbinde einen Controller und drücke „Erkennen“." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conecta un mando y pulsa Detectar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Conecte um controle e pressione Detectar." + } + } + } + }, + "Connected": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connected" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Connesso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connectée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conectado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Conectado" + } + } + } + }, + "Connecting to GitHub…": { + "comment": "SetupCheckModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Connecting to GitHub…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Connessione a GitHub…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Connexion à GitHub…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbindung zu GitHub wird hergestellt …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conectando con GitHub…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Conectando ao GitHub…" + } + } + } + }, + "Console": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Console" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Console" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Console" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Konsole" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Consola" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Console" + } + } + } + }, + "Console mode": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Console mode" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Modalità della console" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode de la console" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Konsolenmodus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Modo de consola" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Modo do console" + } + } + } + }, + "Console profile": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Console profile" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Profilo della console" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Profil de la console" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Konsolenprofil" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Perfil de la consola" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Perfil do console" + } + } + } + }, + "Continue": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Continue" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Continua" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Continuer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fortfahren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Continuar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Continuar" + } + } + } + }, + "Continue where you left off": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Continue where you left off" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprendi da dove avevi lasciato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reprenez là où vous en étiez" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dort weiterspielen, wo du aufgehört hast" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Continúa donde lo dejaste" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Continue de onde parou" + } + } + } + }, + "Controller": { + "comment": "ControlsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Controller" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controller" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Manette" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Controller" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mando" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Controle" + } + } + } + }, + "Controller vibration": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Controller vibration" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Vibrazione del controller" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibrations de la manette" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Controller-Vibration" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vibración del mando" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Vibração do controle" + } + } + } + }, + "Controllers": { + "comment": "SettingsSection.swift:21", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Controllers" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controller" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Manettes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Controller" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mandos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Controles" + } + } + } + }, + "Controls": { + "comment": "ControlsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Controls" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Comandi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commandes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Steuerung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Controles" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Controles" + } + } + } + }, + "Copied": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copied" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copiato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copié" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kopiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copiado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiado" + } + } + } + }, + "Copied once to Application Support, readable only by you.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copied once to Application Support, readable only by you." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copiato una sola volta in Application Support, leggibile solo da te." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copié une seule fois dans Application Support, lisible uniquement par vous." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird einmal nach Application Support kopiert und ist nur für dich lesbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se copia una sola vez en Application Support; solo tú puedes leerlo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiado uma vez para Application Support, com acesso de leitura apenas para você." + } + } + } + }, + "Copies optional sounds and wallpapers from a local Selene folder you own.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copies optional sounds and wallpapers from a local Selene folder you own." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia suoni e sfondi facoltativi da una cartella Selene locale di tua proprietà." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copie des sons et fonds d’écran facultatifs depuis un dossier Selene local que vous possédez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kopiert optionale Sounds und Hintergrundbilder aus einem lokalen Selene-Ordner, den du besitzt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copia sonidos y fondos opcionales de una carpeta local de Selene que te pertenezca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copia sons e papéis de parede opcionais de uma pasta local do Selene que você possui." + } + } + } + }, + "Copies your defaults for new games once. Later changes to those defaults leave this game unchanged.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copies your defaults for new games once. Later changes to those defaults leave this game unchanged." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia una volta i tuoi valori predefiniti per i nuovi giochi. Le modifiche successive a quei valori non cambiano questo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copie une seule fois vos réglages par défaut des nouveaux jeux. Les modifications ultérieures de ces réglages ne changent pas ce jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kopiert deine Standards für neue Spiele einmalig. Spätere Änderungen dieser Standards verändern dieses Spiel nicht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copia una vez tus valores predeterminados para juegos nuevos. Los cambios posteriores de esos valores no modifican este juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copia uma vez seus padrões para novos jogos. Alterações posteriores nesses padrões não afetam este jogo." + } + } + } + }, + "Copy a scrubbed support report.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy a scrubbed support report." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia un rapporto di assistenza privo di dati sensibili." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copier un rapport d’assistance expurgé des données sensibles." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einen um sensible Daten bereinigten Support-Bericht kopieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copia un informe de soporte sin datos sensibles." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copie um relatório de suporte sem informações confidenciais." + } + } + } + }, + "Copy current defaults": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy current defaults" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia i valori predefiniti attuali" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copier les réglages par défaut actuels" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktuelle Standards kopieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copiar valores predeterminados actuales" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiar padrões atuais" + } + } + } + }, + "Copy diagnostics": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy diagnostics" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia dati diagnostici" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copier le diagnostic" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diagnosedaten kopieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copiar diagnóstico" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiar diagnóstico" + } + } + } + }, + "Copy log": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Copy log" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia log" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copier le journal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Protokoll kopieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copiar registro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Copiar log" + } + } + } + }, + "Could not build the GitHub API URL.": { + "comment": "UpstreamChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not build the GitHub API URL." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile comporre l’URL dell’API GitHub." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de construire l’URL de l’API GitHub." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die GitHub-API-URL konnte nicht erstellt werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido crear la URL de la API de GitHub." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível criar o URL da API do GitHub." + } + } + } + }, + "Could not change the app icon of %@: %@": { + "comment": "AppViewModel+AppIcon.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not change the app icon of %@: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile cambiare l’icona dell’app di %@: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de modifier l’icône de l’app %@ : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das App-Symbol von %@ konnte nicht geändert werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido cambiar el icono de la app de %@: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível alterar o ícone do app de %@: %@" + } + } + } + }, + "Could not import prod.keys: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not import prod.keys: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile importare prod.keys: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’importer prod.keys : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "prod.keys konnte nicht importiert werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido importar prod.keys: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível importar prod.keys: %@" + } + } + } + }, + "Could not install the new build of %@: %@": { + "comment": "AppViewModel+PendingInstall.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not install the new build of %@: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile installare la nuova compilazione di %@: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’installer la nouvelle compilation de %@ : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der neue Build von %@ konnte nicht installiert werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido instalar la nueva compilación de %@: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível instalar a nova compilação de %@: %@" + } + } + } + }, + "Could not load the library entry for %@.": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not load the library entry for %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile caricare la voce di %@ nella libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de charger l’entrée de %@ dans la bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Bibliothekseintrag für %@ konnte nicht geladen werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se pudo cargar la entrada de %@ en la biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível carregar a entrada de %@ na biblioteca." + } + } + } + }, + "Could not prepare the default Game apps folder: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not prepare the default Game apps folder: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile preparare la cartella predefinita App dei giochi: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de préparer le dossier Apps de jeux par défaut : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Standardordner für Spiele-Apps konnte nicht vorbereitet werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido preparar la carpeta predeterminada de apps de juegos: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível preparar a pasta padrão de Apps de jogos: %@" + } + } + } + }, + "Could not prepare the game": { + "comment": "AppViewModel+AppIcon.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not prepare the game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile preparare il gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de préparer le jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel konnte nicht vorbereitet werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido preparar el juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível preparar o jogo" + } + } + } + }, + "Could not read the library: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not read the library: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile leggere la libreria: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de lire la bibliothèque : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Bibliothek konnte nicht gelesen werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido leer la biblioteca: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível ler a biblioteca: %@" + } + } + } + }, + "Could not read this folder": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not read this folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile leggere questa cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de lire ce dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Ordner konnte nicht gelesen werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido leer esta carpeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível ler esta pasta" + } + } + } + }, + "Could not replace the app: %@": { + "comment": "BuildOutputInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not replace the app: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile sostituire l’app: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de remplacer l’app : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App konnte nicht ersetzt werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido reemplazar la app: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível substituir o app: %@" + } + } + } + }, + "Could not save settings for %@: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not save settings for %@: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile salvare le impostazioni di %@: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’enregistrer les réglages de %@ : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen für %@ konnten nicht gespeichert werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han podido guardar los ajustes de %@: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível salvar os ajustes de %@: %@" + } + } + } + }, + "Could not save the choice in %@.": { + "comment": "SavedChoicesStore.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not save the choice in %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile salvare la scelta in %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’enregistrer le choix dans %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Auswahl konnte nicht in %@ gespeichert werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido guardar la elección en %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível salvar a escolha em %@." + } + } + } + }, + "Could not save the library: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not save the library: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile salvare la libreria: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’enregistrer la bibliothèque : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Bibliothek konnte nicht gespeichert werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido guardar la biblioteca: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível salvar a biblioteca: %@" + } + } + } + }, + "Could not update the library: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not update the library: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile aggiornare la libreria: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de mettre à jour la bibliothèque : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Bibliothek konnte nicht aktualisiert werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido actualizar la biblioteca: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível atualizar a biblioteca: %@" + } + } + } + }, + "Could not use %@ for cloud saves.": { + "comment": "CloudSavesPreferences.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not use %@ for cloud saves." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile usare %@ per i salvataggi cloud." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’utiliser %@ pour les sauvegardes dans le cloud." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ konnte nicht für Cloud-Spielstände verwendet werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido usar %@ para las partidas en la nube." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível usar %@ para os dados salvos na nuvem." + } + } + } + }, + "Could not write the runtime environment: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Could not write the runtime environment: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile scrivere l’ambiente di esecuzione: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’écrire l’environnement d’exécution : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Laufzeitumgebung konnte nicht geschrieben werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido escribir el entorno de ejecución: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível gravar o ambiente de execução: %@" + } + } + } + }, + "Couldn't add the picture": { + "comment": "ThemePickerView.swift:25", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Couldn't add the picture" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile aggiungere l’immagine" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’ajouter l’image" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bild konnte nicht hinzugefügt werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido añadir la imagen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível adicionar a imagem" + } + } + } + }, + "Couldn't add the video": { + "comment": "ThemePickerView.swift:771", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Couldn't add the video" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile aggiungere il video" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible d’ajouter la vidéo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Video konnte nicht hinzugefügt werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido añadir el vídeo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível adicionar o vídeo" + } + } + } + }, + "Couldn't download the wallpaper": { + "comment": "ThemePickerView.swift:679", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Couldn't download the wallpaper" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile scaricare lo sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de télécharger le fond d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundbild konnte nicht geladen werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido descargar el fondo de pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível baixar o papel de parede" + } + } + } + }, + "Couldn't remove it": { + "comment": "ThemePickerView.swift:820", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Couldn't remove it" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimozione non riuscita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de supprimer cet élément" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Konnte nicht entfernt werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido eliminar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível remover" + } + } + } + }, + "Cover": { + "comment": "ArtworkPicker.swift:11", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cover" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copertina" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jaquette" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cover" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capa" + } + } + } + }, + "Cover, background, icon and logo, from GameTDB, Title DB or a file.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Cover, background, icon and logo, from GameTDB, Title DB or a file." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copertina, sfondo, icona e logo, da GameTDB, Title DB o un file." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jaquette, arrière-plan, icône et logo, depuis GameTDB, Title DB ou un fichier." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cover, Hintergrund, Symbol und Logo aus GameTDB, Title DB oder einer Datei." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portada, fondo, icono y logotipo, de GameTDB, Title DB o un archivo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capa, fundo, ícone e logotipo do GameTDB, do Title DB ou de um arquivo." + } + } + } + }, + "Credits": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Credits" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crediti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Crédits" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mitwirkende" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Créditos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Créditos" + } + } + } + }, + "Current commit": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Current commit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Commit attuale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commit actuel" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktueller Commit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Commit actual" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Commit atual" + } + } + } + }, + "D-pad": { + "comment": "ControlsView.swift:30", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "D-pad" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Croce direzionale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Croix directionnelle" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Steuerkreuz" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cruceta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Direcional" + } + } + } + }, + "Dark": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dark" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scuro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sombre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dunkel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Oscuro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escuro" + } + } + } + }, + "Dates, numbers and sizes only. Game descriptions follow the app language.": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dates, numbers and sizes only. Game descriptions follow the app language." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Solo date, numeri e dimensioni. Le descrizioni seguono la lingua dell’app." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Uniquement les dates, nombres et tailles. Les descriptions suivent la langue de l’app." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Datum, Zahlen und Größen. Spielbeschreibungen folgen der App-Sprache." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo fechas, números y tamaños. Las descripciones siguen el idioma de la app." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apenas datas, números e tamanhos. As descrições seguem o idioma do app." + } + } + } + }, + "Deep": { + "comment": "Background brightness/shade choice, not a stored identifier.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Deep" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Intenso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Très sombre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Stark abgedunkelt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Intenso" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Intenso" + } + } + } + }, + "Default controller vibration for new games.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Default controller vibration for new games." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Vibrazione predefinita del controller per i nuovi giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibrations de la manette par défaut pour les nouveaux jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Standard-Controller-Vibration für neue Spiele." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vibración predeterminada del mando para juegos nuevos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Vibração padrão do controle para novos jogos." + } + } + } + }, + "Default game inputs. Older builds may differ.": { + "comment": "ControlsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Default game inputs. Older builds may differ." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Comandi di gioco predefiniti. Le compilazioni precedenti possono differire." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commandes par défaut du jeu. Les anciennes compilations peuvent différer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Standardsteuerung im Spiel. Ältere Builds können abweichen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Controles predeterminados del juego. Pueden variar en compilaciones antiguas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Comandos padrão do jogo. Compilações antigas podem ser diferentes." + } + } + } + }, + "Default haptic feedback multiplier (0: muted, 1: subtle, 2: standard, 3: intense) passed to game builds supporting macOS controller haptics.": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Default haptic feedback multiplier (0: muted, 1: subtle, 2: standard, 3: intense) passed to game builds supporting macOS controller haptics." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Moltiplicatore predefinito del feedback aptico (0: disattivato, 1: lieve, 2: standard, 3: intenso), passato alle compilazioni dei giochi che supportano il feedback aptico dei controller su macOS." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Multiplicateur de retour haptique par défaut (0 : désactivé, 1 : léger, 2 : standard, 3 : intense) transmis aux compilations prenant en charge les vibrations des manettes sous macOS." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Standardmultiplikator für haptisches Feedback (0: aus, 1: dezent, 2: normal, 3: stark). Wird an Spiele-Builds übergeben, die macOS-Controller-Haptik unterstützen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Multiplicador de respuesta háptica predeterminado (0: desactivada, 1: sutil, 2: normal, 3: intensa) que se pasa a las compilaciones compatibles con la vibración de mandos de macOS." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Multiplicador padrão da resposta tátil (0: sem vibração, 1: sutil, 2: padrão, 3: intensa), enviado às compilações de jogos compatíveis com resposta tátil de controles no macOS." + } + } + } + }, + "Default vibration strength for new games.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Default vibration strength for new games." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Intensità predefinita della vibrazione per i nuovi giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intensité des vibrations par défaut pour les nouveaux jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Standard-Vibrationsstärke für neue Spiele." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Intensidad de vibración predeterminada para juegos nuevos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Intensidade padrão da vibração para novos jogos." + } + } + } + }, + "Defaults are in use. Your previous settings file has been kept.": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Defaults are in use. Your previous settings file has been kept." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sono in uso i valori predefiniti. Il file delle impostazioni precedenti è stato conservato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les réglages par défaut sont utilisés. Votre ancien fichier de réglages a été conservé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Standardeinstellungen werden verwendet. Deine bisherige Einstellungsdatei bleibt erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se usan los valores predeterminados. Se ha conservado tu archivo de ajustes anterior." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os ajustes padrão estão em uso. Seu arquivo de ajustes anterior foi mantido." + } + } + } + }, + "Delete": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Elimina" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Löschen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagar" + } + } + } + }, + "Delete %@ of build cache for %@? This permanently removes extracted and compiled build files. The next rebuild may take longer. The game app, original game files, saves and logs are kept.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete %@ of build cache for %@? This permanently removes extracted and compiled build files. The next rebuild may take longer. The game app, original game files, saves and logs are kept." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Eliminare %@ di cache di compilazione per %@? I file di compilazione estratti e compilati verranno rimossi definitivamente. La prossima ricompilazione potrebbe richiedere più tempo. L’app del gioco, i file originali del gioco, i salvataggi e i log vengono conservati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer %@ de cache de compilation pour %@ ? Cette action supprime définitivement les fichiers de compilation extraits et compilés. La prochaine recompilation peut prendre plus de temps. L’app de jeu, les fichiers de jeu d’origine, les sauvegardes et les journaux sont conservés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Build-Cache für %@ löschen? Dadurch werden entpackte und kompilierte Build-Dateien dauerhaft entfernt. Die nächste Neuerstellung kann länger dauern. Spiele-App, ursprüngliche Spieldateien, Spielstände und Protokolle bleiben erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar %@ de caché de compilación de %@? Se eliminarán permanentemente los archivos extraídos y compilados. La próxima recompilación podría tardar más. Se conservan la app del juego, los archivos originales, las partidas guardadas y los registros." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagar %@ de cache de compilação de %@? Isso remove permanentemente os arquivos de compilação extraídos e compilados. A próxima recompilação pode demorar mais. O app do jogo, os arquivos originais do jogo, os dados salvos e os logs serão mantidos." + } + } + } + }, + "Delete %@?": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete %@?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Eliminare %@?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer %@ ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ löschen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar %@?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagar %@?" + } + } + } + }, + "Delete a game's build cache or clear all caches after confirmation; rebuilds may take longer.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete a game's build cache or clear all caches after confirmation; rebuilds may take longer." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Elimina la cache di compilazione di un gioco o tutte le cache dopo conferma; le ricompilazioni potrebbero richiedere più tempo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer le cache de compilation d’un jeu ou tous les caches après confirmation ; les recompilations peuvent prendre plus de temps." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build-Cache eines Spiels oder alle Caches nach Bestätigung löschen; Neuerstellungen können länger dauern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elimina la caché de compilación de un juego o todas las cachés tras confirmar; las recompilaciones podrían tardar más." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apague o cache de compilação de um jogo ou todos os caches após confirmação; as recompilações podem demorar mais." + } + } + } + }, + "Delete all": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete all" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Elimina tutto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout supprimer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle löschen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminar todo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagar tudo" + } + } + } + }, + "Delete build and launch logs after confirmation.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete build and launch logs after confirmation." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Elimina i log di compilazione e avvio dopo conferma." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les journaux de compilation et de lancement après confirmation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build- und Startprotokolle nach Bestätigung löschen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elimina los registros de compilación e inicio tras confirmar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apague logs de compilação e execução após confirmação." + } + } + } + }, + "Delete build caches?": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete build caches?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Eliminare le cache di compilazione?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les caches de compilation ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build-Caches löschen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar cachés de compilación?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagar caches de compilação?" + } + } + } + }, + "Delete cache": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete cache" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Elimina cache" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer le cache" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cache löschen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminar caché" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagar cache" + } + } + } + }, + "Delete cache for %@?": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete cache for %@?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Eliminare la cache di %@?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer le cache de %@ ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cache für %@ löschen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar la caché de %@?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Excluir o cache de %@?" + } + } + } + }, + "Delete logs?": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete logs?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Eliminare i log?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les journaux ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Protokolle löschen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar registros?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagar logs?" + } + } + } + }, + "Delete this game's cache?": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Delete this game's cache?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Eliminare la cache di questo gioco?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer le cache de ce jeu ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cache dieses Spiels löschen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar la caché de este juego?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagar o cache deste jogo?" + } + } + } + }, + "Deleted %lld build caches. Future rebuilds may take longer.": { + "comment": "Quantity-sensitive UI message; preserve %lld count and other placeholders.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Deleted %lld build cache. Future rebuilds may take longer." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Deleted %lld build caches. Future rebuilds may take longer." + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Eliminata %lld cache di compilazione. Le prossime ricompilazioni potrebbero richiedere più tempo." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Eliminate %lld cache di compilazione. Le prossime ricompilazioni potrebbero richiedere più tempo." + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Suppression de %lld cache de compilation effectuée. Les prochaines recompilations peuvent prendre plus de temps." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Suppression de %lld caches de compilation effectuée. Les prochaines recompilations peuvent prendre plus de temps." + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Build-Cache gelöscht. Künftige Neuerstellungen können länger dauern." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Build-Caches gelöscht. Künftige Neuerstellungen können länger dauern." + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Se ha eliminado %lld caché de compilación. Las próximas recompilaciones podrían tardar más." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Se han eliminado %lld cachés de compilación. Las próximas recompilaciones podrían tardar más." + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld cache de compilação apagado. As próximas recompilações podem demorar mais." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld caches de compilação apagados. As próximas recompilações podem demorar mais." + } + } + } + } + } + } + }, + "Deleted logs for %lld games.": { + "comment": "Quantity-sensitive UI message; preserve %lld count and other placeholders.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Deleted logs for %lld game." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Deleted logs for %lld games." + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Eliminati i log di %lld gioco." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Eliminati i log di %lld giochi." + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Journaux de %lld jeu supprimés." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Journaux de %lld jeux supprimés." + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Protokolle für %lld Spiel gelöscht." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Protokolle für %lld Spiele gelöscht." + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Se han eliminado los registros de %lld juego." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Se han eliminado los registros de %lld juegos." + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Logs de %lld jogo apagados." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Logs de %lld jogos apagados." + } + } + } + } + } + } + }, + "Deletes only the source files named in this game and its app-owned workspace. The app and saves stay.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Deletes only the source files named in this game and its app-owned workspace. The app and saves stay." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Elimina solo i file sorgente indicati in questo gioco e la sua area di lavoro gestita dall’app. L’app e i salvataggi restano." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprime uniquement les fichiers sources référencés pour ce jeu et son espace de travail géré par l’app. L’app et les sauvegardes restent en place." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Löscht nur die diesem Spiel zugeordneten Quelldateien und sein app-eigenes Arbeitsverzeichnis. App und Spielstände bleiben erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elimina solo los archivos de origen indicados en este juego y su espacio de trabajo propiedad de la app. La app y las partidas guardadas se conservan." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apaga apenas os arquivos de origem indicados neste jogo e sua área de trabalho pertencente ao app. O app e os dados salvos são mantidos." + } + } + } + }, + "Deleting files…": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Deleting files…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Eliminazione dei file…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Suppression des fichiers…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dateien werden gelöscht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminando archivos…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apagando arquivos…" + } + } + } + }, + "Depends on the game, build and controller": { + "comment": "ControlsView.swift:40", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Depends on the game, build and controller" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dipende dal gioco, dalla compilazione e dal controller" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dépend du jeu, de la compilation et de la manette" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hängt vom Spiel, Build und Controller ab" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Depende del juego, la compilación y el mando" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Depende do jogo, da compilação e do controle" + } + } + } + }, + "Description": { + "comment": "MetadataPicker.swift:10", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Description" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Descrizione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Description" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beschreibung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descripción" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descrição" + } + } + } + }, + "Description in %@.": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Description in %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Descrizione in %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Description en %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beschreibung auf %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descripción en %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descrição em %@." + } + } + } + }, + "Description language unavailable.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Description language unavailable." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lingua della descrizione non disponibile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Langue de la description indisponible." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beschreibungssprache nicht verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Idioma de la descripción no disponible." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Idioma da descrição indisponível." + } + } + } + }, + "Description, genres, developer, publisher and release date.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Description, genres, developer, publisher and release date." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Descrizione, generi, sviluppatore, editore e data di uscita." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Description, genres, développeur, éditeur et date de sortie." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beschreibung, Genres, Entwickler, Publisher und Veröffentlichungsdatum." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descripción, géneros, desarrolladora, editora y fecha de lanzamiento." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descrição, gêneros, desenvolvedora, publicadora e data de lançamento." + } + } + } + }, + "Descriptions, genres and artwork are fetched from the public Nintendo Store catalog for personal use, cached locally for seven days, and never committed or redistributed.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Descriptions, genres and artwork are fetched from the public Nintendo Store catalog for personal use, cached locally for seven days, and never committed or redistributed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Descrizioni, generi e immagini vengono recuperati dal catalogo pubblico del Nintendo Store per uso personale, conservati nella cache locale per sette giorni e mai inseriti nel repository o ridistribuiti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les descriptions, genres et illustrations sont récupérés depuis le catalogue public du Nintendo Store pour un usage personnel, mis en cache localement pendant sept jours, et jamais intégrés au dépôt ni redistribués." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beschreibungen, Genres und Artwork werden für den persönlichen Gebrauch aus dem öffentlichen Nintendo Store-Katalog geladen, sieben Tage lokal zwischengespeichert und weder ins Repository aufgenommen noch weiterverbreitet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las descripciones, los géneros y las imágenes se obtienen del catálogo público de Nintendo Store para uso personal, se guardan en la caché local durante siete días y nunca se incorporan al repositorio ni se redistribuyen." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descrições, gêneros e artes são obtidos do catálogo público da Nintendo Store para uso pessoal, armazenados em cache local por sete dias e nunca incluídos no repositório ou redistribuídos." + } + } + } + }, + "Destination for imports only.": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Destination for imports only." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Destinazione solo per le importazioni." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Destination pour les importations uniquement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ziel nur für Importe." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Destino solo para importaciones." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Destino apenas para importações." + } + } + } + }, + "Details": { + "comment": "ThemePreview.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Details" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dettagli" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détails" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Details" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Detalles" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Detalhes" + } + } + } + }, + "Details · Launch · Home: wallpaper": { + "comment": "ChangeArtworkView.swift:313", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Details · Launch · Home: wallpaper" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dettagli · Avvio · Schermata iniziale: sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détails · Lancement · Accueil : fond d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Details · Spielstart · Startseite: Hintergrundbild" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Detalles · Inicio · Pantalla principal: fondo de pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Detalhes · Abertura · Início: papel de parede" + } + } + } + }, + "Detect": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Detect" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rileva" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détecter" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erkennen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Detectar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Detectar" + } + } + } + }, + "Detect Ryujinx...": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Detect Ryujinx..." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rileva Ryujinx..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détecter Ryujinx..." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ryujinx erkennen..." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Detectar Ryujinx..." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Detectar Ryujinx..." + } + } + } + }, + "Detected %lld games in %@.": { + "comment": "Quantity-sensitive UI message; preserve %lld count and other placeholders.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Detected %lld game in %@." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Detected %lld games in %@." + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Rilevato %lld gioco in %@." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Rilevati %lld giochi in %@." + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld jeu détecté dans %@." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld jeux détectés dans %@." + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Spiel in %@ erkannt." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Spiele in %@ erkannt." + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Se ha detectado %lld juego en %@." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Se han detectado %lld juegos en %@." + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld jogo detectado em %@." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld jogos detectados em %@." + } + } + } + } + } + } + }, + "Developer": { + "comment": "MetadataPicker.swift:12", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Developer" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sviluppatore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Développeur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Entwickler" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desarrolladora" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desenvolvedora" + } + } + } + }, + "Developer build: %@": { + "comment": "SettingsEngineSection.swift:499", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Developer build: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilazione di sviluppo: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilation de développement : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Entwicklungs-Build: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación de desarrollo: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilação de desenvolvimento: %@" + } + } + } + }, + "Development copy · manual checks only": { + "comment": "SettingsAboutSection.swift:219", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Development copy · manual checks only" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copia di sviluppo · solo verifiche manuali" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copie de développement · vérifications manuelles uniquement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Entwicklungsversion · nur manuelle Update-Suche" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copia de desarrollo · solo comprobaciones manuales" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cópia de desenvolvimento · apenas buscas manuais de atualizações" + } + } + } + }, + "Device save": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Device save" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salvataggio del dispositivo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde de l’appareil" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gerätespielstand" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Partida del dispositivo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Salvamento do dispositivo" + } + } + } + }, + "Didn't install": { + "comment": "DetailsBands.swift:296", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Didn't install" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installazione non riuscita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non installé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht installiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha instalado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi instalado" + } + } + } + }, + "Dim": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dim" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scuro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sombre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Abgedunkelt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tenue" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escurecido" + } + } + } + }, + "Dim applies an even shade; Bright on the right keeps the left dim for text.": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dim applies an even shade; Bright on the right keeps the left dim for text." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scuro applica un’ombreggiatura uniforme; Luminoso a destra mantiene scura la parte sinistra per il testo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sombre applique un voile uniforme ; Clair à droite garde la gauche sombre pour le texte." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Abgedunkelt“ dunkelt gleichmäßig ab; „Rechts hell“ lässt die linke Seite für Text dunkel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tenue aplica una sombra uniforme; Brillo a la derecha mantiene el lado izquierdo oscuro para el texto." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escurecido aplica uma sombra uniforme; Claro à direita mantém o lado esquerdo escuro para o texto." + } + } + } + }, + "Discard": { + "comment": "AddGameView.swift:66", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Discard" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scarta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Abandonner" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verwerfen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descartar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descartar" + } + } + } + }, + "Discard selected sources?": { + "comment": "AddGameView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Discard selected sources?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scartare le sorgenti selezionate?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Abandonner les sources sélectionnées ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausgewählte Quellen verwerfen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Descartar los archivos de origen seleccionados?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descartar arquivos de origem selecionados?" + } + } + } + }, + "Display mode": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Display mode" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Modalità di visualizzazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode d’affichage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anzeigemodus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Modo de visualización" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Modo de exibição" + } + } + } + }, + "Displays Apple's official Metal Performance HUD overlay during gameplay showing real-time frame rates, frame time graphs, and GPU utilization.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Displays Apple's official Metal Performance HUD overlay during gameplay showing real-time frame rates, frame time graphs, and GPU utilization." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra durante il gioco l’HUD ufficiale Metal Performance HUD di Apple, con frequenza dei fotogrammi in tempo reale, grafici dei tempi dei fotogrammi e utilizzo della GPU." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche en jeu la surimpression officielle Metal Performance HUD d’Apple : fréquence d’images en temps réel, graphiques du temps par image et utilisation du GPU." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt während des Spielens Apples offizielles Metal Performance HUD mit aktueller Bildrate, Framezeitdiagrammen und GPU-Auslastung." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra durante el juego el HUD oficial de rendimiento de Metal de Apple con los fotogramas por segundo en tiempo real, gráficos de tiempo por fotograma y uso de GPU." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Exibe a sobreposição oficial Metal Performance HUD da Apple durante o jogo, mostrando a taxa de quadros, gráficos de tempo por quadro e o uso da GPU em tempo real." + } + } + } + }, + "Displays Apple's real-time Metal developer HUD (DM_METAL_HUD=1). Shows frame rate, frame pacing graphs, GPU time per frame in milliseconds, and memory allocation.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Displays Apple's real-time Metal developer HUD (DM_METAL_HUD=1). Shows frame rate, frame pacing graphs, GPU time per frame in milliseconds, and memory allocation." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra l’HUD Metal per sviluppatori di Apple in tempo reale (DM_METAL_HUD=1). Mostra frequenza dei fotogrammi, grafici della regolarità dei fotogrammi, tempo GPU per fotogramma in millisecondi e memoria allocata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche le HUD Metal de développement d’Apple en temps réel (DM_METAL_HUD=1). Indique la fréquence d’images, les graphiques de régularité des images, le temps GPU par image en millisecondes et l’allocation mémoire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt Apples Metal-Entwickler-HUD in Echtzeit (DM_METAL_HUD=1). Zeigt Bildrate, Diagramme zum Frame-Pacing, GPU-Zeit pro Frame in Millisekunden und Speicherbelegung." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra el HUD de desarrollo de Metal de Apple en tiempo real (DM_METAL_HUD=1). Indica los fotogramas por segundo, gráficos de regularidad de fotogramas, tiempo de GPU por fotograma en milisegundos y memoria asignada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Exibe o HUD de desenvolvimento Metal da Apple em tempo real (DM_METAL_HUD=1). Mostra a taxa de quadros, gráficos de cadência dos quadros, tempo de GPU por quadro em milissegundos e alocação de memória." + } + } + } + }, + "Dock & app": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dock & app" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dock e app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dock et app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dock & App" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Dock y app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dock e app" + } + } + } + }, + "Docked": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Docked" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dock" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Téléviseur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dock-Modus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Televisor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Modo TV" + } + } + } + }, + "Docked mode": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Docked mode" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Modalità dock" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode téléviseur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dock-Modus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Modo televisor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Modo TV" + } + } + } + }, + "Docked uses the game's TV profile; Handheld uses its portable profile.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Docked uses the game's TV profile; Handheld uses its portable profile." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dock usa il profilo TV del gioco; Portatile usa il suo profilo portatile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Téléviseur utilise le profil TV du jeu ; Portable utilise son profil portable." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Dock-Modus“ verwendet das TV-Profil des Spiels; „Handheld“ das Profil für unterwegs." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Televisor usa el perfil de TV del juego; Portátil usa su perfil portátil." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Modo TV usa o perfil de TV do jogo; Portátil usa o perfil portátil." + } + } + } + }, + "Docs unavailable": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Docs unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Documentazione non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Documentation indisponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dokumentation nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Documentación no disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Documentação indisponível" + } + } + } + }, + "Done": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Done" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Fine" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fertig" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Hecho" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "OK" + } + } + } + }, + "Double-clickable native macOS app for Apple Silicon.": { + "comment": "TargetPlatform.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Double-clickable native macOS app for Apple Silicon." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App nativa per macOS su Apple Silicon, avviabile con un doppio clic." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "App macOS native pour Apple Silicon, lançable par double-clic." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Native macOS-App für Apple Silicon, per Doppelklick startbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "App nativa de macOS para Apple Silicon que se abre con doble clic." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "App nativo de macOS para Apple Silicon que pode ser aberto com dois cliques." + } + } + } + }, + "Download": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Download" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scarica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Télécharger" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Laden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descargar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Baixar" + } + } + } + }, + "Downloading %@%%": { + "comment": "SettingsAboutSection.swift:228", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Downloading %@%%" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Download %@%%" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Téléchargement %@%%" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird geladen: %@%%" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descargando %@%%" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Baixando %@%%" + } + } + } + }, + "Downloading the full image from %@…": { + "comment": "ChangeArtworkView.swift:294", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Downloading the full image from %@…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Download dell’immagine completa da %@…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Téléchargement de l’image complète depuis %@…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vollständiges Bild wird von %@ geladen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descargando la imagen completa de %@…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Baixando a imagem completa de %@…" + } + } + } + }, + "Downloading update": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Downloading update" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Download dell’aggiornamento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Téléchargement de la mise à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Update wird geladen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descargando actualización" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Baixando atualização" + } + } + } + }, + "Drop": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Drop" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rilascia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déposer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ablegen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Soltar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Soltar" + } + } + } + }, + "Drop to add these files": { + "comment": "AddGameView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Drop to add these files" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rilascia per aggiungere questi file" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déposez pour ajouter ces fichiers" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ablegen, um diese Dateien hinzuzufügen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Suelta estos archivos para añadirlos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Solte para adicionar estes arquivos" + } + } + } + }, + "Drop to scan this folder": { + "comment": "DropZoneView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Drop to scan this folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rilascia per analizzare questa cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déposez pour analyser ce dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ablegen, um diesen Ordner zu durchsuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Suelta para analizar esta carpeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Solte para examinar esta pasta" + } + } + } + }, + "Drop your game files here": { + "comment": "DropZoneView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Drop your game files here" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Trascina qui i file del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déposez les fichiers du jeu ici" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lege deine Spieldateien hier ab" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Suelta aquí tus archivos de juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Solte os arquivos do jogo aqui" + } + } + } + }, + "Dump or download the game again, or port anyway with the damaged files as read.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dump or download the game again, or port anyway with the damaged files as read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esegui di nuovo il dump o il download del gioco, oppure crea comunque l’app per Mac usando i file danneggiati così come vengono letti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Extrayez ou téléchargez à nouveau le jeu, ou créez l’app Mac quand même avec les fichiers endommagés tels qu’ils sont lus." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstelle erneut einen Dump des Spiels, lade es erneut herunter oder erstelle die App trotzdem mit den beschädigten Dateien in ihrer eingelesenen Form." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a volcar o descargar el juego, o compila de todos modos con los archivos dañados tal como se han leído." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Extraia ou baixe o jogo novamente ou compile mesmo assim usando os arquivos danificados conforme foram lidos." + } + } + } + }, + "Dump or download the game again.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dump or download the game again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esegui di nuovo il dump o il download del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Extrayez ou téléchargez à nouveau le jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstelle erneut einen Dump des Spiels oder lade es erneut herunter." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a volcar o descargar el juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Extraia ou baixe o jogo novamente." + } + } + } + }, + "Dump prod.keys again from a console on firmware %@ or later.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dump prod.keys again from a console on firmware %@ or later." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Estrai di nuovo prod.keys da una console con firmware %@ o successivo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Extrayez à nouveau prod.keys depuis une console dotée du firmware %@ ou ultérieur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstelle erneut einen Dump von prod.keys auf einer Konsole mit Firmware %@ oder neuer." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a volcar prod.keys de una consola con firmware %@ o posterior." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Extraia prod.keys novamente de um console com firmware %@ ou posterior." + } + } + } + }, + "Dump prod.keys again from an up-to-date console.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dump prod.keys again from an up-to-date console." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Estrai di nuovo prod.keys da una console aggiornata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Extrayez à nouveau prod.keys depuis une console à jour." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstelle erneut einen Dump von prod.keys auf einer Konsole mit aktueller Firmware." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a volcar prod.keys de una consola actualizada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Extraia prod.keys novamente de um console atualizado." + } + } + } + }, + "Dynamic resolution": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dynamic resolution" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Risoluzione dinamica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résolution dynamique" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dynamische Auflösung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Resolución dinámica" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Resolução dinâmica" + } + } + } + }, + "E": { + "comment": "ButtonPrompts.swift:40", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "E" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "E" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "E" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "E" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "E" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "E" + } + } + } + }, + "Each card shows how it is going.": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Each card shows how it is going." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ogni scheda mostra l’avanzamento." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque carte indique la progression." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jede Karte zeigt den Fortschritt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cada tarjeta muestra el progreso." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cada cartão mostra o andamento." + } + } + } + }, + "Each porting needs one game; this folder holds files of several.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Each porting needs one game; this folder holds files of several." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ogni creazione di app per Mac richiede un solo gioco; questa cartella contiene file di più giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaque création d’app Mac nécessite un seul jeu ; ce dossier contient les fichiers de plusieurs jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für jede App-Erstellung wird ein einzelnes Spiel benötigt; dieser Ordner enthält Dateien mehrerer Spiele." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cada compilación necesita un solo juego; esta carpeta contiene archivos de varios." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cada compilação precisa de um jogo; esta pasta contém arquivos de vários." + } + } + } + }, + "Edit": { + "comment": "AddGameView.swift:296", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Edit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Modifica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bearbeiten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Editar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Editar" + } + } + } + }, + "Edit details": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Edit details" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Modifica dettagli" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Modifier les détails" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Details bearbeiten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Editar detalles" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Editar detalhes" + } + } + } + }, + "Empty for Automatic": { + "comment": "ChangeMetadataView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Empty for Automatic" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lascia vuoto per Automatico" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Laisser vide pour Automatique" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Leer lassen für „Automatisch“" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vacío para Automático" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Deixe em branco para Automático" + } + } + } + }, + "Enable": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Enable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Abilita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktivieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Activar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ativar" + } + } + } + }, + "Enable untested toolchain?": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Enable untested toolchain?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Abilitare la toolchain non testata?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer la chaîne d’outils non testée ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ungetestete Toolchain aktivieren?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Activar la cadena de herramientas no probada?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Permitir conjunto de ferramentas não testado?" + } + } + } + }, + "Encode thread": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Encode thread" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Thread di codifica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Thread d’encodage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kodierungs-Thread" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Hilo de codificación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Thread de codificação" + } + } + } + }, + "Engine": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine" + } + } + } + }, + "Engine %@": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moteur %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine %@" + } + } + } + }, + "Engine %@ nxnative main %@ (%@); builds are recorded as untested.": { + "comment": "AppViewModel+NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine %@ nxnative main %@ (%@); builds are recorded as untested." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore %@ nxnative main %@ (%@); le compilazioni sono registrate come non testate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moteur %@ nxnative main %@ (%@) ; les compilations sont enregistrées comme non testées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine %@ nxnative main %@ (%@); Builds werden als ungetestet erfasst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor %@ nxnative main %@ (%@); las compilaciones se registran como no probadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine %@ nxnative main %@ (%@); as compilações são registradas como não testadas." + } + } + } + }, + "Engine %@ · %@ · %@ · %@": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine %@ · %@ · %@ · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore %@ · %@ · %@ · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moteur %@ · %@ · %@ · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine %@ · %@ · %@ · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor %@ · %@ · %@ · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine %@ · %@ · %@ · %@" + } + } + } + }, + "Engine back to the tested %@.": { + "comment": "AppViewModel+NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine back to the tested %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore riportato alla versione testata %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moteur rétabli à la version testée %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine wieder auf der getesteten Version %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El motor vuelve a la versión probada %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A engine voltou à versão testada %@." + } + } + } + }, + "Engine commit": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine commit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Commit del motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commit du moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine-Commit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Commit del motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Commit da engine" + } + } + } + }, + "Engine commit, Metal compiler, toolchain gate and required flags.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine commit, Metal compiler, toolchain gate and required flags." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Commit del motore, compilatore Metal, controllo della toolchain e flag richiesti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commit du moteur, compilateur Metal, contrôle de la chaîne d’outils et options requises." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine-Commit, Metal-Compiler, Toolchain-Prüfung und erforderliche Optionen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Commit del motor, compilador Metal, control de la cadena de herramientas y opciones necesarias." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Commit da engine, compilador Metal, verificação do conjunto de ferramentas e opções obrigatórias." + } + } + } + }, + "Engine pin, toolchain gate and keys.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine pin, toolchain gate and keys." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione fissata del motore, controllo della toolchain e chiavi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version épinglée du moteur, contrôle de la chaîne d’outils et clés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Festgelegte Engine-Version, Toolchain-Prüfung und Schlüssel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión fijada del motor, control de la cadena de herramientas y claves." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão fixada da engine, verificação do conjunto de ferramentas e chaves." + } + } + } + }, + "Engine repository": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine repository" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Repository del motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dépôt du moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine-Repository" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Repositorio del motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Repositório da engine" + } + } + } + }, + "Engine switched to nxnative main %@ (%@); builds are recorded as untested.": { + "comment": "Complete engine-selection status sentence; parameters are commits and ahead-count text.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine switched to nxnative main %@ (%@); builds are recorded as untested." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore passato a nxnative main %@ (%@); le compilazioni sono registrate come non testate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moteur basculé vers nxnative main %@ (%@) ; les compilations sont enregistrées comme non testées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine zu nxnative main %@ (%@) gewechselt; Builds werden als ungetestet erfasst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor cambiado a nxnative main %@ (%@); las compilaciones se registran como no probadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine trocada para nxnative main %@ (%@); as compilações são registradas como não testadas." + } + } + } + }, + "Engine updated from %@ to nxnative main %@ (%@); builds are recorded as untested.": { + "comment": "Complete engine-selection status sentence; parameters are commits and ahead-count text.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Engine updated from %@ to nxnative main %@ (%@); builds are recorded as untested." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore aggiornato da %@ a nxnative main %@ (%@); le compilazioni sono registrate come non testate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moteur mis à jour de %@ vers nxnative main %@ (%@) ; les compilations sont enregistrées comme non testées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine von %@ auf nxnative main %@ (%@) aktualisiert; Builds werden als ungetestet erfasst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor actualizado de %@ a nxnative main %@ (%@); las compilaciones se registran como no probadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine atualizada de %@ para nxnative main %@ (%@); as compilações são registradas como não testadas." + } + } + } + }, + "English description shown.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "English description shown." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "È mostrata la descrizione in inglese." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Description affichée en anglais." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Englische Beschreibung wird angezeigt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se muestra la descripción en inglés." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descrição em inglês exibida." + } + } + } + }, + "Enlarges a game's finished picture, menus and HUD included, to your window or screen with Apple's MetalFX instead of smooth scaling: sharper edges and text, for a little more GPU time. A game whose own upscaler can use MetalFX offers that choice in its Game options, as Game's upscaler.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Enlarges a game's finished picture, menus and HUD included, to your window or screen with Apple's MetalFX instead of smooth scaling: sharper edges and text, for a little more GPU time. A game whose own upscaler can use MetalFX offers that choice in its Game options, as Game's upscaler." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ingrandisce l’immagine finale del gioco, inclusi menu e HUD, fino alle dimensioni della finestra o dello schermo usando MetalFX di Apple al posto del ridimensionamento morbido: bordi e testo più nitidi, con un lieve aumento del tempo GPU. Se l’upscaler interno di un gioco può usare MetalFX, l’opzione appare nelle sue Opzioni del gioco come Upscaler del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Agrandit l’image finale du jeu, menus et interface compris, à la taille de votre fenêtre ou de votre écran avec MetalFX d’Apple, au lieu du redimensionnement lissé : contours et texte plus nets, au prix d’un peu plus de temps GPU. Si le système de mise à l’échelle interne du jeu peut utiliser MetalFX, ce choix apparaît dans ses Options du jeu, sous Mise à l’échelle du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vergrößert das fertig gerenderte Spielbild einschließlich Menüs und HUD mit Apples MetalFX auf Fenster- oder Bildschirmgröße statt mit glatter Skalierung: schärfere Kanten und Texte bei etwas mehr GPU-Zeit. Kann der eigene Upscaler eines Spiels MetalFX nutzen, bietet es diese Wahl in den Spieloptionen unter „Upscaler des Spiels“ an." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Amplía la imagen final del juego, incluidos los menús y el HUD, a tu ventana o pantalla con MetalFX de Apple en lugar de un escalado suave: bordes y texto más nítidos, a cambio de algo más de tiempo de GPU. Si el reescalador propio de un juego admite MetalFX, esa opción aparece en sus opciones como «Reescalador del juego»." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Amplia a imagem final do jogo, incluindo menus e HUD, para sua janela ou tela usando o MetalFX da Apple em vez da ampliação suave: bordas e textos mais nítidos, com um pequeno aumento no tempo de GPU. Jogos cujo próprio ampliador de imagem pode usar MetalFX oferecem essa escolha nas Opções do jogo, em Ampliador de imagem do jogo." + } + } + } + }, + "Enlarges the game's finished picture, menus and HUD included, to your window or screen with Apple's MetalFX instead of smooth scaling: sharper edges and text, for a little more GPU time. Nothing changes when the picture is already as large as the screen. Not the same as Game's upscaler. (DM_PRESENT_UPSCALE)": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Enlarges the game's finished picture, menus and HUD included, to your window or screen with Apple's MetalFX instead of smooth scaling: sharper edges and text, for a little more GPU time. Nothing changes when the picture is already as large as the screen. Not the same as Game's upscaler. (DM_PRESENT_UPSCALE)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ingrandisce l’immagine finale del gioco, inclusi menu e HUD, fino alle dimensioni della finestra o dello schermo usando MetalFX di Apple al posto del ridimensionamento morbido: bordi e testo più nitidi, con un lieve aumento del tempo GPU. Non cambia nulla quando l’immagine è già grande quanto lo schermo. È diverso da Upscaler del gioco. (DM_PRESENT_UPSCALE)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Agrandit l’image finale du jeu, menus et interface compris, à la taille de votre fenêtre ou de votre écran avec MetalFX d’Apple, au lieu du redimensionnement lissé : contours et texte plus nets, au prix d’un peu plus de temps GPU. Aucun changement si l’image est déjà aussi grande que l’écran. Distinct de Mise à l’échelle du jeu. (DM_PRESENT_UPSCALE)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vergrößert das fertige Spielbild einschließlich Menüs und HUD mit Apples MetalFX auf Fenster- oder Bildschirmgröße statt mit glatter Skalierung: schärfere Kanten und Texte bei etwas mehr GPU-Zeit. Ist das Bild bereits so groß wie der Bildschirm, ändert sich nichts. Dies ist eine andere Funktion als „Upscaler des Spiels“. (DM_PRESENT_UPSCALE)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Amplía la imagen final del juego, incluidos los menús y el HUD, a tu ventana o pantalla con MetalFX de Apple en lugar de un escalado suave: bordes y texto más nítidos, a cambio de algo más de tiempo de GPU. No cambia nada si la imagen ya tiene el tamaño de la pantalla. Es una función distinta de Reescalador del juego. (DM_PRESENT_UPSCALE)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Amplia a imagem final do jogo, incluindo menus e HUD, para sua janela ou tela usando o MetalFX da Apple em vez da ampliação suave: bordas e textos mais nítidos, com um pequeno aumento no tempo de GPU. Nada muda quando a imagem já tem o tamanho da tela. É diferente do Ampliador de imagem do jogo. (DM_PRESENT_UPSCALE)" + } + } + } + }, + "Environment": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Environment" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ambiente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Environnement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Umgebung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Entorno" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ambiente" + } + } + } + }, + "Errors": { + "comment": "ConsoleView.swift:16", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Errors" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Errori" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Erreurs" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fehler" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Errores" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Erros" + } + } + } + }, + "Escape · ": { + "comment": "ControlsView.swift:48", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Escape · " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esc · " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échap · " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Escape · " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Escape · " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escape · " + } + } + } + }, + "Every cheat off; the Cheats switch stays as it is.": { + "comment": "CheatsPanel.swift:294", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Every cheat off; the Cheats switch stays as it is." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattiva tutti i trucchi; l’interruttore Trucchi resta invariato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tous les codes de triche désactivés ; l’interrupteur Codes de triche reste inchangé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deaktiviert alle einzelnen Cheats; der Schalter „Cheats“ bleibt unverändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactiva todos los trucos; el interruptor Trucos se mantiene como está." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desativa todas as trapaças; a opção Trapaças permanece como está." + } + } + } + }, + "Every cheat starts off.": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Every cheat starts off." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutti i trucchi sono inizialmente disattivati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tous les codes de triche sont désactivés au départ." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Cheats sind zunächst deaktiviert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Todos los trucos empiezan desactivados." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Todas as trapaças começam desativadas." + } + } + } + }, + "Every file, comments and credits": { + "comment": "ModDetailPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Every file, comments and credits" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutti i file, i commenti e i crediti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tous les fichiers, commentaires et crédits" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Dateien, Kommentare und Danksagungen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Todos los archivos, comentarios y créditos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Todos os arquivos, comentários e créditos" + } + } + } + }, + "Every game in your library has been played at least once.": { + "comment": "SpotlightRowView.swift:195", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Every game in your library has been played at least once." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ogni gioco della libreria è stato giocato almeno una volta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous avez joué au moins une fois à chaque jeu de votre bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jedes Spiel in deiner Bibliothek wurde mindestens einmal gespielt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Has jugado al menos una vez a todos los juegos de tu biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Todos os jogos da sua biblioteca foram jogados pelo menos uma vez." + } + } + } + }, + "Every game in your library, in one row.": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Every game in your library, in one row." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutti i giochi della libreria, in un’unica riga." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tous les jeux de votre bibliothèque, sur une seule rangée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Spiele deiner Bibliothek in einer Reihe." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Todos los juegos de tu biblioteca en una sola fila." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Todos os jogos da sua biblioteca em uma única fileira." + } + } + } + }, + "Everything NX Library can install by itself is installed.": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Everything NX Library can install by itself is installed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tutto ciò che NX Library può installare da sola è installato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout ce que NX Library peut installer seul est installé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alles, was NX Library selbst installieren kann, ist installiert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ya está instalado todo lo que NX Library puede instalar por sí mismo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tudo o que o NX Library pode instalar por conta própria está instalado." + } + } + } + }, + "Everything NX Library needs is here.": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Everything NX Library needs is here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "È presente tutto ciò che serve a NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout ce dont NX Library a besoin est présent." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alles, was NX Library benötigt, ist vorhanden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ya está todo lo que necesita NX Library." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tudo o que o NX Library precisa está aqui." + } + } + } + }, + "Example: %@ · %@ · %@": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Example: %@ · %@ · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esempio: %@ · %@ · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exemple : %@ · %@ · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beispiel: %@ · %@ · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ejemplo: %@ · %@ · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Exemplo: %@ · %@ · %@" + } + } + } + }, + "Existing game apps play without sources. Choose later in Settings → Library.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Existing game apps play without sources. Choose later in Settings → Library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le app dei giochi esistenti funzionano senza sorgenti. Puoi scegliere più tardi in Impostazioni → Libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les apps de jeux existantes fonctionnent sans sources. Vous pourrez choisir plus tard dans Réglages → Bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorhandene Spiele-Apps lassen sich ohne Quellen spielen. Wähle sie später unter Einstellungen → Bibliothek aus." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las apps de juegos existentes se pueden ejecutar sin sus archivos de origen. Puedes elegirlos más tarde en Ajustes → Biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apps de jogos existentes funcionam sem os arquivos de origem. Escolha depois em Ajustes → Biblioteca." + } + } + } + }, + "Existing game apps play without their sources. Reset group keeps this folder. Clear removes the selection, keeping your files.": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Existing game apps play without their sources. Reset group keeps this folder. Clear removes the selection, keeping your files." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le app dei giochi esistenti funzionano anche senza sorgenti. Reimposta gruppo conserva questa cartella. Svuota rimuove la selezione, conservando i tuoi file." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les apps de jeux existantes fonctionnent sans leurs sources. Réinitialiser le groupe conserve ce dossier. Effacer retire la sélection et conserve vos fichiers." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorhandene Spiele-Apps lassen sich ohne ihre Quellen spielen. „Gruppe zurücksetzen“ behält diesen Ordner bei. „Leeren“ entfernt die Auswahl und behält deine Dateien." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las apps de juegos existentes se pueden ejecutar sin sus archivos de origen. Restablecer el grupo conserva esta carpeta. Borrar quita la selección y conserva tus archivos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apps de jogos existentes funcionam sem os arquivos de origem. Redefinir o grupo mantém esta pasta. Limpar remove a seleção e mantém seus arquivos." + } + } + } + }, + "Existing games and new builds.": { + "comment": "GameFoldersCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Existing games and new builds." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi esistenti e nuove compilazioni." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux existants et nouvelles compilations." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorhandene Spiele und neue Builds." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos existentes y nuevas compilaciones." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos existentes e novas compilações." + } + } + } + }, + "Explicitly allow unverified new portings.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Explicitly allow unverified new portings." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Consenti esplicitamente nuove creazioni di app per Mac non verificate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autoriser explicitement les nouvelles créations d’apps Mac non vérifiées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue App-Erstellungen ausdrücklich ohne Verifizierung zulassen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Permite explícitamente nuevas compilaciones no verificadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Permita explicitamente novas compilações não verificadas." + } + } + } + }, + "Export the complete game package again from your console, including its ticket.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Export the complete game package again from your console, including its ticket." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esporta di nuovo dalla tua console il pacchetto completo del gioco, incluso il ticket." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exportez à nouveau le paquet de jeu complet depuis votre console, ticket compris." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Exportiere das vollständige Spielpaket einschließlich Ticket erneut von deiner Konsole." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a exportar el paquete completo del juego desde tu consola, incluido su ticket." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Exporte novamente o pacote completo do jogo do seu console, incluindo o ticket." + } + } + } + }, + "Extracted build caches for every library game. Apps, sources and saves are untouched.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extracted build caches for every library game. Apps, sources and saves are untouched." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cache di compilazione estratte per ogni gioco della libreria. App, sorgenti e salvataggi restano invariati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Caches de compilation extraits pour tous les jeux de la bibliothèque. Les apps, sources et sauvegardes restent intacts." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Entpackte Build-Caches aller Bibliotheksspiele. Apps, Quellen und Spielstände bleiben unverändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cachés de compilación extraídas de todos los juegos de la biblioteca. Las apps, los archivos de origen y las partidas guardadas siguen intactos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Caches de compilação extraídos de todos os jogos da biblioteca. Apps, arquivos de origem e dados salvos não são alterados." + } + } + } + }, + "Extracted folder": { + "comment": "AddGameView+Rows.swift:116", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Extracted folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cartella estratta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossier extrait" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Entpackter Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carpeta extraída" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pasta extraída" + } + } + } + }, + "FEATURED · %@": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "FEATURED · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "IN EVIDENZA · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À DÉCOUVRIR · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "EMPFOHLEN · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "DESTACADO · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "EM DESTAQUE · %@" + } + } + } + }, + "FILE": { + "comment": "ModDetailPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "FILE" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "FILE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FICHIER" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "DATEI" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ARCHIVO" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ARQUIVO" + } + } + } + }, + "FILES": { + "comment": "ModDetailPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "FILES" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "FILE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "FICHIERS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "DATEIEN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ARCHIVOS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ARQUIVOS" + } + } + } + }, + "FOR": { + "comment": "SpotlightCardInfoView.swift:94", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "FOR" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "PER" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "POUR" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "FÜR" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "PARA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "PARA" + } + } + } + }, + "Failed to delete game: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Failed to delete game: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impossibile eliminare il gioco: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de la suppression du jeu : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel konnte nicht gelöscht werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido eliminar el juego: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Falha ao apagar o jogo: %@" + } + } + } + }, + "Favorite": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Favorite" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Preferito" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Favori" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Favorit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Favorito" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Favorito" + } + } + } + }, + "Favorites": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Favorites" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Preferiti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Favoris" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Favoriten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Favoritos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Favoritos" + } + } + } + }, + "Featured": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Featured" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "In evidenza" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À découvrir" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Empfohlen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Destacados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Em destaque" + } + } + } + }, + "Featured: %@, for %@, by %@": { + "comment": "ModStorePage.swift:169", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Featured: %@, for %@, by %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "In evidenza: %@, per %@, di %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À découvrir : %@, pour %@, par %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Empfohlen: %@, für %@, von %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Destacado: %@, para %@, de %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Em destaque: %@, para %@, por %@" + } + } + } + }, + "File mods, no rebuild": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "File mods, no rebuild" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mod dei file, senza ricompilazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mods de fichiers, sans recompilation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei-Mods, keine Neuerstellung nötig" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mods de archivos, sin recompilar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mods de arquivos, sem recompilar" + } + } + } + }, + "Files in iCloud Drive or another synced folder carry data that code signing refuses.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Files in iCloud Drive or another synced folder carry data that code signing refuses." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file in iCloud Drive o in un’altra cartella sincronizzata contengono dati che la firma del codice rifiuta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers dans iCloud Drive ou un autre dossier synchronisé portent des données que la signature de code refuse." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dateien in iCloud Drive oder anderen synchronisierten Ordnern enthalten Daten, die die Codesignierung ablehnt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos de iCloud Drive u otra carpeta sincronizada contienen datos que la firma de código rechaza." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos no iCloud Drive ou em outra pasta sincronizada carregam dados que a assinatura de código recusa." + } + } + } + }, + "Fill": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fill" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riempi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplir" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Füllen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Rellenar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Preencher" + } + } + } + }, + "Fill crops the game's 16:9 picture to cover the screen. Health bars, subtitles or maps near the edges can be cut off. A full-screen picture without cropping needs a patch for each game.": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fill crops the game's 16:9 picture to cover the screen. Health bars, subtitles or maps near the edges can be cut off. A full-screen picture without cropping needs a patch for each game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riempi ritaglia l’immagine 16:9 del gioco per coprire lo schermo. Barre della salute, sottotitoli o mappe vicino ai bordi possono essere tagliati. Un’immagine a schermo intero senza ritagli richiede una patch specifica per ogni gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplir recadre l’image 16:9 du jeu pour couvrir l’écran. Les barres de vie, sous-titres ou cartes proches des bords peuvent être coupés. Une image en plein écran sans recadrage nécessite un correctif propre à chaque jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Füllen“ beschneidet das 16:9-Spielbild bildschirmfüllend. Lebensanzeigen, Untertitel oder Karten am Rand können abgeschnitten werden. Ein Vollbild ohne Beschnitt benötigt einen eigenen Patch pro Spiel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Rellenar recorta la imagen 16:9 del juego para cubrir la pantalla. Las barras de salud, los subtítulos o los mapas cercanos a los bordes pueden quedar cortados. Mostrar una imagen a pantalla completa sin recortarla requiere un parche para cada juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Preencher recorta a imagem 16:9 do jogo para cobrir a tela. Barras de vida, legendas ou mapas perto das bordas podem ser cortados. Uma imagem em tela cheia sem cortes exige um patch para cada jogo." + } + } + } + }, + "Fill crops the picture to cover the screen, Fit shows all of it, Stretch distorts it to fit.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fill crops the picture to cover the screen, Fit shows all of it, Stretch distorts it to fit." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riempi ritaglia l’immagine per coprire lo schermo, Adatta la mostra interamente, Allunga la deforma per adattarla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplir recadre l’image pour couvrir l’écran, Ajuster l’affiche entièrement, Étirer la déforme pour l’adapter." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Füllen“ beschneidet das Bild bildschirmfüllend, „Einpassen“ zeigt es vollständig und „Strecken“ verzerrt es passend." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Rellenar recorta la imagen para cubrir la pantalla, Ajustar la muestra entera y Estirar la deforma para encajar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Preencher recorta a imagem para cobrir a tela; Ajustar mostra a imagem inteira; Esticar distorce a imagem para caber." + } + } + } + }, + "Fill crops to cover, Fit shows all of it, Stretch distorts it to fit. Applies to wallpapers.": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fill crops to cover, Fit shows all of it, Stretch distorts it to fit. Applies to wallpapers." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riempi ritaglia per coprire l’area, Adatta mostra l’intera immagine, Allunga la deforma per adattarla. Si applica agli sfondi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplir recadre pour couvrir, Ajuster affiche toute l’image, Étirer la déforme pour l’adapter. S’applique aux fonds d’écran." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Füllen“ beschneidet das Bild flächendeckend, „Einpassen“ zeigt es vollständig und „Strecken“ verzerrt es passend. Gilt für Hintergrundbilder." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Rellenar recorta para cubrir, Ajustar muestra toda la imagen y Estirar la deforma para encajar. Se aplica a los fondos de pantalla." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Preencher recorta a imagem para cobrir a área; Ajustar mostra a imagem inteira; Esticar distorce a imagem para caber. Aplica-se aos papéis de parede." + } + } + } + }, + "Filter": { + "comment": "CollectionView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Filter" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Filtra" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Filtrer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Filter" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Filtrar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Filtrar" + } + } + } + }, + "Filter, search and copy build and launch logs.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Filter, search and copy build and launch logs." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Filtra, cerca e copia i log di compilazione e avvio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Filtrer, rechercher et copier les journaux de compilation et de lancement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Build- und Startprotokolle filtern, durchsuchen und kopieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Filtra, busca y copia registros de compilación e inicio." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Filtre, busque e copie logs de compilação e execução." + } + } + } + }, + "Find a game": { + "comment": "FindGameView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Find a game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Trova un gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trouver un jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel finden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar un juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encontrar um jogo" + } + } + } + }, + "Find a region": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Find a region" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Trova una zona" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trouver une région" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Region suchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar una región" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encontrar uma região" + } + } + } + }, + "Find any setting by name or description.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Find any setting by name or description." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Trova qualsiasi impostazione per nome o descrizione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trouver un réglage par son nom ou sa description." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jede Einstellung nach Name oder Beschreibung finden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Busca cualquier ajuste por su nombre o descripción." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encontre qualquer ajuste por nome ou descrição." + } + } + } + }, + "Find existing nxnative builds and refresh the library.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Find existing nxnative builds and refresh the library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Trova le compilazioni nxnative esistenti e aggiorna la libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher les compilations nxnative existantes et actualiser la bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorhandene nxnative-Builds suchen und die Bibliothek aktualisieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Busca compilaciones existentes de nxnative y actualiza la biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encontre compilações existentes do nxnative e atualize a biblioteca." + } + } + } + }, + "Find the game": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Find the game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Trova il gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trouver le jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel finden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar el juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encontrar o jogo" + } + } + } + }, + "Finishing…": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Finishing…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Completamento…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Finalisation…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird abgeschlossen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Finalizando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Finalizando…" + } + } + } + }, + "Fit": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Adatta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajuster" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einpassen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajustar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajustar" + } + } + } + }, + "Folder": { + "comment": "CloudSaves.swift:35", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carpeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pasta" + } + } + } + }, + "Folder unavailable": { + "comment": "AppViewModel+Saves.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Folder unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cartella non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossier indisponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carpeta no disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pasta indisponível" + } + } + } + }, + "Folders, imports and artwork caches.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Folders, imports and artwork caches." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cartelle, importazioni e cache delle immagini." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossiers, importations et caches d’illustrations." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner, Importe und Artwork-Caches." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carpetas, importaciones y cachés de imágenes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pastas, importações e caches de artes." + } + } + } + }, + "Follow": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Follow" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Segui" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Suivre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Folgen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Seguir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Acompanhar" + } + } + } + }, + "Follow the Mac’s preferred languages. Game language stays separate.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Follow the Mac’s preferred languages. Game language stays separate." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Segue le lingue preferite del Mac. La lingua dei giochi resta separata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Suivre les langues préférées du Mac. La langue des jeux reste indépendante." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Den bevorzugten Sprachen des Macs folgen. Die Spielsprache bleibt separat." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sigue los idiomas preferidos del Mac. El idioma del juego se configura por separado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Segue os idiomas preferidos do Mac. O idioma dos jogos continua separado." + } + } + } + }, + "Formats follow this region; online descriptions follow the app language.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Formats follow this region; online descriptions follow the app language." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I formati seguono questa zona; le descrizioni online seguono la lingua dell’app." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les formats suivent cette région ; les descriptions en ligne suivent la langue de l’app." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Formate richten sich nach dieser Region; Online-Beschreibungen nach der App-Sprache." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los formatos siguen esta región; las descripciones en línea siguen el idioma de la app." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os formatos seguem esta região; as descrições on-line seguem o idioma do app." + } + } + } + }, + "Foundational native execution architecture": { + "comment": "SettingsAboutSection.swift:15", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Foundational native execution architecture" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Architettura di base per l’esecuzione nativa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Architecture de base pour l’exécution native" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Grundlegende Architektur für die native Ausführung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Arquitectura base de ejecución nativa" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquitetura de base para execução nativa" + } + } + } + }, + "Frame buffering": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Frame buffering" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Buffer dei fotogrammi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise en mémoire tampon des images" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Frame-Pufferung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Almacenamiento de fotogramas en búfer" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buffer de quadros" + } + } + } + }, + "Frame rate": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Frame rate" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Frequenza dei fotogrammi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence d’images" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bildrate" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fotogramas por segundo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Taxa de quadros" + } + } + } + }, + "Frame-rate counter": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Frame-rate counter" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Contatore FPS" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compteur d’images par seconde" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bildratenzähler" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Contador de fotogramas por segundo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Contador de quadros por segundo" + } + } + } + }, + "Frame-rate target": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Frame-rate target" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Frequenza dei fotogrammi desiderata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fréquence d’images cible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zielbildrate" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Objetivo de fotogramas por segundo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Taxa de quadros desejada" + } + } + } + }, + "Free on game apps volume": { + "comment": "SettingsStorageSection.swift:26", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Free on game apps volume" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spazio libero sul volume delle app dei giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Espace libre sur le volume des apps de jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Frei auf dem Volume für Spiele-Apps" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Libre en el volumen de apps de juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Espaço livre no volume de apps de jogos" + } + } + } + }, + "Free some space, then try again.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Free some space, then try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Libera un po’ di spazio, poi riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Libérez de l’espace, puis réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gib Speicherplatz frei und versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Libera espacio e inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Libere espaço e tente novamente." + } + } + } + }, + "Free with a SteamGridDB account.": { + "comment": "SteamGridDBKeyDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Free with a SteamGridDB account." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gratuita con un account SteamGridDB." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gratuit avec un compte SteamGridDB." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit einem SteamGridDB-Konto kostenlos." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Gratis con una cuenta de SteamGridDB." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Grátis com uma conta do SteamGridDB." + } + } + } + }, + "Frees disk space by deleting original NSP/XCI dump files and the compilation cache workspace. The installed game app and all your save files are completely safe.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Frees disk space by deleting original NSP/XCI dump files and the compilation cache workspace. The installed game app and all your save files are completely safe." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Libera spazio su disco eliminando i file dump NSP/XCI originali e l’area di lavoro della cache di compilazione. L’app del gioco installata e tutti i salvataggi sono completamente al sicuro." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Libère de l’espace disque en supprimant les fichiers d’origine extraits NSP/XCI et le cache de compilation de l’espace de travail. L’app de jeu installée et toutes vos sauvegardes restent entièrement préservées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gibt Speicherplatz frei, indem ursprüngliche NSP/XCI-Dump-Dateien und das Arbeitsverzeichnis des Kompilierungscaches gelöscht werden. Die installierte Spiele-App und alle deine Spielstände bleiben vollständig erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Libera espacio en disco eliminando los volcados originales NSP/XCI y el espacio de trabajo de la caché de compilación. La app del juego instalada y todas tus partidas guardadas están completamente a salvo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Libera espaço em disco apagando os arquivos originais de cópia extraída NSP/XCI e a área de trabalho do cache de compilação. O app do jogo instalado e todos os seus arquivos de dados salvos ficam totalmente preservados." + } + } + } + }, + "From": { + "comment": "SaveImportDetails.swift:22", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "From" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Da" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Depuis" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Von" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desde" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Origem" + } + } + } + }, + "From file…": { + "comment": "ChangeArtworkView.swift:450", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "From file…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Da file…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Depuis un fichier…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus Datei …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desde un archivo…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "De um arquivo…" + } + } + } + }, + "Fullscreen": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fullscreen" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermo intero" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plein écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vollbild" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pantalla completa" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tela cheia" + } + } + } + }, + "GAME OPTIONS": { + "comment": "GameOptionsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GAME OPTIONS" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "OPZIONI DEL GIOCO" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "OPTIONS DU JEU" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SPIELOPTIONEN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "OPCIONES DEL JUEGO" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "OPÇÕES DO JOGO" + } + } + } + }, + "GAMEBANANA": { + "comment": "GameBananaFindPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GAMEBANANA" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "GAMEBANANA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "GAMEBANANA" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GAMEBANANA" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "GAMEBANANA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "GAMEBANANA" + } + } + } + }, + "GAMEBANANA MOD": { + "comment": "ModDetailPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GAMEBANANA MOD" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "MOD DI GAMEBANANA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MOD GAMEBANANA" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GAMEBANANA-MOD" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "MOD DE GAMEBANANA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "MOD DO GAMEBANANA" + } + } + } + }, + "GAMES": { + "comment": "SpotlightCardInfoView.swift:81", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GAMES" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "GIOCHI" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "JEUX" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SPIELE" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "JUEGOS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "JOGOS" + } + } + } + }, + "Game": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo" + } + } + } + }, + "Game Default": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game Default" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Predefinita del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par défaut du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstandard" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Predeterminado del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Padrão do jogo" + } + } + } + }, + "Game Default uses the language supplied by the game's build.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game Default uses the language supplied by the game's build." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Predefinita del gioco usa la lingua fornita dalla compilazione del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par défaut du jeu utilise la langue fournie par la compilation du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Spielstandard“ verwendet die im Build des Spiels vorgegebene Sprache." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "«Predeterminado del juego» usa el idioma proporcionado por la compilación del juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Padrão do jogo usa o idioma fornecido pela compilação do jogo." + } + } + } + }, + "Game Details": { + "comment": "NXLibraryApp.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game Details" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dettagli del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Détails du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldetails" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Detalles del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Detalhes do jogo" + } + } + } + }, + "Game Options…": { + "comment": "NXLibraryApp.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game Options…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Opzioni del gioco…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Options du jeu…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieloptionen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opciones del juego…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opções do jogo…" + } + } + } + }, + "Game app deleted: %@%@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game app deleted: %@%@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App del gioco eliminata: %@%@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "App de jeu supprimée : %@%@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiele-App gelöscht: %@%@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "App del juego eliminada: %@%@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "App do jogo apagado: %@%@" + } + } + } + }, + "Game apps": { + "comment": "Display copy only; internal values and paths remain unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game apps" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App dei giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apps de jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiele-Apps" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Apps de juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apps de jogos" + } + } + } + }, + "Game apps volume unavailable.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game apps volume unavailable." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Volume di App dei giochi non disponibile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Volume des apps de jeux indisponible." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Volume für Spiele-Apps nicht verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El volumen de Apps de juegos no está disponible." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Volume de Apps de jogos indisponível." + } + } + } + }, + "Game backgrounds": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game backgrounds" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondi dei giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrière-plans des jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielhintergründe" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondos de juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fundos dos jogos" + } + } + } + }, + "Game cues only": { + "comment": "SettingsSoundSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game cues only" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Solo segnali del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Signaux du jeu uniquement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Spielsignale" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo avisos del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apenas avisos do jogo" + } + } + } + }, + "Game cues only during launch; movement and selection sounds stay quiet.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game cues only during launch; movement and selection sounds stay quiet." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Durante l’avvio si sentono solo i segnali del gioco; i suoni di movimento e selezione restano silenziosi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Seuls les signaux du jeu sont émis au lancement ; les sons de navigation et de sélection restent silencieux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Während des Starts nur Spielsignale; Navigations- und Auswahlklänge bleiben stumm." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo avisos del juego durante el inicio; los sonidos de movimiento y selección se silencian." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apenas avisos do jogo durante a abertura; os sons de navegação e seleção ficam silenciados." + } + } + } + }, + "Game default": { + "comment": "AddGameView+Rows.swift:186", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game default" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Predefinita del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Par défaut du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstandard" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Predeterminado del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Padrão do jogo" + } + } + } + }, + "Game files": { + "comment": "Display copy only; internal values and paths remain unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game files" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "File dei giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichiers de jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldateien" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos de juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos do jogo" + } + } + } + }, + "Game files are missing or not attached. Attach sources in Edit details.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game files are missing or not attached. Attach sources in Edit details." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file del gioco non sono disponibili o non sono collegati. Collega le sorgenti in Modifica dettagli." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers du jeu sont introuvables ou non associés. Associez les sources dans Modifier les détails." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldateien fehlen oder sind nicht zugeordnet. Ordne die Quellen unter „Details bearbeiten“ zu." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos del juego faltan o no están vinculados. Vincula los archivos de origen en Editar detalles." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos do jogo não foram encontrados ou não estão vinculados. Vincule-os em Editar detalhes." + } + } + } + }, + "Game files are missing or not attached. Attach them to build.": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game files are missing or not attached. Attach them to build." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file del gioco non sono disponibili o non sono collegati. Collegali per compilare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers du jeu sont introuvables ou non associés. Associez-les pour compiler." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldateien fehlen oder sind nicht zugeordnet. Ordne sie zu, um die App zu erstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos del juego faltan o no están vinculados. Vincúlalos para compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos do jogo não foram encontrados ou não estão vinculados. Vincule-os para compilar." + } + } + } + }, + "Game files are missing or not attached. Attach them to rebuild.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game files are missing or not attached. Attach them to rebuild." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file del gioco non sono disponibili o non sono collegati. Collegali per ricompilare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers du jeu sont introuvables ou non associés. Associez-les pour recompiler." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldateien fehlen oder sind nicht zugeordnet. Ordne sie zu, um die App erneut zu erstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos del juego faltan o no están vinculados. Vincúlalos para recompilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos do jogo não foram encontrados ou não estão vinculados. Vincule-os para recompilar." + } + } + } + }, + "Game files are missing or not attached. Play is available; attach them to rebuild.": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game files are missing or not attached. Play is available; attach them to rebuild." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file del gioco non sono disponibili o non sono collegati. Puoi giocare; collegali per ricompilare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers du jeu sont introuvables ou non associés. Vous pouvez jouer ; associez-les pour recompiler." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldateien fehlen oder sind nicht zugeordnet. Spielen ist möglich; ordne die Dateien zu, um die App erneut zu erstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos del juego faltan o no están vinculados. Puedes jugar; vincúlalos para recompilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos do jogo não foram encontrados ou não estão vinculados. Você pode jogar; vincule-os para recompilar." + } + } + } + }, + "Game files not found": { + "comment": "GameResourceState.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game files not found" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "File del gioco non trovati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichiers du jeu introuvables" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldateien nicht gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos del juego no encontrados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos do jogo não encontrados" + } + } + } + }, + "Game files not found · Rebuild unavailable": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game files not found · Rebuild unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "File del gioco non trovati · Ricompilazione non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichiers du jeu introuvables · Recompilation indisponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieldateien nicht gefunden · Neuerstellung nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se encuentran los archivos del juego · No se puede recompilar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos do jogo não encontrados · Recompilação indisponível" + } + } + } + }, + "Game genre: Action": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Action" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Azione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Action" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Action" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Acción" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ação" + } + } + } + }, + "Game genre: Action-adventure": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Action-adventure" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Azione e avventura" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Action-aventure" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Action-Adventure" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Acción y aventura" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ação e aventura" + } + } + } + }, + "Game genre: Adventure": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Adventure" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avventura" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aventure" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Abenteuer" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aventura" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aventura" + } + } + } + }, + "Game genre: Arcade": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Arcade" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Arcade" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arcade" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Arcade" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Arcade" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arcade" + } + } + } + }, + "Game genre: Board game": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Board game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi da tavolo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux de société" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Brettspiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos de mesa" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos de tabuleiro" + } + } + } + }, + "Game genre: Card game": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Card game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi di carte" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux de cartes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kartenspiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos de cartas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos de cartas" + } + } + } + }, + "Game genre: Communication": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Communication" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Comunicazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Communication" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kommunikation" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comunicación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Comunicação" + } + } + } + }, + "Game genre: Education": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Education" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Educativi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éducatif" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lernspiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Educativos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Educativos" + } + } + } + }, + "Game genre: Fighting": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fighting" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Picchiaduro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Combat" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kampfspiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Lucha" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Luta" + } + } + } + }, + "Game genre: Fitness": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Fitness" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Fitness" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fitness" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fitness" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ejercicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Exercícios" + } + } + } + }, + "Game genre: Lifestyle": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Lifestyle" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Stile di vita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode de vie" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lifestyle" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estilo de vida" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Estilo de vida" + } + } + } + }, + "Game genre: Music": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Music" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Musica e ritmo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Musique et rythme" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Musik und Rhythmus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Música y ritmo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Música e ritmo" + } + } + } + }, + "Game genre: Party": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Party" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Party" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fête" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Party" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fiesta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Festa" + } + } + } + }, + "Game genre: Platformer": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Platformer" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Piattaforme" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plateforme" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jump ’n’ Run" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Plataformas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Plataforma" + } + } + } + }, + "Game genre: Puzzle": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Puzzle" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rompicapo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réflexion" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Denkspiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Puzles" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Quebra-cabeças" + } + } + } + }, + "Game genre: Racing": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Racing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Corse" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Course" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Rennspiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carreras" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Corrida" + } + } + } + }, + "Game genre: Role-playing": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Role-playing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi di ruolo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux de rôle" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Rollenspiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos de rol" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "RPG" + } + } + } + }, + "Game genre: Shooter": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shooter" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sparatutto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tir" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Shooter" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Disparos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tiro" + } + } + } + }, + "Game genre: Simulation": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Simulation" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Simulazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Simulation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Simulation" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Simulación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Simulação" + } + } + } + }, + "Game genre: Sports": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sports" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sport" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sport" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sport" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Deportes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esportes" + } + } + } + }, + "Game genre: Strategy": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Strategy" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Strategia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stratégie" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Strategie" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estrategia" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Estratégia" + } + } + } + }, + "Game genre: Utility": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Utility" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Utilità" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilitaires" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dienstprogramme" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Utilidades" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Utilitários" + } + } + } + }, + "Game language": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game language" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lingua del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Langue du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielsprache" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Idioma del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Idioma do jogo" + } + } + } + }, + "Game name": { + "comment": "GameBananaFindPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game name" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nome del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielname" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nombre del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nome do jogo" + } + } + } + }, + "Game names, icons and banners": { + "comment": "SettingsAboutSection.swift:21", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game names, icons and banners" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nomi, icone e banner dei giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Noms, icônes et bannières des jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielnamen, Symbole und Banner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nombres, iconos y banners de juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nomes, ícones e banners de jogos" + } + } + } + }, + "Game options": { + "comment": "CollectionView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game options" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Opzioni del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Options du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieloptionen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opciones del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opções do jogo" + } + } + } + }, + "Game packages or an extracted game folder. You can also drop them here.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game packages or an extracted game folder. You can also drop them here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pacchetti di gioco o una cartella di gioco estratta. Puoi anche trascinarli qui." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Paquets de jeu ou dossier de jeu extrait. Vous pouvez aussi les déposer ici." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielpakete oder ein entpackter Spielordner. Du kannst sie auch hier ablegen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Paquetes de juegos o una carpeta de juego extraída. También puedes arrastrarlos aquí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pacotes de jogos ou uma pasta de jogo extraída. Você também pode soltá-los aqui." + } + } + } + }, + "Game ready. No icon found; add one in Change artwork.": { + "comment": "AppIconProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game ready. No icon found; add one in Change artwork." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioco pronto. Nessuna icona trovata; aggiungine una in Cambia grafica." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu prêt. Aucune icône trouvée ; ajoutez-en une dans Changer l’illustration." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel bereit. Kein Symbol gefunden; füge eines unter „Artwork ändern“ hinzu." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juego listo. No se ha encontrado ningún icono; añade uno en Cambiar imágenes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo pronto. Nenhum ícone encontrado; adicione um em Alterar arte." + } + } + } + }, + "Game rumble": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game rumble" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Vibrazione del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibrations en jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielvibration" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vibración del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Vibração do jogo" + } + } + } + }, + "Game running": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game running" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioco in esecuzione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu en cours" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel läuft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juego en ejecución" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo em execução" + } + } + } + }, + "Game sources: NSP, XCI, NSZ and XCZ files, or Switch 2 NUP files and extracted folders, for builds. Optional for existing apps.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game sources: NSP, XCI, NSZ and XCZ files, or Switch 2 NUP files and extracted folders, for builds. Optional for existing apps." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sorgenti dei giochi: file NSP, XCI, NSZ e XCZ, oppure file NUP di Switch 2 e cartelle estratte, per le compilazioni. Facoltative per le app esistenti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sources des jeux : fichiers NSP, XCI, NSZ et XCZ, ou fichiers NUP Switch 2 et dossiers extraits, pour compiler. Facultatives pour les apps existantes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielquellen: NSP-, XCI-, NSZ- und XCZ-Dateien oder Switch 2-NUP-Dateien und entpackte Ordner zum Erstellen von Apps. Für vorhandene Apps optional." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos de origen para compilar: NSP, XCI, NSZ y XCZ, o archivos NUP de Switch 2 y carpetas extraídas. Opcionales para apps existentes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos de origem dos jogos: arquivos NSP, XCI, NSZ e XCZ, ou arquivos NUP de Switch 2 e pastas extraídas, para compilar. Opcionais para apps existentes." + } + } + } + }, + "Game ticket missing": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game ticket missing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ticket del gioco mancante" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ticket de jeu manquant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel-Ticket fehlt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta el ticket del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ticket do jogo ausente" + } + } + } + }, + "Game tiles": { + "comment": "ChangeArtworkView.swift:309", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game tiles" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riquadri dei giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vignettes des jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielkacheln" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fichas de juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Blocos de jogos" + } + } + } + }, + "Game update": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Update" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiornamento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Update" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualización" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualização" + } + } + } + }, + "Game v%@": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game v%@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioco v%@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu v%@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel v%@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juego v%@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo v%@" + } + } + } + }, + "Game's own": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game's own" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Celui du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieleigener" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Propio del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Próprio do jogo" + } + } + } + }, + "Game's upscaler": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Game's upscaler" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Upscaler del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à l’échelle du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Upscaler des Spiels" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reescalador del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ampliador de imagem do jogo" + } + } + } + }, + "GameBanana didn't answer. Try again in a moment.": { + "comment": "GameBananaBandModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GameBanana didn't answer. Try again in a moment." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "GameBanana non ha risposto. Riprova tra poco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "GameBanana n’a pas répondu. Réessayez dans un instant." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameBanana hat nicht geantwortet. Versuche es gleich erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "GameBanana no ha respondido. Inténtalo de nuevo en un momento." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O GameBanana não respondeu. Tente novamente em instantes." + } + } + } + }, + "GameBanana: %@": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GameBanana: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "GameBanana: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "GameBanana : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameBanana: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "GameBanana: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "GameBanana: %@" + } + } + } + }, + "GameTDB covers": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GameTDB covers" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copertine GameTDB" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jaquettes GameTDB" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameTDB-Cover" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portadas de GameTDB" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capas do GameTDB" + } + } + } + }, + "GameTDB's database could not be read.": { + "comment": "GameTDB.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GameTDB's database could not be read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile leggere il database di GameTDB." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La base de données de GameTDB n’a pas pu être lue." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die GameTDB-Datenbank konnte nicht gelesen werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido leer la base de datos de GameTDB." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível ler o banco de dados do GameTDB." + } + } + } + }, + "GameTDB, or a composed cover": { + "comment": "ChangeArtworkView.swift:435", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GameTDB, or a composed cover" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "GameTDB oppure una copertina composta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "GameTDB, ou une jaquette composée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameTDB oder ein zusammengestelltes Cover" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "GameTDB o una portada compuesta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "GameTDB ou uma capa composta" + } + } + } + }, + "Gamepad": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Gamepad" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gamepad" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Manette de jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gamepad" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mando de juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Controle de jogo" + } + } + } + }, + "Gameplay": { + "comment": "ModCatalog.swift:14", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Gameplay" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Meccaniche di gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mécaniques de jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielmechanik" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Jugabilidad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogabilidade" + } + } + } + }, + "Games": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Games" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos" + } + } + } + }, + "Games not played yet": { + "comment": "SpotlightView.swift:441", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Games not played yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi mai giocati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux pas encore joués" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nicht gespielte Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos aún sin jugar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos ainda não jogados" + } + } + } + }, + "Games you hid from Game options; Show puts one back in your lists.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Games you hid from Game options; Show puts one back in your lists." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I giochi nascosti da Opzioni del gioco; Mostra li reinserisce negli elenchi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les jeux masqués depuis Options du jeu ; Afficher les remet dans vos listes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiele, die du in den Spieloptionen ausgeblendet hast; „Anzeigen“ bringt sie zurück in deine Listen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los juegos que has ocultado desde sus opciones; Mostrar los devuelve a tus listas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos ocultados nas Opções do jogo; Mostrar os devolve às suas listas." + } + } + } + }, + "Games you play show up here.": { + "comment": "SpotlightRowView.swift:193", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Games you play show up here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Qui trovi i giochi a cui hai giocato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les jeux auxquels vous jouez apparaissent ici." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiele, die du spielst, erscheinen hier." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los juegos a los que juegues aparecerán aquí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os jogos que você joga aparecem aqui." + } + } + } + }, + "General rating only, 2K and up. Every picture belongs to its artist.": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "General rating only, 2K and up. Every picture belongs to its artist." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Solo classificazione General, da 2K in su. Ogni immagine appartiene al rispettivo artista." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Classification General uniquement, en 2K et plus. Chaque image appartient à son artiste." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur für alle Altersgruppen freigegebene Inhalte, ab 2K. Alle Bilder gehören ihren jeweiligen Künstlern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo clasificación general, 2K o más. Cada imagen pertenece a su artista." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apenas classificação geral, a partir de 2K. Cada imagem pertence ao seu artista." + } + } + } + }, + "Genres": { + "comment": "MetadataPicker.swift:11", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Genres" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Generi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Genres" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Genres" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Géneros" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Gêneros" + } + } + } + }, + "Get Xcode": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Get Xcode" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scarica Xcode" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Obtenir Xcode" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Xcode laden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Obtener Xcode" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Obter Xcode" + } + } + } + }, + "Get a key": { + "comment": "SteamGridDBKeyDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Get a key" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ottieni una chiave" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Obtenir une clé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel anfordern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Obtener una clave" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Obter uma chave" + } + } + } + }, + "Get it from the App Store, then open it once.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Get it from the App Store, then open it once." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scaricalo dall’App Store, poi aprilo una volta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Téléchargez-le depuis l’App Store, puis ouvrez-le une fois." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lade es aus dem App Store und öffne es einmal." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descárgalo de App Store y ábrelo una vez." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Obtenha na App Store e abra uma vez." + } + } + } + }, + "Get ready": { + "comment": "YourGamesSetupView.swift:187", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Get ready" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Preparazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorbereiten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Preparación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Preparação" + } + } + } + }, + "GitHub API rate limit reached. Try again after %@.": { + "comment": "UpstreamChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GitHub API rate limit reached. Try again after %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Raggiunto il limite di richieste all’API GitHub. Riprova dopo %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Limite de requêtes de l’API GitHub atteinte. Réessayez après %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Anfragelimit der GitHub-API ist erreicht. Versuche es nach %@ erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha alcanzado el límite de solicitudes de la API de GitHub. Inténtalo de nuevo después de %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O limite de requisições da API do GitHub foi atingido. Tente novamente após %@." + } + } + } + }, + "GitHub API rate limit reached. Try again later.": { + "comment": "UpstreamChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GitHub API rate limit reached. Try again later." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Raggiunto il limite di richieste all’API GitHub. Riprova più tardi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Limite de requêtes de l’API GitHub atteinte. Réessayez plus tard." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Anfragelimit der GitHub-API ist erreicht. Versuche es später erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha alcanzado el límite de solicitudes de la API de GitHub. Inténtalo de nuevo más tarde." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O limite de requisições da API do GitHub foi atingido. Tente novamente mais tarde." + } + } + } + }, + "GitHub returned a response this app could not read.": { + "comment": "UpstreamChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GitHub returned a response this app could not read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "GitHub ha restituito una risposta non leggibile da questa app." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "GitHub a renvoyé une réponse illisible par cette app." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GitHub hat eine Antwort zurückgegeben, die diese App nicht lesen konnte." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "GitHub ha devuelto una respuesta que esta app no ha podido leer." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O GitHub retornou uma resposta que este app não conseguiu ler." + } + } + } + }, + "GitHub returned status %@%@": { + "comment": "UpstreamChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "GitHub returned status %@%@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "GitHub ha restituito lo stato %@%@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "GitHub a renvoyé le statut %@%@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GitHub hat Status %@%@ zurückgegeben" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "GitHub ha devuelto el estado %@%@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O GitHub retornou o status %@%@" + } + } + } + }, + "Go back to the review step and choose Add & Build to start a build.": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Go back to the review step and choose Add & Build to start a build." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Torna al riepilogo e scegli Aggiungi e compila per avviare una compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Revenez à l’étape de vérification et choisissez Ajouter et compiler pour lancer une compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gehe zur Überprüfung zurück und wähle „Hinzufügen & erstellen“, um einen Build zu starten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve al paso de revisión y elige «Añadir y compilar» para iniciar una compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Volte à etapa de revisão e escolha Adicionar e compilar para iniciar uma compilação." + } + } + } + }, + "Graphics & effects": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Graphics & effects" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Grafica ed effetti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Graphismes et effets" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Grafik & Effekte" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Gráficos y efectos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Gráficos e efeitos" + } + } + } + }, + "Group reset": { + "comment": "SettingsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Group reset" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Reimpostazione del gruppo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialisation du groupe" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gruppe zurückgesetzt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restablecimiento del grupo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Grupo redefinido" + } + } + } + }, + "Handheld": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Handheld" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Portatile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Portable" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Handheld" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portátil" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Portátil" + } + } + } + }, + "Header key and %@ application key generations.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Header key and %@ application key generations." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiave header e %@ generazioni di chiavi applicazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé d’en-tête et %@ générations de clés d’application." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Header-Schlüssel und %@ Anwendungsschlüsselgenerationen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clave de cabecera y %@ generaciones de claves de aplicación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chave de cabeçalho e %@ gerações de chaves de aplicativo." + } + } + } + }, + "Hero and cover art downloaded for this library.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hero and cover art downloaded for this library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Immagini di sfondo e copertine scaricate per questa libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Illustrations de fond et jaquettes téléchargées pour cette bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für diese Bibliothek geladene Hintergrund- und Coverbilder." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Imágenes de fondo y portadas descargadas para esta biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Artes de destaque e capas baixadas para esta biblioteca." + } + } + } + }, + "Hidden": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hidden" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nascosto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masqué" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausgeblendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Oculto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Oculto" + } + } + } + }, + "Hidden games": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hidden games" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi nascosti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux masqués" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausgeblendete Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos ocultos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos ocultos" + } + } + } + }, + "Hidden games stay out of your lists.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hidden games stay out of your lists." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I giochi nascosti restano esclusi dai tuoi elenchi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les jeux masqués n’apparaissent pas dans vos listes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausgeblendete Spiele erscheinen nicht in deinen Listen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los juegos ocultos no aparecen en tus listas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos ocultos ficam fora das suas listas." + } + } + } + }, + "Hide": { + "comment": "MenuSelectionModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hide" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nascondi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ocultar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ocultar" + } + } + } + }, + "Hide Log": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hide Log" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nascondi log" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer le journal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Protokoll ausblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ocultar registro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ocultar log" + } + } + } + }, + "Hide card": { + "comment": "SpotlightCardInfoView.swift:189", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hide card" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nascondi scheda" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer la carte" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Karte ausblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ocultar tarjeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ocultar cartão" + } + } + } + }, + "Hide info": { + "comment": "GameOptionsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hide info" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nascondi informazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Masquer les infos" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Infos ausblenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ocultar información" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ocultar informações" + } + } + } + }, + "High-resolution game box art and hero wallpapers cached locally from the Nintendo Store catalog.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "High-resolution game box art and hero wallpapers cached locally from the Nintendo Store catalog." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copertine dei giochi e sfondi ad alta risoluzione conservati nella cache locale dal catalogo del Nintendo Store." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jaquettes et fonds d’écran de jeu haute résolution mis en cache localement depuis le catalogue du Nintendo Store." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hochauflösende Spiele-Cover und großflächige Hintergrundbilder aus dem Nintendo Store-Katalog, lokal zwischengespeichert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portadas de alta resolución y fondos de juegos guardados en la caché local desde el catálogo de Nintendo Store." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capas de jogos em alta resolução e papéis de parede de destaque em cache local, obtidos do catálogo da Nintendo Store." + } + } + } + }, + "Hold Confirm on a Spotlight tile": { + "comment": "ControlsView.swift:51", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hold Confirm on a Spotlight tile" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tieni premuto Conferma su un riquadro di Spotlight" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maintenir Confirmer sur une vignette d’À la une" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bestätigungstaste auf einer Spotlight-Kachel gedrückt halten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mantén pulsado Confirmar en una ficha de Destacados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mantenha Confirmar pressionado em um bloco de Destaques" + } + } + } + }, + "Hold Space": { + "comment": "ControlsView.swift:36", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hold Space" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tieni premuto Spazio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maintenir Espace" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Leertaste gedrückt halten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mantén pulsada la barra espaciadora" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mantenha a barra de espaço pressionada" + } + } + } + }, + "Hold frames when late": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hold frames when late" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mantieni i fotogrammi in caso di ritardo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maintenir les images en cas de retard" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verspätete Frames halten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Retener fotogramas si se retrasan" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Manter quadros em caso de atraso" + } + } + } + }, + "Hold: Search": { + "comment": "SeleneHomeChrome.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hold: Search" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tieni premuto: Cerca" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maintenir : Rechercher" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gedrückt halten: Suchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mantén pulsado: Buscar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mantenha pressionado: Buscar" + } + } + } + }, + "Hold: Settings": { + "comment": "SeleneHomeChrome.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Hold: Settings" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tieni premuto: Impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maintenir : Réglages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gedrückt halten: Einstellungen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mantén pulsado: Ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mantenha pressionado: Ajustes" + } + } + } + }, + "How NX Library looks and moves.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "How NX Library looks and moves." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’aspetto e le animazioni di NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’apparence et les animations de NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie NX Library aussieht und sich bewegt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El aspecto y el movimiento de NX Library." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A aparência e os movimentos do NX Library." + } + } + } + }, + "How dark a wallpaper or live background is: dim evenly, bright on the right away from the text, or bright everywhere.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "How dark a wallpaper or live background is: dim evenly, bright on the right away from the text, or bright everywhere." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Quanto scurire uno sfondo statico o animato: in modo uniforme, lasciando luminoso il lato destro lontano dal testo oppure lasciando tutto luminoso." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’assombrissement d’un fond d’écran ou d’un arrière-plan animé : sombre uniformément, clair à droite loin du texte, ou clair partout." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie dunkel ein Hintergrundbild oder Live-Hintergrund ist: gleichmäßig abgedunkelt, rechts abseits vom Text hell oder überall hell." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El brillo de un fondo estático o animado: tenue uniforme, brillo a la derecha lejos del texto o brillo en toda la imagen." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O quanto um papel de parede ou fundo animado é escurecido: escurecer por igual, deixar claro à direita longe do texto ou deixar tudo claro." + } + } + } + }, + "How loud the interface sounds are.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "How loud the interface sounds are." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il volume dei suoni dell’interfaccia." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le volume des sons de l’interface." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lautstärke der Oberflächenklänge." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El volumen de los sonidos de la interfaz." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Volume dos sons da interface." + } + } + } + }, + "How much the launch art is darkened.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "How much the launch art is darkened." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Quanto scurire l’immagine all’avvio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le niveau d’assombrissement de l’illustration de lancement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie stark das Artwork beim Start abgedunkelt wird." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cuánto se oscurecen las imágenes de inicio." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O quanto a arte de abertura é escurecida." + } + } + } + }, + "HowLongToBeat": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "HowLongToBeat" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "HowLongToBeat" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "HowLongToBeat" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "HowLongToBeat" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "HowLongToBeat" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "HowLongToBeat" + } + } + } + }, + "INSTALLED": { + "comment": "SpotlightCardInfoView.swift:83", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "INSTALLED" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "INSTALLATI" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "INSTALLÉS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "INSTALLIERT" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "INSTALADOS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "INSTALADOS" + } + } + } + }, + "Icon": { + "comment": "ArtworkPicker.swift:13", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Icon" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Icona" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Icône" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Icono" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ícone" + } + } + } + }, + "Icon location": { + "comment": "ChangeArtworkView.swift:443", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Icon location" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Posizione dell’icona" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Emplacement de l’icône" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anzeigeort des Symbols" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ubicación del icono" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Local do ícone" + } + } + } + }, + "Icon used only in NX Library": { + "comment": "AppIconProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Icon used only in NX Library" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Icona usata solo in NX Library" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Icône utilisée uniquement dans NX Library" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol wird nur in NX Library verwendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Icono usado solo en NX Library" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ícone usado apenas no NX Library" + } + } + } + }, + "Image fit": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Image fit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Adattamento dell’immagine" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajustement de l’image" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bildanpassung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajuste de imagen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajuste da imagem" + } + } + } + }, + "Import": { + "comment": "SettingsSoundSection.swift:68", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Importieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importar" + } + } + } + }, + "Import Key...": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import Key..." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa chiave..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer une clé..." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel importieren..." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importar clave..." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importar chave..." + } + } + } + }, + "Import existing builds": { + "comment": "CollectionView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import existing builds" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa compilazioni esistenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer des compilations existantes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorhandene Builds importieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importar compilaciones existentes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importar compilações existentes" + } + } + } + }, + "Import prod.keys": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import prod.keys" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa prod.keys" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer prod.keys" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "prod.keys importieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importar prod.keys" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importar prod.keys" + } + } + } + }, + "Import prod.keys first.": { + "comment": "AddGameView+Rows.swift:174", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import prod.keys first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa prima prod.keys." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez d’abord prod.keys." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Importiere zuerst prod.keys." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importa prod.keys primero." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importe prod.keys primeiro." + } + } + } + }, + "Import prod.keys from the console these files were dumped with.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import prod.keys from the console these files were dumped with." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa prod.keys dalla console da cui sono stati estratti questi file." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez prod.keys depuis la console utilisée pour extraire ces fichiers." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Importiere prod.keys von der Konsole, mit der diese Dateien ausgelesen wurden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importa prod.keys de la consola de la que se volcaron estos archivos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importe prod.keys do console usado para extrair estes arquivos." + } + } + } + }, + "Import prod.keys from your console again.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import prod.keys from your console again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa di nuovo prod.keys dalla tua console." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez à nouveau prod.keys depuis votre console." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Importiere prod.keys erneut von deiner Konsole." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a importar prod.keys de tu consola." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importe prod.keys do seu console novamente." + } + } + } + }, + "Import save…": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import save…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa salvataggio…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer une sauvegarde…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstand importieren …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importar partida…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importar dados salvos…" + } + } + } + }, + "Import sounds and wallpapers from Selene": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import sounds and wallpapers from Selene" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa suoni e sfondi da Selene" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer les sons et fonds d’écran de Selene" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sounds und Hintergrundbilder aus Selene importieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importar sonidos y fondos de Selene" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importar sons e papéis de parede do Selene" + } + } + } + }, + "Import this save?": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import this save?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importare questo salvataggio?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer cette sauvegarde ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diesen Spielstand importieren?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Importar esta partida?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importar estes dados salvos?" + } + } + } + }, + "Import your keys on their card if needed.": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import your keys on their card if needed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Se necessario, importa le tue chiavi dalla relativa scheda." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importez vos clés depuis leur carte si nécessaire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Importiere deine Schlüssel bei Bedarf über die zugehörige Karte." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importa tus claves desde su tarjeta si es necesario." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importe suas chaves no cartão correspondente, se necessário." + } + } + } + }, + "Import, replace or detect private keys.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Import, replace or detect private keys." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importa, sostituisci o rileva le chiavi private." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importer, remplacer ou détecter les clés privées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Private Schlüssel importieren, ersetzen oder erkennen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importa, reemplaza o detecta claves privadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importe, substitua ou detecte chaves privadas." + } + } + } + }, + "Imported %@ files": { + "comment": "SettingsSoundSection.swift:75", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Imported %@ files" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "File importati: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ fichiers importés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Dateien importiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos importados: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos importados: %@" + } + } + } + }, + "Imported successfully": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Imported successfully" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Importazione riuscita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Importation réussie" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erfolgreich importiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Importado correctamente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Importação concluída" + } + } + } + }, + "In Game options, set Language to Game Default or to one of the game's languages, then port again.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "In Game options, set Language to Game Default or to one of the game's languages, then port again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "In Opzioni del gioco, imposta Lingua su Predefinita del gioco o su una delle lingue incluse nel gioco, poi crea di nuovo l’app per Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dans Options du jeu, réglez Langue sur Par défaut du jeu ou sur l’une des langues du jeu, puis recréez l’app Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Setze die Sprache in den Spieloptionen auf „Spielstandard“ oder eine Sprache des Spiels und erstelle die App erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "En Opciones del juego, establece Idioma en Predeterminado del juego o en uno de los idiomas del juego y vuelve a compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Em Opções do jogo, defina Idioma como Padrão do jogo ou um dos idiomas do jogo e compile novamente." + } + } + } + }, + "In game": { + "comment": "ControlsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "In game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nel gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "En el juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "No jogo" + } + } + } + }, + "In package": { + "comment": "AddGameView+Rows.swift:115", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "In package" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nel pacchetto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dans le paquet" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Paket" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "En el paquete" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "No pacote" + } + } + } + }, + "In the NUP file": { + "comment": "AddGameView+Rows.swift:115", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "In the NUP file" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nel file NUP" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dans le fichier NUP" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In der NUP-Datei" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "En el archivo NUP" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "No arquivo NUP" + } + } + } + }, + "In use": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "In use" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "In uso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In Verwendung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "En uso" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Em uso" + } + } + } + }, + "Included in NX Library": { + "comment": "SettingsEngineSection.swift:497", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Included in NX Library" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Incluso in NX Library" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intégré à NX Library" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In NX Library enthalten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Incluido en NX Library" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Incluído no NX Library" + } + } + } + }, + "Info": { + "comment": "GameOptionsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Info" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Informazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Infos" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Info" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Información" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Informações" + } + } + } + }, + "Information": { + "comment": "SeleneSettingsRow.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Information" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Informazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Informations" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Informationen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Información" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Informações" + } + } + } + }, + "Inspired by Selene (MIT) by Matthew Silva.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Inspired by Selene (MIT) by Matthew Silva." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ispirata a Selene (MIT) di Matthew Silva." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inspiré de Selene (MIT), par Matthew Silva." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Inspiriert von Selene (MIT) von Matthew Silva." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Inspirado en Selene (MIT), de Matthew Silva." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Inspirado no Selene (MIT), de Matthew Silva." + } + } + } + }, + "Install": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalar" + } + } + } + }, + "Install %@": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installa %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installer %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ installieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalar %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalar %@" + } + } + } + }, + "Install all": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install all" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installa tutto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout installer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle installieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalar todo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalar tudo" + } + } + } + }, + "Install all (%@)": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install all (%@)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installa tutto (%@)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout installer (%@)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle installieren (%@)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalar todo (%@)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalar tudo (%@)" + } + } + } + }, + "Install and switch this game's cheats.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install and switch this game's cheats." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installa e attiva o disattiva i trucchi di questo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installer et activer ou désactiver les codes de triche de ce jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cheats dieses Spiels installieren und umschalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instala y activa o desactiva los trucos de este juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instale e ative ou desative as trapaças deste jogo." + } + } + } + }, + "Install as cheats for %@? Cheats made for another version can crash the game.": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install as cheats for %@? Cheats made for another version can crash the game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installare come trucchi per %@? I trucchi creati per un’altra versione possono causare l’arresto anomalo del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installer comme codes de triche pour %@ ? Des codes conçus pour une autre version peuvent faire planter le jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Als Cheats für %@ installieren? Cheats für eine andere Version können das Spiel zum Absturz bringen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Instalar como trucos para %@? Los trucos de otra versión pueden provocar que el juego se cierre inesperadamente." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalar como trapaças para %@? Trapaças feitas para outra versão podem fazer o jogo falhar." + } + } + } + }, + "Install cheats?": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install cheats?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installare i trucchi?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installer les codes de triche ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cheats installieren?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Instalar trucos?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalar trapaças?" + } + } + } + }, + "Install the certified compiler from Setup, or allow untested toolchains in Settings → Engine.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install the certified compiler from Setup, or allow untested toolchains in Settings → Engine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installa il compilatore certificato da Configurazione oppure consenti le toolchain non testate in Impostazioni → Motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installez le compilateur certifié depuis Configuration, ou autorisez les chaînes d’outils non testées dans Réglages → Moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installiere den zertifizierten Compiler über die Einrichtung oder erlaube ungetestete Toolchains unter Einstellungen → Engine." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instala el compilador certificado desde Configuración inicial o permite cadenas de herramientas no probadas en Ajustes → Motor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instale o compilador certificado em Configuração ou permita conjuntos de ferramentas não testados em Ajustes → Engine." + } + } + } + }, + "Install these cheats?": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Install these cheats?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installare questi trucchi?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installer ces codes de triche ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Cheats installieren?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Instalar estos trucos?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalar estas trapaças?" + } + } + } + }, + "Installation failed": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installation failed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installazione non riuscita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de l’installation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installation fehlgeschlagen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La instalación ha fallado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Falha na instalação" + } + } + } + }, + "Installed": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalados" + } + } + } + }, + "Installed and set up.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installed and set up." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installato e configurato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installé et configuré." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installiert und eingerichtet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalado y configurado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalado e configurado." + } + } + } + }, + "Installed games": { + "comment": "SpotlightView.swift:440", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installed games" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi installati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux installés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installierte Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos instalados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogos instalados" + } + } + } + }, + "Installed the new build of %@.": { + "comment": "AppViewModel+PendingInstall.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installed the new build of %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuova compilazione di %@ installata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La nouvelle compilation de %@ a été installée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der neue Build von %@ wurde installiert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha instalado la nueva compilación de %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A nova compilação de %@ foi instalada." + } + } + } + }, + "Installed, but too many mods are on. Turn one off first.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installed, but too many mods are on. Turn one off first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installata, ma ci sono troppe mod attive. Disattivane prima una." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installé, mais trop de mods sont activés. Désactivez-en un d’abord." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installiert, aber zu viele Mods sind aktiv. Deaktiviere zuerst einen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalado, pero hay demasiados mods activados. Desactiva uno primero." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalado, mas há mods demais ativados. Desative um primeiro." + } + } + } + }, + "Installing update": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installing update" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installazione dell’aggiornamento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installation de la mise à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Update wird installiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalando actualización" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalando atualização" + } + } + } + }, + "Installing…": { + "comment": "DetailsBands.swift:295", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installing…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installazione…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installation…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird installiert …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Instalando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instalando…" + } + } + } + }, + "Installs to %@. On a failure or a cancel the previous app stays as it was.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Installs to %@. On a failure or a cancel the previous app stays as it was." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Installazione in %@. In caso di errore o annullamento, l’app precedente resta invariata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Installation dans %@. En cas d’échec ou d’annulation, l’app précédente reste inchangée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installation in %@. Bei einem Fehler oder Abbruch bleibt die bisherige App unverändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se instala en %@. Si hay un fallo o se cancela, la app anterior sigue intacta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Instala em %@. Em caso de falha ou cancelamento, o app anterior permanece como estava." + } + } + } + }, + "Interface & text": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Interface & text" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Interfaccia e testo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Interface et texte" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Oberfläche & Text" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Interfaz y texto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Interface e texto" + } + } + } + }, + "Interface colours": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Interface colours" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colori dell’interfaccia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleurs de l’interface" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Oberflächenfarben" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Colores de la interfaz" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cores da interface" + } + } + } + }, + "Interface sounds": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Interface sounds" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Suoni dell’interfaccia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sons de l’interface" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Oberflächenklänge" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sonidos de la interfaz" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sons da interface" + } + } + } + }, + "Interface sounds, volume and the launch screen.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Interface sounds, volume and the launch screen." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Suoni dell’interfaccia, volume e schermata di avvio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sons de l’interface, volume et écran de lancement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Oberflächenklänge, Lautstärke und Startbildschirm." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sonidos de la interfaz, volumen y pantalla de inicio." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sons da interface, volume e tela de abertura." + } + } + } + }, + "Intermediate files created during shader translation and staging in the app-owned workspace. Deleting them frees disk space; your built games and saves will not be touched.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Intermediate files created during shader translation and staging in the app-owned workspace. Deleting them frees disk space; your built games and saves will not be touched." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "File intermedi creati durante la traduzione degli shader e la preparazione nell’area di lavoro gestita dall’app. Eliminarli libera spazio su disco; i giochi compilati e i salvataggi non verranno modificati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichiers intermédiaires créés lors de la traduction des shaders et de la préparation dans l’espace de travail de l’app. Les supprimer libère de l’espace disque ; vos jeux compilés et sauvegardes restent intacts." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zwischendateien, die beim Übersetzen von Shadern und bei der Vorbereitung im app-eigenen Arbeitsverzeichnis entstehen. Löschen gibt Speicherplatz frei; erstellte Spiele-Apps und Spielstände bleiben unverändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos intermedios creados durante la traducción de shaders y la preparación en el espacio de trabajo de la app. Eliminarlos libera espacio en disco; los juegos compilados y las partidas guardadas seguirán intactos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos intermediários criados durante a tradução de shaders e a preparação na área de trabalho do app. Apagá-los libera espaço em disco; seus jogos compilados e dados salvos não serão alterados." + } + } + } + }, + "Internal resolution": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Internal resolution" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Risoluzione interna" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résolution interne" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Interne Auflösung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Resolución interna" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Resolução interna" + } + } + } + }, + "Intro animation": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Intro animation" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Animazione introduttiva" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Animation d’introduction" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Intro-Animation" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Animación de entrada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Animação de abertura" + } + } + } + }, + "Invalid key line number %@.": { + "comment": "KeyStore.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Invalid key line number %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riga %@ del file delle chiavi non valida." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ligne de clé non valide, numéro %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ungültige Schlüsselzeile Nummer %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La línea %@ del archivo de claves no es válida." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chave inválida na linha %@." + } + } + } + }, + "Is this the right game?": { + "comment": "GameBananaBandModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Is this the right game?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "È il gioco giusto?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Est-ce le bon jeu ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ist dies das richtige Spiel?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Es este el juego correcto?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este é o jogo certo?" + } + } + } + }, + "It leaves the library. Your game files and the build cache stay on disk.": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "It leaves the library. Your game files and the build cache stay on disk." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Viene rimosso dalla libreria. I file del gioco e la cache di compilazione restano sul disco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le jeu quitte la bibliothèque. Vos fichiers de jeu et le cache de compilation restent sur le disque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Spiel wird aus der Bibliothek entfernt. Deine Spieldateien und der Build-Cache bleiben auf dem Datenträger." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se quita de la biblioteca. Los archivos del juego y la caché de compilación se conservan en el disco." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O jogo sairá da biblioteca. Seus arquivos de jogo e o cache de compilação permanecerão no disco." + } + } + } + }, + "It may have been moved or removed.": { + "comment": "GameResourceState.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "It may have been moved or removed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Potrebbe essere stata spostata o rimossa." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Elle a peut-être été déplacée ou supprimée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sie wurde möglicherweise verschoben oder entfernt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Puede que se haya movido o eliminado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ele pode ter sido movido ou removido." + } + } + } + }, + "Its new features reach NX Library in a future version.": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Its new features reach NX Library in a future version." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le sue nuove funzionalità arriveranno in una futura versione di NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ses nouvelles fonctionnalités seront intégrées à une future version de NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Seine neuen Funktionen kommen in einer künftigen Version in NX Library an." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sus nuevas funciones llegarán a NX Library en una versión futura." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Seus novos recursos chegarão ao NX Library em uma versão futura." + } + } + } + }, + "Just now": { + "comment": "LibraryPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Just now" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Adesso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À l’instant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gerade eben" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ahora mismo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Agora mesmo" + } + } + } + }, + "Keep": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keep" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Conserva" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conserver" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Behalten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conservar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Manter" + } + } + } + }, + "Keep a copy of your current progress.": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keep a copy of your current progress." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Conserva una copia dei tuoi progressi attuali." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conserver une copie de votre progression actuelle." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eine Kopie deines aktuellen Fortschritts sichern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Guarda una copia de tu progreso actual." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mantenha uma cópia do seu progresso atual." + } + } + } + }, + "Keep editing": { + "comment": "AddGameView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keep editing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Continua a modificare" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Continuer les modifications" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weiter bearbeiten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Seguir editando" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Continuar editando" + } + } + } + }, + "Keep it": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keep it" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Conserva" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conserver" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Behalten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conservarlo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Manter" + } + } + } + }, + "Keep the game closed, including launches from Finder, until the operation finishes.": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keep the game closed, including launches from Finder, until the operation finishes." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tieni il gioco chiuso, anche evitando avvii dal Finder, finché l’operazione non termina." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gardez le jeu fermé, y compris depuis le Finder, jusqu’à la fin de l’opération." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lass das Spiel geschlossen und starte es auch nicht im Finder, bis der Vorgang abgeschlossen ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mantén el juego cerrado, también al abrirlo desde Finder, hasta que termine la operación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mantenha o jogo fechado, sem iniciá-lo pelo Finder, até a operação terminar." + } + } + } + }, + "Keeps a pipeline archive; uses more memory.": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keeps a pipeline archive; uses more memory." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Conserva un archivio delle pipeline; usa più memoria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Conserve une archive de pipelines ; utilise plus de mémoire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Behält ein Pipeline-Archiv bei; benötigt mehr Speicher." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Conserva un archivo de pipelines; usa más memoria." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mantém um arquivo de pipelines; usa mais memória." + } + } + } + }, + "Key check, failure explanations and diagnostics, adapted (MIT)": { + "comment": "SettingsAboutSection.swift:24", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Key check, failure explanations and diagnostics, adapted (MIT)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controllo delle chiavi, spiegazioni degli errori e dati diagnostici, adattati (MIT)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification des clés, explications des échecs et diagnostic, adaptés (MIT)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüsselprüfung, Fehlererklärungen und Diagnosedaten, angepasst (MIT)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobación de claves, explicaciones de errores y diagnóstico, adaptados (MIT)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificação de chaves, explicações de falhas e diagnóstico, adaptados (MIT)" + } + } + } + }, + "Key not removed": { + "comment": "AppViewModel+SteamGridDB.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Key not removed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiave non rimossa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé non supprimée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel nicht entfernt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha eliminado la clave" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chave não removida" + } + } + } + }, + "Key saved · covers, backgrounds, logos and icons in Change artwork": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Key saved · covers, backgrounds, logos and icons in Change artwork" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiave salvata · copertine, sfondi, loghi e icone in Cambia grafica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé enregistrée · jaquettes, arrière-plans, logos et icônes dans Changer l’illustration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel gespeichert · Cover, Hintergründe, Logos und Symbole unter „Artwork ändern“" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clave guardada · portadas, fondos, logotipos e iconos en Cambiar imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chave salva · capas, fundos, logotipos e ícones em Alterar arte" + } + } + } + }, + "Keys": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keys" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiavi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Claves" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chaves" + } + } + } + }, + "Keys imported": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keys imported" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiavi importate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clés importées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel importiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Claves importadas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chaves importadas" + } + } + } + }, + "Keys not changed": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Keys not changed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiavi non modificate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clés non modifiées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel nicht geändert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han cambiado las claves" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chaves não alteradas" + } + } + } + }, + "LAST PLAYED": { + "comment": "DetailsTopBlock.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "LAST PLAYED" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "ULTIMA PARTITA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "DERNIÈRE PARTIE" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ZULETZT GESPIELT" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ÚLTIMA PARTIDA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ÚLTIMA SESSÃO" + } + } + } + }, + "LOOKING ON %@…": { + "comment": "OnlineBrowsePanel.swift:146", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "LOOKING ON %@…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "RICERCA SU %@…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RECHERCHE SUR %@…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SUCHE AUF %@ …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "BUSCANDO EN %@…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "BUSCANDO EM %@…" + } + } + } + }, + "Landscapes": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Landscapes" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Paesaggi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Paysages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Landschaften" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Paisajes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Paisagens" + } + } + } + }, + "Language": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Language" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lingua" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Langue" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sprache" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Idioma" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Idioma" + } + } + } + }, + "Language & region": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Language & region" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lingua e zona" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Langue et région" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sprache & Region" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Idioma y región" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Idioma e região" + } + } + } + }, + "Largest build caches first. Sizes describe file contents; the space recovered can differ. Shared folders count once in totals.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Largest build caches first. Sizes describe file contents; the space recovered can differ. Shared folders count once in totals." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prima le cache di compilazione più grandi. Le dimensioni descrivono il contenuto dei file; lo spazio recuperato può essere diverso. Le cartelle condivise vengono contate una sola volta nei totali." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les plus gros caches de compilation en premier. Les tailles décrivent le contenu des fichiers ; l’espace récupéré peut différer. Les dossiers partagés ne sont comptés qu’une fois dans les totaux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Größte Build-Caches zuerst. Größen beschreiben Dateiinhalte; der freigegebene Speicher kann abweichen. Gemeinsam genutzte Ordner zählen in Summen nur einmal." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Primero las cachés de compilación más grandes. Los tamaños indican el contenido de los archivos; el espacio recuperado puede variar. Las carpetas compartidas se cuentan una sola vez en los totales." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Maiores caches de compilação primeiro. Os tamanhos correspondem ao conteúdo dos arquivos; o espaço recuperado pode ser diferente. Pastas compartilhadas são contadas uma vez nos totais." + } + } + } + }, + "Last porting cancelled · Open log": { + "comment": "SpotlightInfoView.swift:265", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Last porting cancelled · Open log" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ultima creazione dell’app per Mac annullata · Apri log" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernière création d’app Mac annulée · Ouvrir le journal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Letzte App-Erstellung abgebrochen · Protokoll öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Última compilación cancelada · Abrir registro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Última compilação cancelada · Abrir log" + } + } + } + }, + "Last porting failed": { + "comment": "SpotlightInfoView.swift:266", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Last porting failed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ultima creazione dell’app per Mac non riuscita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de la dernière création d’app Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Letzte App-Erstellung fehlgeschlagen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La última compilación ha fallado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A última compilação falhou" + } + } + } + }, + "Latest": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Latest" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Novità" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveautés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueste" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Novedades" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novidades" + } + } + } + }, + "Launch": { + "comment": "ConsoleView.swift:18", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launch" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstart" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Execução" + } + } + } + }, + "Launch brightness": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launch brightness" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Luminosità all’avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Luminosité au lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Helligkeit beim Start" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Brillo al iniciar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Brilho da abertura" + } + } + } + }, + "Launch mode": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launch mode" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Modalità di avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mode de lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Startmodus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Modo de inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Modo de abertura" + } + } + } + }, + "Launch screen motion": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launch screen motion" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Animazione della schermata di avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Animation de l’écran de lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Startbildschirmbewegung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Movimiento en la pantalla de inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Movimento da tela de abertura" + } + } + } + }, + "Launch screen shade": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launch screen shade" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ombreggiatura della schermata di avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assombrissement de l’écran de lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Startbildschirmabdunklung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Oscurecimiento de la pantalla de inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sombra da tela de abertura" + } + } + } + }, + "Launch screen style": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launch screen style" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Stile della schermata di avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Style de l’écran de lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Startbildschirmstil" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estilo de la pantalla de inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Estilo da tela de abertura" + } + } + } + }, + "Launch screen, hidden games and defaults for new games.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launch screen, hidden games and defaults for new games." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermata di avvio, giochi nascosti e valori predefiniti per i nuovi giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écran de lancement, jeux masqués et réglages par défaut des nouveaux jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Startbildschirm, ausgeblendete Spiele und Standards für neue Spiele." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pantalla de inicio, juegos ocultos y valores predeterminados para juegos nuevos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tela de abertura, jogos ocultos e padrões para novos jogos." + } + } + } + }, + "Launch the installed Mac app.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launch the installed Mac app." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avvia l’app per Mac installata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lancer l’app Mac installée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installierte Mac-App starten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la app de Mac instalada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra o app para Mac instalado." + } + } + } + }, + "Launched game: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Launched game: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioco avviato: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu lancé : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel gestartet: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juego iniciado: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo iniciado: %@" + } + } + } + }, + "Left stick": { + "comment": "ControlsView.swift:29", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Left stick" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Stick sinistro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stick gauche" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Linker Stick" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Stick izquierdo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Analógico esquerdo" + } + } + } + }, + "Let's get NX Library ready": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Let's get NX Library ready" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prepariamo NX Library" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparons NX Library" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Machen wir NX Library startklar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vamos a preparar NX Library" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Vamos preparar o NX Library" + } + } + } + }, + "Lets the game lower its resolution under load. Turn off to keep full resolution for the selected console mode.": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Lets the game lower its resolution under load. Turn off to keep full resolution for the selected console mode." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Consente al gioco di ridurre la risoluzione sotto carico. Disattivala per mantenere la piena risoluzione della modalità console selezionata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Permet au jeu de réduire sa résolution en cas de forte charge. Désactivez pour conserver la pleine résolution du mode de console sélectionné." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erlaubt dem Spiel, bei hoher Last seine Auflösung zu senken. Deaktivieren, um die volle Auflösung des ausgewählten Konsolenmodus beizubehalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Permite que el juego reduzca su resolución bajo carga. Desactívalo para mantener la resolución completa del modo de consola seleccionado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Permite que o jogo reduza a resolução sob carga. Desative para manter a resolução total do modo de console selecionado." + } + } + } + }, + "Library": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothek" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Biblioteca" + } + } + } + }, + "Library & game": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library & game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Libreria e gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bibliothèque et jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothek & Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Biblioteca y juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Biblioteca e jogo" + } + } + } + }, + "Library apps": { + "comment": "SettingsStorageSection.swift:22", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library apps" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App della libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apps de la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliotheks-Apps" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Apps de la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Apps da biblioteca" + } + } + } + }, + "Library controls": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library controls" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Comandi della libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commandes de la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothekssteuerung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Controles de la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Controles da biblioteca" + } + } + } + }, + "Library controls and new-game rumble.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library controls and new-game rumble." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Comandi della libreria e vibrazione dei nuovi giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commandes de la bibliothèque et vibrations des nouveaux jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothekssteuerung und Vibration für neue Spiele." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Controles de la biblioteca y vibración de juegos nuevos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Controles da biblioteca e vibração para novos jogos." + } + } + } + }, + "Library navigation": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library navigation" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Navigazione nella libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Navigation dans la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliotheksnavigation" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Navegación de la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Navegação na biblioteca" + } + } + } + }, + "Library refresh": { + "comment": "MenuSelectionModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library refresh" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiorna libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actualiser la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothek aktualisieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualización de la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizar biblioteca" + } + } + } + }, + "Library screens": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library screens" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermate della libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écrans de la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliotheksansichten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pantallas de la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Telas da biblioteca" + } + } + } + }, + "Library shade": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library shade" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ombreggiatura della libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assombrissement de la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliotheksabdunklung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Oscurecimiento de la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sombra da biblioteca" + } + } + } + }, + "Library · Launch": { + "comment": "ChangeArtworkView.swift:310", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Library · Launch" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Libreria · Avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bibliothèque · Lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothek · Spielstart" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Biblioteca · Inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Biblioteca · Abertura" + } + } + } + }, + "Licenses": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Licenses" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Licenze" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Licences" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lizenzen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Licencias" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Licenças" + } + } + } + }, + "Light": { + "comment": "AppIconAppearanceMode.swift:5", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Light" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiaro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clair" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hell" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Claro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Claro" + } + } + } + }, + "Live": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Live" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Animati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Animés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Live" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Animados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Animados" + } + } + } + }, + "Live output": { + "comment": "BuildLiveLogsDrawer.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Live output" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Output in tempo reale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sortie en direct" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Live-Ausgabe" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Salida en directo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Saída em tempo real" + } + } + } + }, + "Loading…": { + "comment": "ChangeArtworkView.swift:299", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Loading…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Caricamento…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chargement…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird geladen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cargando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Carregando…" + } + } + } + }, + "Locally cached game metadata such as title IDs, publisher names, release dates, and category tags fetched during library scans.": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Locally cached game metadata such as title IDs, publisher names, release dates, and category tags fetched during library scans." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metadati dei giochi conservati nella cache locale, come title ID, nomi degli editori, date di uscita e tag delle categorie, recuperati durante le scansioni della libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Métadonnées de jeu mises en cache localement, telles que les title ID, les noms d’éditeur, les dates de sortie et les catégories, récupérées lors des analyses de la bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lokal zwischengespeicherte Spielmetadaten wie title IDs, Publisher-Namen, Veröffentlichungsdaten und Kategorien, die beim Durchsuchen der Bibliothek abgerufen werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Metadatos de juegos guardados en la caché local, como title IDs, nombres de editoras, fechas de lanzamiento y etiquetas de categorías, obtenidos al analizar la biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Metadados de jogos em cache local, como title IDs, nomes de publicadoras, datas de lançamento e categorias, obtidos durante as buscas na biblioteca." + } + } + } + }, + "Log": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Log" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Log" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Journal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Protokoll" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Registro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Log" + } + } + } + }, + "Logo": { + "comment": "ArtworkPicker.swift:14", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Logo" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Logo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Logo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Logo" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Logotipo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Logotipo" + } + } + } + }, + "Logo on black": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Logo on black" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Logo su nero" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Logo sur fond noir" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Logo auf Schwarz" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Logotipo sobre negro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Logotipo em fundo preto" + } + } + } + }, + "Logs": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Logs" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Log" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Journaux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Protokolle" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Registros" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Logs" + } + } + } + }, + "Look": { + "comment": "SettingsSection.swift:19", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Look" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aspetto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Apparence" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erscheinungsbild" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Apariencia" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aparência" + } + } + } + }, + "Look for a newer NX Library release at launch, at most once a day.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Look for a newer NX Library release at launch, at most once a day." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca una nuova versione di NX Library all’avvio, al massimo una volta al giorno." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher une nouvelle version de NX Library au lancement, au maximum une fois par jour." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beim Start nach einer neueren NX Library-Veröffentlichung suchen, höchstens einmal täglich." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Busca una versión más reciente de NX Library al abrir, como máximo una vez al día." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Busca uma versão mais recente do NX Library ao iniciar, no máximo uma vez por dia." + } + } + } + }, + "Looking for a new release…": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for a new release…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca di una nuova versione…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche d’une nouvelle version…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Veröffentlichung wird gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando una nueva versión…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando uma nova versão…" + } + } + } + }, + "Looking for an icon…": { + "comment": "AppIconProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for an icon…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca di un’icona…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche d’une icône…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Symbol wird gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando un icono…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando um ícone…" + } + } + } + }, + "Looking for existing game apps…": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for existing game apps…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca delle app dei giochi esistenti…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche des apps de jeux existantes…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorhandene Spiele-Apps werden gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando apps de juegos existentes…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando apps de jogos existentes…" + } + } + } + }, + "Looking for mods that work with your games…": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for mods that work with your games…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca di mod compatibili con i tuoi giochi…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche de mods compatibles avec vos jeux…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mods, die mit deinen Spielen funktionieren, werden gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando mods compatibles con tus juegos…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando mods compatíveis com seus jogos…" + } + } + } + }, + "Looking for mods that work…": { + "comment": "GameBananaBandModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for mods that work…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca di mod compatibili…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche de mods compatibles…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kompatible Mods werden gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando mods compatibles…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando mods compatíveis…" + } + } + } + }, + "Looking for more mods…": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking for more mods…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca di altre mod…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche d’autres mods…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weitere Mods werden gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando más mods…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando mais mods…" + } + } + } + }, + "Looking on Title DB and GameTDB…": { + "comment": "ChangeMetadataView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking on Title DB and GameTDB…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca su Title DB e GameTDB…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche sur Title DB et GameTDB…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Title DB und GameTDB werden durchsucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando en Title DB y GameTDB…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando no Title DB e no GameTDB…" + } + } + } + }, + "Looking…": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looking…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando…" + } + } + } + }, + "Looks for a newer NX Library release at launch, at most once a day.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looks for a newer NX Library release at launch, at most once a day." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca una nuova versione di NX Library all’avvio, al massimo una volta al giorno." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche une nouvelle version de NX Library au lancement, au maximum une fois par jour." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sucht beim Start nach einer neueren NX Library-Veröffentlichung, höchstens einmal täglich." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Busca una versión más reciente de NX Library al abrir, como máximo una vez al día." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Busca uma versão mais recente do NX Library ao iniciar, no máximo uma vez por dia." + } + } + } + }, + "Looping live wallpapers in 4K and HD. Every video belongs to its owner.": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Looping live wallpapers in 4K and HD. Every video belongs to its owner." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondi animati in loop in 4K e HD. Ogni video appartiene al rispettivo proprietario." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fonds d’écran animés en boucle, en 4K et HD. Chaque vidéo appartient à son propriétaire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Live-Hintergrundbilder in Dauerschleife in 4K und HD. Alle Videos gehören ihren jeweiligen Rechteinhabern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondos animados en bucle en 4K y HD. Cada vídeo pertenece a su propietario." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Papéis de parede animados em loop, em 4K e HD. Cada vídeo pertence ao seu titular." + } + } + } + }, + "MODS": { + "comment": "ModsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "MODS" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "MOD" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MODS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "MODS" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "MODS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "MODS" + } + } + } + }, + "MORE SETTINGS": { + "comment": "MoreSettingsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "MORE SETTINGS" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "ALTRE IMPOSTAZIONI" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "AUTRES RÉGLAGES" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "WEITERE EINSTELLUNGEN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "MÁS AJUSTES" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "MAIS AJUSTES" + } + } + } + }, + "MOST PLAYED": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "MOST PLAYED" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "PIÙ GIOCATO" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "LES PLUS JOUÉS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "AM MEISTEN GESPIELT" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "MÁS JUGADOS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "MAIS JOGADO" + } + } + } + }, + "Mac App (.app)": { + "comment": "TargetPlatform.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mac App (.app)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App per Mac (.app)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "App Mac (.app)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mac-App (.app)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "App de Mac (.app)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "App para Mac (.app)" + } + } + } + }, + "Manage mods": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Manage mods" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gestisci mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Gérer les mods" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mods verwalten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Gestionar mods" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Gerenciar mods" + } + } + } + }, + "Manual backup": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Manual backup" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Backup manuale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde manuelle" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Manuelles Backup" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copia de seguridad manual" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Backup manual" + } + } + } + }, + "Match screen": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Match screen" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Come lo schermo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Adapter à l’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "An Bildschirm anpassen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Adaptar a la pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Acompanhar tela" + } + } + } + }, + "Maxwell shader translator and SPIR-V generator": { + "comment": "SettingsAboutSection.swift:16", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Maxwell shader translator and SPIR-V generator" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Traduttore degli shader Maxwell e generatore SPIR-V" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Traducteur de shaders Maxwell et générateur SPIR-V" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Maxwell-Shader-Übersetzer und SPIR-V-Generator" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Traductor de shaders Maxwell y generador de SPIR-V" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tradutor de shaders Maxwell e gerador de SPIR-V" + } + } + } + }, + "Menu": { + "comment": "SeleneHomeChrome.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Menu" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Menu" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Menu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Menü" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Menú" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Menu" + } + } + } + }, + "Menus, genres and available game descriptions. The games’ own language stays separate.": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Menus, genres and available game descriptions. The games’ own language stays separate." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Menu, generi e descrizioni dei giochi, quando disponibili. La lingua dei giochi si sceglie a parte." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Menus, genres et descriptions des jeux disponibles. La langue des jeux se règle séparément." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Menüs, Genres und verfügbare Spielbeschreibungen. Die Sprache der Spiele wird separat eingestellt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Menús, géneros y descripciones de juegos disponibles. El idioma de los juegos se elige por separado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Menus, gêneros e descrições dos jogos, quando disponíveis. O idioma dos jogos é escolhido separadamente." + } + } + } + }, + "Mesosphère kernel synchronization semantics": { + "comment": "SettingsAboutSection.swift:17", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mesosphère kernel synchronization semantics" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Semantica di sincronizzazione del kernel Mesosphère" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sémantique de synchronisation du noyau Mesosphère" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Synchronisierungssemantik des Mesosphère-Kernels" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Semántica de sincronización del kernel de Mesosphère" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Semântica de sincronização do kernel Mesosphère" + } + } + } + }, + "Metadata cache": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Metadata cache" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cache dei metadati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cache des métadonnées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Metadaten-Cache" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Caché de metadatos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cache de metadados" + } + } + } + }, + "Metal 4 pipelines": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Metal 4 pipelines" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pipeline Metal 4" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pipelines Metal 4" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Metal 4-Pipelines" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pipelines de Metal 4" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pipelines do Metal 4" + } + } + } + }, + "Metal HUD": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Metal HUD" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "HUD Metal" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "HUD Metal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Metal HUD" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "HUD de Metal" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "HUD do Metal" + } + } + } + }, + "Metal compiler": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Metal compiler" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilatore Metal" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilateur Metal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Metal-Compiler" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilador Metal" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilador Metal" + } + } + } + }, + "Metal unknown": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Metal unknown" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metal sconosciuto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Metal inconnu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Metal unbekannt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Metal desconocido" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Metal desconhecido" + } + } + } + }, + "MetalFX": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "MetalFX" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "MetalFX" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "MetalFX" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "MetalFX" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "MetalFX" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "MetalFX" + } + } + } + }, + "MetalFX upscaling": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "MetalFX upscaling" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Upscaling MetalFX" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à l’échelle MetalFX" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "MetalFX-Upscaling" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reescalado con MetalFX" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ampliação com MetalFX" + } + } + } + }, + "Microsoft's release list has no .NET 10 SDK installer for this Mac.": { + "comment": "DotnetRelease.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Microsoft's release list has no .NET 10 SDK installer for this Mac." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’elenco delle versioni di Microsoft non contiene un programma di installazione dell’SDK .NET 10 per questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La liste des versions de Microsoft ne contient aucun programme d’installation du SDK .NET 10 pour ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Microsofts Veröffentlichungsliste enthält kein Installationsprogramm für das .NET 10 SDK für diesen Mac." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La lista de versiones de Microsoft no contiene un instalador del SDK de .NET 10 para este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A lista de versões da Microsoft não tem um instalador do SDK do .NET 10 para este Mac." + } + } + } + }, + "Minimal": { + "comment": "AppSettings.swift:91", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Minimal" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Minimale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Minimal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Minimal" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mínimo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mínimo" + } + } + } + }, + "Missing": { + "comment": "ThemeStore.swift:107", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Missing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Manquant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fehlt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ausente" + } + } + } + }, + "Mod data provided by GameBanana": { + "comment": "ModDetailPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mod data provided by GameBanana" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dati delle mod forniti da GameBanana" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données des mods fournies par GameBanana" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mod-Daten von GameBanana" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Datos de mods proporcionados por GameBanana" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dados de mods fornecidos pelo GameBanana" + } + } + } + }, + "Mod data provided by GameBanana · only mods that work with NX Library are shown": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mod data provided by GameBanana · only mods that work with NX Library are shown" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dati delle mod forniti da GameBanana · vengono mostrate solo le mod compatibili con NX Library" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données des mods fournies par GameBanana · seuls les mods compatibles avec NX Library sont affichés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mod-Daten von GameBanana · Es werden nur Mods angezeigt, die mit NX Library funktionieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Datos de mods proporcionados por GameBanana · solo se muestran mods compatibles con NX Library" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dados de mods fornecidos pelo GameBanana · apenas mods compatíveis com o NX Library são exibidos" + } + } + } + }, + "Mods": { + "comment": "DetailsBands.swift:236", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mods" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mods" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mods" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mods" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mods" + } + } + } + }, + "Mods change the game's files only: mods that patch code (exefs, such as 60 fps patches) don't work. ": { + "comment": "ModsPanel.swift:399", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mods change the game's files only: mods that patch code (exefs, such as 60 fps patches) don't work. " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le mod modificano solo i file del gioco: quelle che applicano patch al codice (exefs, come le patch per 60 fps) non funzionano. " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les mods modifient uniquement les fichiers du jeu : les mods qui modifient le code (exefs, comme les correctifs 60 FPS) ne fonctionnent pas. " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mods ändern nur die Dateien des Spiels: Mods, die Code patchen (exefs, etwa 60-FPS-Patches), funktionieren nicht. " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los mods solo cambian los archivos del juego: los que modifican el código (exefs, como los parches de 60 fps) no funcionan. " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os mods alteram apenas os arquivos do jogo: mods que modificam código (exefs, como patches de 60 fps) não funcionam. " + } + } + } + }, + "Mods that work with your games, from GameBanana.": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mods that work with your games, from GameBanana." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mod compatibili con i tuoi giochi, da GameBanana." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Des mods compatibles avec vos jeux, depuis GameBanana." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mods von GameBanana, die mit deinen Spielen funktionieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mods compatibles con tus juegos, de GameBanana." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mods compatíveis com seus jogos, do GameBanana." + } + } + } + }, + "Mods · %@ on": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Mods · %@ on" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mod · %@ attive" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mods · %@ activés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mods · %@ aktiv" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mods · %@ activados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mods · %@ ativados" + } + } + } + }, + "Monochrome": { + "comment": "AppIconAppearanceMode.swift:7", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Monochrome" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Monocromatico" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Monochrome" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Monochrom" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Monocromo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Monocromático" + } + } + } + }, + "Moonlight": { + "comment": "ThemePickerView.swift:484", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Moonlight" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Luce lunare" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clair de lune" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mondlicht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Luz de luna" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Luar" + } + } + } + }, + "More like this": { + "comment": "DetailsBands.swift:118", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "More like this" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi simili" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeux similaires" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ähnliche Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Más juegos similares" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mais jogos como este" + } + } + } + }, + "More mods": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "More mods" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Altre mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autres mods" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weitere Mods" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Más mods" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mais mods" + } + } + } + }, + "More settings": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "More settings" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Altre impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autres réglages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weitere Einstellungen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Más ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mais ajustes" + } + } + } + }, + "Most liked": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Most liked" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Più apprezzate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les plus appréciés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Am beliebtesten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Más populares" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mais curtidos" + } + } + } + }, + "Move": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Move" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sposta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verschieben" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mover" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mover" + } + } + } + }, + "Move %@ to the Trash?": { + "comment": "ModsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Move %@ to the Trash?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spostare %@ nel Cestino?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Placer %@ dans la corbeille ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ in den Papierkorb legen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Mover %@ a la Papelera?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mover %@ para o Lixo?" + } + } + } + }, + "Move NX Library to Applications before updating.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Move NX Library to Applications before updating." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sposta NX Library in Applicazioni prima di aggiornarla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacez NX Library dans Applications avant de la mettre à jour." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verschiebe NX Library vor dem Aktualisieren in den Programme-Ordner." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mueve NX Library a Aplicaciones antes de actualizar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mova o NX Library para Applications antes de atualizar." + } + } + } + }, + "Move NX Library to Applications to update": { + "comment": "SettingsAboutSection.swift:220", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Move NX Library to Applications to update" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sposta NX Library in Applicazioni per aggiornarla" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacez NX Library dans Applications pour la mettre à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library zum Aktualisieren in den Programme-Ordner verschieben" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mueve NX Library a Aplicaciones para actualizar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mova o NX Library para Applications para atualizar" + } + } + } + }, + "Move a tile": { + "comment": "ControlsView.swift:51", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Move a tile" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sposta un riquadro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacer une vignette" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kachel verschieben" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mover una ficha" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mover um bloco" + } + } + } + }, + "Move the library to a folder outside iCloud Drive.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Move the library to a folder outside iCloud Drive." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sposta la libreria in una cartella esterna a iCloud Drive." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Déplacez la bibliothèque dans un dossier hors d’iCloud Drive." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verschiebe die Bibliothek in einen Ordner außerhalb von iCloud Drive." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mueve la biblioteca a una carpeta fuera de iCloud Drive." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mova a biblioteca para uma pasta fora do iCloud Drive." + } + } + } + }, + "Move to Trash": { + "comment": "ModsPanel.swift:218", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Move to Trash" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sposta nel Cestino" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Placer dans la corbeille" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In den Papierkorb legen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mover a la Papelera" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mover para o Lixo" + } + } + } + }, + "Movement and selection sounds stay quiet during launch. Game starting and Game ready still play.": { + "comment": "SettingsSoundSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Movement and selection sounds stay quiet during launch. Game starting and Game ready still play." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I suoni di movimento e selezione restano silenziosi durante l’avvio. I suoni di avvio del gioco e gioco pronto vengono comunque riprodotti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les sons de navigation et de sélection restent silencieux pendant le lancement. Les sons Démarrage du jeu et Jeu prêt sont toujours émis." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Navigations- und Auswahlklänge bleiben während des Starts stumm. „Spiel startet“ und „Spiel bereit“ werden weiterhin abgespielt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los sonidos de movimiento y selección se silencian durante el inicio. Los avisos de inicio y de juego listo siguen sonando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os sons de navegação e seleção ficam silenciados durante a abertura. Os sons de Jogo iniciando e Jogo pronto continuam sendo reproduzidos." + } + } + } + }, + "Music & sound": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Music & sound" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Musica e suoni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Musique et sons" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Musik & Sound" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Música y sonido" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Música e som" + } + } + } + }, + "NEWER": { + "comment": "SettingsEngineSection.swift:488", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NEWER" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "PIÙ RECENTE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PLUS RÉCENT" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NEUER" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "MÁS RECIENTES" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "MAIS RECENTE" + } + } + } + }, + "NOTHING FOUND": { + "comment": "OnlineBrowsePanel.swift:146", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NOTHING FOUND" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NESSUN RISULTATO" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "AUCUN RÉSULTAT" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NICHTS GEFUNDEN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NO SE HA ENCONTRADO NADA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "NADA ENCONTRADO" + } + } + } + }, + "NOW PLAYING": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NOW PLAYING" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "IN ESECUZIONE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EN COURS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "JETZT IM SPIEL" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "EN JUEGO" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "JOGANDO AGORA" + } + } + } + }, + "NSP reading and PFS/HFS layered filesystem": { + "comment": "SettingsAboutSection.swift:18", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NSP reading and PFS/HFS layered filesystem" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lettura NSP e file system a livelli PFS/HFS" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lecture des NSP et système de fichiers en couches PFS/HFS" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lesen von NSP-Dateien und mehrschichtiges PFS/HFS-Dateisystem" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Lectura de NSP y sistema de archivos por capas PFS/HFS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Leitura de NSP e sistema de arquivos em camadas PFS/HFS" + } + } + } + }, + "NSP · XCI · NSZ · XCZ": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · XCZ" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · XCZ" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · XCZ" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · XCZ" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · XCZ" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · XCZ" + } + } + } + }, + "NSP · XCI · NSZ · folders": { + "comment": "DropZoneView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · folders" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · cartelle" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · dossiers" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · carpetas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "NSP · XCI · NSZ · pastas" + } + } + } + }, + "NSP, XCI, NSZ and XCZ files": { + "comment": "GameFoldersCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NSP, XCI, NSZ and XCZ files" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "file NSP, XCI, NSZ e XCZ" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "fichiers NSP, XCI, NSZ et XCZ" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NSP-, XCI-, NSZ- und XCZ-Dateien" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos NSP, XCI, NSZ y XCZ" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "arquivos NSP, XCI, NSZ e XCZ" + } + } + } + }, + "NSP, XCI, NSZ, XCZ or a Switch 2 NUP.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NSP, XCI, NSZ, XCZ or a Switch 2 NUP." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NSP, XCI, NSZ, XCZ o un NUP di Switch 2." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NSP, XCI, NSZ, XCZ ou NUP Switch 2." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NSP, XCI, NSZ, XCZ oder eine Switch 2-NUP-Datei." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NSP, XCI, NSZ, XCZ o un NUP de Switch 2." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "NSP, XCI, NSZ, XCZ ou um NUP de Switch 2." + } + } + } + }, + "NX Library": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "NX Library" + } + } + } + }, + "NX Library %@": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@" + } + } + } + }, + "NX Library %@ needs macOS %@ or later; nothing was changed.": { + "comment": "AppUpdater.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@ needs macOS %@ or later; nothing was changed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@ richiede macOS %@ o successivo; non è stato modificato nulla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@ nécessite macOS %@ ou ultérieur ; aucune modification n’a été effectuée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@ benötigt macOS %@ oder neuer; es wurde nichts geändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library %@ requiere macOS %@ o posterior; no se ha cambiado nada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library %@ requer macOS %@ ou posterior; nada foi alterado." + } + } + } + }, + "NX Library cannot read this folder. Reconnect it or choose another.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library cannot read this folder. Reconnect it or choose another." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library non può leggere questa cartella. Ricollegala o scegline un’altra." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library ne peut pas lire ce dossier. Reconnectez-le ou choisissez-en un autre." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library kann diesen Ordner nicht lesen. Verbinde ihn erneut oder wähle einen anderen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library no puede leer esta carpeta. Vuelve a conectarla o elige otra." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library não consegue ler esta pasta. Reconecte ou escolha outra." + } + } + } + }, + "NX Library clones it into %@ at %@.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library clones it into %@ at %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library lo clona in %@ al commit %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library le clone dans %@ au commit %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library klont es nach %@ mit Stand %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library lo clona en %@ en el commit %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library clona o repositório em %@ no commit %@." + } + } + } + }, + "NX Library could not read the file.": { + "comment": "CheatsPanel.swift:455", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library could not read the file." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library non ha potuto leggere il file." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library n’a pas pu lire le fichier." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library konnte die Datei nicht lesen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library no ha podido leer el archivo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library não conseguiu ler o arquivo." + } + } + } + }, + "NX Library could not restart: %@": { + "comment": "AppShellView.swift:1029", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library could not restart: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library non ha potuto riavviarsi: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library n’a pas pu redémarrer : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library konnte nicht neu gestartet werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library no ha podido reiniciarse: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível reiniciar o NX Library: %@" + } + } + } + }, + "NX Library could not save your library, so recent changes may be lost when it quits: ": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library could not save your library, so recent changes may be lost when it quits: " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library non ha potuto salvare la libreria; le modifiche recenti potrebbero andare perse alla chiusura: " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library n’a pas pu enregistrer votre bibliothèque. Les modifications récentes risquent donc d’être perdues à sa fermeture : " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library konnte deine Bibliothek nicht speichern. Daher können beim Beenden die letzten Änderungen verloren gehen: " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library no ha podido guardar la biblioteca, por lo que los cambios recientes podrían perderse al salir: " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library não conseguiu salvar sua biblioteca. Por isso, as alterações recentes podem ser perdidas quando o app for encerrado: " + } + } + } + }, + "NX Library downloads Microsoft's installer; macOS asks for your password.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library downloads Microsoft's installer; macOS asks for your password." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library scarica il programma di installazione di Microsoft; macOS chiede la tua password." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library télécharge le programme d’installation de Microsoft ; macOS vous demande votre mot de passe." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library lädt das Installationsprogramm von Microsoft; macOS fragt nach deinem Passwort." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library descarga el instalador de Microsoft; macOS pide tu contraseña." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library baixa o instalador da Microsoft; o macOS pede sua senha." + } + } + } + }, + "NX Library downloads it through Xcode.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library downloads it through Xcode." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library lo scarica tramite Xcode." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library le télécharge via Xcode." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library lädt ihn über Xcode herunter." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library lo descarga a través de Xcode." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library faz o download pelo Xcode." + } + } + } + }, + "NX Library is personal software released under the MIT license. nxnative, Ryujinx, SPIR-V Cross, LibHac and the other dependencies retain their own licenses through the engine.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library is personal software released under the MIT license. nxnative, Ryujinx, SPIR-V Cross, LibHac and the other dependencies retain their own licenses through the engine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library è un software personale distribuito con licenza MIT. nxnative, Ryujinx, SPIR-V Cross, LibHac e le altre dipendenze mantengono le proprie licenze tramite il motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library est un logiciel personnel publié sous licence MIT. nxnative, Ryujinx, SPIR-V Cross, LibHac et les autres dépendances conservent leurs propres licences via le moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library ist persönliche Software unter der MIT-Lizenz. nxnative, Ryujinx, SPIR-V Cross, LibHac und die übrigen Abhängigkeiten behalten über die Engine ihre eigenen Lizenzen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library es software personal distribuido bajo la licencia MIT. nxnative, Ryujinx, SPIR-V Cross, LibHac y las demás dependencias conservan sus propias licencias a través del motor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library é um software pessoal disponibilizado sob a licença MIT. nxnative, Ryujinx, SPIR-V Cross, LibHac e as demais dependências mantêm suas próprias licenças por meio da engine." + } + } + } + }, + "NX Library is up to date": { + "comment": "SettingsAboutSection.swift:223", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library is up to date" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library è aggiornata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library est à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library ist auf dem neuesten Stand" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library está al día" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library está atualizado" + } + } + } + }, + "NX Library restarts to finish.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library restarts to finish." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library si riavvia per completare l’operazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library redémarre pour terminer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library wird zum Abschließen neu gestartet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library se reinicia para finalizar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library será reiniciado para concluir." + } + } + } + }, + "NX Library uses symlinks to your game ROM files to build the macOS native binary. Rebuilding uses your current engine pin and settings, and preserves your game saves. ": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library uses symlinks to your game ROM files to build the macOS native binary. Rebuilding uses your current engine pin and settings, and preserves your game saves. " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library usa collegamenti simbolici ai file ROM dei tuoi giochi per compilare il binario nativo per macOS. La ricompilazione usa la versione del motore attualmente fissata e le impostazioni correnti, e conserva i salvataggi. " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library utilise des liens symboliques vers vos fichiers ROM de jeu pour compiler le binaire natif macOS. La recompilation utilise votre version de moteur épinglée et vos réglages actuels, et préserve vos sauvegardes de jeu. " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library verwendet symbolische Links zu deinen Spiel-ROM-Dateien, um die native macOS-App zu erstellen. Beim erneuten Erstellen werden die aktuell festgelegte Engine-Version und deine Einstellungen verwendet. Deine Spielstände bleiben erhalten. " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library usa enlaces simbólicos a los archivos ROM de tus juegos para compilar el binario nativo de macOS. Al recompilar, se usan la versión fijada del motor y los ajustes actuales, y se conservan las partidas guardadas. " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library usa links simbólicos para os arquivos ROM dos seus jogos para compilar o binário nativo para macOS. A recompilação usa a versão fixada atual da engine e seus ajustes, preservando os dados salvos do jogo. " + } + } + } + }, + "NX Library will reopen automatically.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library will reopen automatically." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "NX Library si riaprirà automaticamente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "NX Library se rouvrira automatiquement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library wird automatisch wieder geöffnet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "NX Library se volverá a abrir automáticamente." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O NX Library será reaberto automaticamente." + } + } + } + }, + "NX Library's copy of %@ goes to the Trash, so you can bring it back from there. Your original file stays where it is.": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "NX Library's copy of %@ goes to the Trash, so you can bring it back from there. Your original file stays where it is." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La copia di %@ di NX Library viene spostata nel Cestino: puoi ripristinarla da lì. Il file originale resta dov’è." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La copie de %@ utilisée par NX Library est placée dans la corbeille ; vous pourrez l’y récupérer. Votre fichier d’origine reste à son emplacement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die NX Library-Kopie von %@ wird in den Papierkorb gelegt. Du kannst sie von dort zurückholen. Deine Originaldatei bleibt an ihrem Platz." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La copia de %@ de NX Library se mueve a la Papelera para que puedas recuperarla. Tu archivo original permanece en su ubicación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A cópia de %@ do NX Library vai para o Lixo, de onde você poderá restaurá-la. Seu arquivo original permanece onde está." + } + } + } + }, + "Name": { + "comment": "AddGameView+Rows.swift:156", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Name" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nome" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nom" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Name" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nombre" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nome" + } + } + } + }, + "Navigate NX Library.": { + "comment": "ControlsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Navigate NX Library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Naviga in NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Naviguer dans NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In NX Library navigieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Navega por NX Library." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Navegue pelo NX Library." + } + } + } + }, + "Navigation": { + "comment": "SettingsSection.swift:16", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Navigation" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Navigazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Navigation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Navigation" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Navegación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Navegação" + } + } + } + }, + "Needed for NSP/XCI packages. Extracted folders do not use keys.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Needed for NSP/XCI packages. Extracted folders do not use keys." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Necessario per i pacchetti NSP/XCI. Le cartelle estratte non usano chiavi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nécessaire pour les paquets NSP/XCI. Les dossiers extraits n’utilisent pas de clés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für NSP/XCI-Pakete erforderlich. Entpackte Ordner benötigen keine Schlüssel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Necesario para paquetes NSP/XCI. Las carpetas extraídas no usan claves." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Necessário para pacotes NSP/XCI. Pastas extraídas não usam chaves." + } + } + } + }, + "Never": { + "comment": "LibraryPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Never" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mai" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jamais" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nie" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nunca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nunca" + } + } + } + }, + "New": { + "comment": "SettingsEngineSection.swift:365", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveau" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nuevo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novo" + } + } + } + }, + "New games": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New games" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovi giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaux jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos nuevos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novos jogos" + } + } + } + }, + "New games: Metal performance overlay.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New games: Metal performance overlay." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovi giochi: prestazioni Metal in sovrimpressione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaux jeux : surimpression des performances Metal." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Spiele: Metal-Leistungsanzeige." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos nuevos: panel de rendimiento de Metal superpuesto." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novos jogos: sobreposição de desempenho do Metal." + } + } + } + }, + "New games: MetalFX enlarges the finished picture to your screen.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New games: MetalFX enlarges the finished picture to your screen." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovi giochi: MetalFX ingrandisce l’immagine finale fino alle dimensioni dello schermo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaux jeux : MetalFX agrandit l’image finale à la taille de votre écran." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Spiele: MetalFX vergrößert das fertige Bild auf deinen Bildschirm." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos nuevos: MetalFX amplía la imagen final a tu pantalla." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novos jogos: MetalFX amplia a imagem final para sua tela." + } + } + } + }, + "New games: console profile or handheld mode.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New games: console profile or handheld mode." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovi giochi: profilo della console o modalità portatile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaux jeux : profil de console ou mode portable." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Spiele: Konsolenprofil oder Handheld-Modus." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos nuevos: perfil de consola o modo portátil." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novos jogos: perfil do console ou modo portátil." + } + } + } + }, + "New games: frame-rate target of 30 or 60 FPS.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New games: frame-rate target of 30 or 60 FPS." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovi giochi: frequenza dei fotogrammi desiderata di 30 o 60 FPS." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaux jeux : fréquence cible de 30 ou 60 FPS." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Spiele: Zielbildrate von 30 oder 60 FPS." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos nuevos: objetivo de 30 o 60 FPS." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novos jogos: taxa de quadros desejada de 30 ou 60 FPS." + } + } + } + }, + "New games: internal 3D resolution, from 50% to 100% when supported.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New games: internal 3D resolution, from 50% to 100% when supported." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovi giochi: risoluzione 3D interna, dal 50% al 100% quando supportata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaux jeux : résolution 3D interne, de 50 % à 100 % si prise en charge." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Spiele: interne 3D-Auflösung von 50% bis 100%, sofern unterstützt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos nuevos: resolución 3D interna, del 50% al 100% si es compatible." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novos jogos: resolução 3D interna, de 50% a 100% quando compatível." + } + } + } + }, + "New games: start in fullscreen or a window.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New games: start in fullscreen or a window." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovi giochi: avvio a schermo intero o in finestra." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaux jeux : démarrer en plein écran ou dans une fenêtre." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Spiele: im Vollbild oder Fenster starten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos nuevos: iniciar en pantalla completa o en una ventana." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novos jogos: iniciar em tela cheia ou em uma janela." + } + } + } + }, + "New games: starting language; Game Default uses the build's language.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New games: starting language; Game Default uses the build's language." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nuovi giochi: lingua all’avvio; Predefinita del gioco usa la lingua della compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nouveaux jeux : langue au démarrage ; Par défaut du jeu utilise la langue de la compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Spiele: Sprache beim Start; „Spielstandard“ verwendet die Sprache des Builds." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Juegos nuevos: idioma inicial; Predeterminado del juego usa el idioma de la compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Novos jogos: idioma inicial; Padrão do jogo usa o idioma da compilação." + } + } + } + }, + "Newer Metal": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Newer Metal" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metal più recente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Metal plus récent" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueres Metal" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Metal más reciente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Metal mais recente" + } + } + } + }, + "Newer Metal · override on": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Newer Metal · override on" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metal più recente · controllo ignorato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Metal plus récent · dérogation activée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueres Metal · Ausnahme aktiv" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Metal más reciente · excepción activada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Metal mais recente · exceção ativada" + } + } + } + }, + "Newer Metal: turn on the override in Settings → Engine": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Newer Metal: turn on the override in Settings → Engine" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metal più recente: ignora il controllo in Impostazioni → Motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Metal plus récent : activez la dérogation dans Réglages → Moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueres Metal: Ausnahme unter Einstellungen → Engine aktivieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Metal más reciente: activa la excepción en Ajustes → Motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Metal mais recente: ative a exceção em Ajustes → Engine" + } + } + } + }, + "Newest": { + "comment": "ModCatalog.swift:62", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Newest" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Più recenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les plus récents" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueste" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Más recientes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mais recentes" + } + } + } + }, + "Newest engine": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Newest engine" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore più recente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueste Engine" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor más reciente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine mais recente" + } + } + } + }, + "Newest engine: %@": { + "comment": "AppViewModel+NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Newest engine: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore più recente: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dernier moteur : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueste Engine: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor más reciente: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine mais recente: %@" + } + } + } + }, + "Newest nxnative, not yet tested by NX Library. It usually works, but something may not. ": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Newest nxnative, not yet tested by NX Library. It usually works, but something may not. " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La versione più recente di nxnative, non ancora testata da NX Library. Di solito funziona, ma qualcosa potrebbe non funzionare. " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dernier nxnative, pas encore testé par NX Library. Il fonctionne généralement, mais des problèmes restent possibles. " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neuestes nxnative, noch nicht von NX Library getestet. Es funktioniert in der Regel, aber manches möglicherweise nicht. " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El nxnative más reciente, aún no probado por NX Library. Suele funcionar, pero algo podría fallar. " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "nxnative mais recente, ainda não testado pelo NX Library. Geralmente funciona, mas algo pode falhar. " + } + } + } + }, + "Next": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Next" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Successivo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Suivant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weiter" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Siguiente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Próximo" + } + } + } + }, + "Next %@": { + "comment": "SeleneSettingsRow.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Next %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ successivo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choix suivant pour %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nächster Wert für %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ siguiente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Próximo: %@" + } + } + } + }, + "Next launch": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Next launch" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prossimo avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prochain lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nächster Start" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Próximo inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Próxima abertura" + } + } + } + }, + "Night": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Night" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Notte" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nuit" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nacht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Noche" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Noite" + } + } + } + }, + "Nintendo Store data": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store data" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dati del Nintendo Store" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données du Nintendo Store" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store-Daten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Datos de Nintendo Store" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dados da Nintendo Store" + } + } + } + }, + "Nintendo Store descriptions and links.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store descriptions and links." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Descrizioni e link del Nintendo Store." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Descriptions et liens du Nintendo Store." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beschreibungen und Links aus dem Nintendo Store." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descripciones y enlaces de Nintendo Store." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descrições e links da Nintendo Store." + } + } + } + }, + "Nintendo Store descriptions, genres and links.": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store descriptions, genres and links." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Descrizioni, generi e link del Nintendo Store." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Descriptions, genres et liens du Nintendo Store." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beschreibungen, Genres und Links aus dem Nintendo Store." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descripciones, géneros y enlaces de Nintendo Store." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Descrições, gêneros e links da Nintendo Store." + } + } + } + }, + "Nintendo Store returned a response this app could not read.": { + "comment": "NintendoStoreMetadata.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store returned a response this app could not read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il Nintendo Store ha restituito una risposta non leggibile da questa app." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le Nintendo Store a renvoyé une réponse illisible par cette app." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store hat eine Antwort zurückgegeben, die diese App nicht lesen konnte." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store ha devuelto una respuesta que esta app no ha podido leer." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A Nintendo Store retornou uma resposta que este app não conseguiu ler." + } + } + } + }, + "Nintendo Store returned status %@.": { + "comment": "NintendoStoreMetadata.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store returned status %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il Nintendo Store ha restituito lo stato %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le Nintendo Store a renvoyé le statut %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store hat Status %@ zurückgegeben." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store ha devuelto el estado %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A Nintendo Store retornou o status %@." + } + } + } + }, + "Nintendo Switch cryptographic keys required to decrypt game packages (NSP/XCI). NX Library stores a private copy with mode 0600 permissions; key values are never displayed or logged.": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Switch cryptographic keys required to decrypt game packages (NSP/XCI). NX Library stores a private copy with mode 0600 permissions; key values are never displayed or logged." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiavi crittografiche Nintendo Switch necessarie per decrittare i pacchetti di gioco (NSP/XCI). NX Library ne conserva una copia privata con permessi 0600; i valori delle chiavi non vengono mai mostrati né registrati nei log." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clés cryptographiques Nintendo Switch nécessaires au déchiffrement des paquets de jeu (NSP/XCI). NX Library en conserve une copie privée avec des permissions 0600 ; les valeurs des clés ne sont jamais affichées ni inscrites dans les journaux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kryptografische Nintendo Switch-Schlüssel zum Entschlüsseln von Spielpaketen (NSP/XCI). NX Library speichert eine private Kopie mit Zugriffsrechten 0600; Schlüsselwerte werden nie angezeigt oder protokolliert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Claves criptográficas de Nintendo Switch necesarias para descifrar paquetes de juegos (NSP/XCI). NX Library guarda una copia privada con permisos 0600; los valores de las claves nunca se muestran ni se escriben en los registros." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chaves criptográficas do Nintendo Switch necessárias para descriptografar pacotes de jogos (NSP/XCI). O NX Library armazena uma cópia privada com permissões 0600; os valores das chaves nunca são exibidos nem registrados em logs." + } + } + } + }, + "Nintendo store": { + "comment": "ChangeMetadataView.swift:46", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nintendo store" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nintendo store" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store" + } + } + } + }, + "Nintendo store: %@": { + "comment": "ChangeMetadataView.swift:175", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nintendo store: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nintendo store: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store: %@" + } + } + } + }, + "No GameBanana page found": { + "comment": "GameBananaBandModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No GameBanana page found" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna pagina GameBanana trovata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune page GameBanana trouvée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine GameBanana-Seite gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha encontrado ninguna página en GameBanana" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma página encontrada no GameBanana" + } + } + } + }, + "No Nintendo Store entry was found for this game.": { + "comment": "NintendoStoreMetadata.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No Nintendo Store entry was found for this game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna voce del Nintendo Store trovata per questo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune entrée Nintendo Store trouvée pour ce jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für dieses Spiel wurde kein Nintendo Store-Eintrag gefunden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha encontrado ninguna entrada de Nintendo Store para este juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma entrada da Nintendo Store foi encontrada para este jogo." + } + } + } + }, + "No backups": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No backups" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun backup" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune copie de sauvegarde" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Backups" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin copias de seguridad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum backup" + } + } + } + }, + "No base game in this NUP": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No base game in this NUP" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco base in questo NUP" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu de base dans ce NUP" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Basisspiel in dieser NUP-Datei" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay juego base en este NUP" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo base neste NUP" + } + } + } + }, + "No base game in this folder": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No base game in this folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco base in questa cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu de base dans ce dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Basisspiel in diesem Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay juego base en esta carpeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo base nesta pasta" + } + } + } + }, + "No build caches were deleted.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No build caches were deleted." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna cache di compilazione è stata eliminata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun cache de compilation n’a été supprimé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Es wurden keine Build-Caches gelöscht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha eliminado ninguna caché de compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum cache de compilação foi apagado." + } + } + } + }, + "No build is active": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No build is active" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna compilazione in corso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune compilation en cours" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Build aktiv" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay ninguna compilación activa" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma compilação em andamento" + } + } + } + }, + "No connection. Mods you installed still work.": { + "comment": "GameBananaBandModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No connection. Mods you installed still work." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna connessione. Le mod installate funzionano comunque." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune connexion. Les mods installés fonctionnent toujours." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Verbindung. Deine installierten Mods funktionieren weiterhin." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin conexión. Los mods instalados siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sem conexão. Os mods instalados continuam funcionando." + } + } + } + }, + "No description for this game yet.": { + "comment": "SpotlightPageSections.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No description for this game yet." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non c’è ancora una descrizione per questo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune description pour ce jeu pour le moment." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für dieses Spiel gibt es noch keine Beschreibung." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay descripción para este juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não há descrição para este jogo." + } + } + } + }, + "No favorites yet": { + "comment": "SpotlightRowView.swift:194", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No favorites yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ancora nessun preferito" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun favori pour le moment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch keine Favoriten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay favoritos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não há favoritos" + } + } + } + }, + "No game found in the files": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No game found in the files" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco trovato nei file" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu trouvé dans les fichiers" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Spiel in den Dateien gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha encontrado ningún juego en los archivos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo encontrado nos arquivos" + } + } + } + }, + "No games found. Try another name.": { + "comment": "GameBananaFindPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No games found. Try another name." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco trovato. Prova un altro nome." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu trouvé. Essayez un autre nom." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Spiele gefunden. Versuche einen anderen Namen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han encontrado juegos. Prueba con otro nombre." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo encontrado. Tente outro nome." + } + } + } + }, + "No games in %@": { + "comment": "CollectionView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No games in %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco in %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu dans %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Spiele in %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay juegos en %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo em %@" + } + } + } + }, + "No games match.": { + "comment": "FindGameView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No games match." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco corrispondente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu correspondant." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine passenden Spiele." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ningún juego coincide." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo corresponde à busca." + } + } + } + }, + "No games were found in this folder.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No games were found in this folder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco trovato in questa cartella." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu trouvé dans ce dossier." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In diesem Ordner wurden keine Spiele gefunden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han encontrado juegos en esta carpeta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo foi encontrado nesta pasta." + } + } + } + }, + "No games yet": { + "comment": "SpotlightRowView.swift:196", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No games yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ancora nessun gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu pour le moment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch keine Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não há jogos" + } + } + } + }, + "No hidden games. Hide one from its Game options; it comes back here.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No hidden games. Hide one from its Game options; it comes back here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco nascosto. Puoi nasconderne uno dalle sue Opzioni del gioco; lo ritroverai qui." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu masqué. Masquez-en un depuis ses Options du jeu ; il réapparaîtra ici." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine ausgeblendeten Spiele. Blende ein Spiel in seinen Spieloptionen aus; hier kannst du es wieder einblenden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay juegos ocultos. Oculta uno desde sus opciones; aparecerá aquí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo oculto. Oculte um jogo em suas Opções do jogo; ele aparecerá aqui." + } + } + } + }, + "No icon available. You can choose one.": { + "comment": "AppIconProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No icon available. You can choose one." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna icona disponibile. Puoi sceglierne una." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune icône disponible. Vous pouvez en choisir une." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Symbol verfügbar. Du kannst eines auswählen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay ningún icono disponible. Puedes elegir uno." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum ícone disponível. Você pode escolher um." + } + } + } + }, + "No images found": { + "comment": "ChangeArtworkView.swift:273", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No images found" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna immagine trovata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune image trouvée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Bilder gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han encontrado imágenes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma imagem encontrada" + } + } + } + }, + "No lines match": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No lines match" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna riga corrispondente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune ligne correspondante" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine passenden Zeilen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ninguna línea coincide" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma linha corresponde" + } + } + } + }, + "No log path is known for %@.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No log path is known for %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun percorso del log noto per %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun chemin de journal connu pour %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für %@ ist kein Protokollpfad bekannt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se conoce la ruta del registro de %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não há caminho de log conhecido para %@." + } + } + } + }, + "No log yet for %@; opened the log folder.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No log yet for %@; opened the log folder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun log ancora disponibile per %@; aperta la cartella dei log." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun journal pour %@ pour le moment ; le dossier des journaux a été ouvert." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für %@ gibt es noch kein Protokoll; der Protokollordner wurde geöffnet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay registro de %@; se ha abierto la carpeta de registros." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não há log para %@; a pasta de logs foi aberta." + } + } + } + }, + "No logs were deleted.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No logs were deleted." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun log è stato eliminato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun journal n’a été supprimé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Es wurden keine Protokolle gelöscht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha eliminado ningún registro." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum log foi apagado." + } + } + } + }, + "No matching regions.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No matching regions." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna zona corrispondente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune région correspondante." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine passenden Regionen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ninguna región coincide." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma região corresponde à busca." + } + } + } + }, + "No mods for this game work with NX Library yet": { + "comment": "GameBananaBandModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No mods for this game work with NX Library yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Per questo gioco non ci sono ancora mod compatibili con NX Library" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun mod de ce jeu n’est encore compatible avec NX Library" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für dieses Spiel gibt es noch keine Mods, die mit NX Library funktionieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay mods de este juego compatibles con NX Library" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não há mods deste jogo compatíveis com o NX Library" + } + } + } + }, + "No mods for your games work with NX Library yet.": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No mods for your games work with NX Library yet." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Per i tuoi giochi non ci sono ancora mod compatibili con NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun mod pour vos jeux n’est encore compatible avec NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für deine Spiele gibt es noch keine Mods, die mit NX Library funktionieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay mods para tus juegos compatibles con NX Library." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não há mods para seus jogos compatíveis com o NX Library." + } + } + } + }, + "No mods in this game's folder": { + "comment": "ModsPanel.swift:387", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No mods in this game's folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna mod nella cartella di questo gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun mod dans le dossier de ce jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Mods im Ordner dieses Spiels" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay mods en la carpeta de este juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum mod na pasta deste jogo" + } + } + } + }, + "No mods yet": { + "comment": "ModsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No mods yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ancora nessuna mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun mod pour le moment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch keine Mods" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay mods" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não há mods" + } + } + } + }, + "No prod.keys imported": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No prod.keys imported" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun prod.keys importato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun fichier prod.keys importé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine prod.keys importiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "prod.keys sin importar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum prod.keys importado" + } + } + } + }, + "No readable title ID in %@.": { + "comment": "SourceCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No readable title ID in %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun title ID leggibile in %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun title ID lisible dans %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine lesbare title ID in %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay un title ID legible en %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum title ID legível em %@." + } + } + } + }, + "No screenshots for this game": { + "comment": "SpotlightScreenshotStrip.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No screenshots for this game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna schermata per questo gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune capture d’écran pour ce jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine Screenshots für dieses Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay capturas de este juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma captura de tela deste jogo" + } + } + } + }, + "No settings match.": { + "comment": "SettingsSearchSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No settings match." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna impostazione corrispondente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun réglage correspondant." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine passenden Einstellungen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ningún ajuste coincide." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum ajuste corresponde à busca." + } + } + } + }, + "No wallpaper": { + "comment": "ThemePickerView.swift:734", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "No wallpaper" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuno sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun fond d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Hintergrundbild" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin fondo de pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sem papel de parede" + } + } + } + }, + "None": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "None" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuno" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ninguno" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum" + } + } + } + }, + "None chosen yet.": { + "comment": "SettingsGamesSection.swift:41", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "None chosen yet." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuno ancora scelto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun choix pour le moment." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts ausgewählt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no se ha elegido ninguna." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhuma escolhida ainda." + } + } + } + }, + "None of the files holds a game the engine can read.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "None of the files holds a game the engine can read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuno dei file contiene un gioco leggibile dal motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun des fichiers ne contient de jeu lisible par le moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keine der Dateien enthält ein Spiel, das die Engine lesen kann." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ningún archivo contiene un juego que el motor pueda leer." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum arquivo contém um jogo que a engine consiga ler." + } + } + } + }, + "None of these": { + "comment": "GameBananaFindPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "None of these" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuno di questi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun de ces jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Keines davon" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ninguno de estos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum destes" + } + } + } + }, + "None yet": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "None yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ancora nessuno" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun pour le moment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch keine" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún ninguno" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum por enquanto" + } + } + } + }, + "Not a mod": { + "comment": "DetailsBands.swift:239", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not a mod" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è una mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce n’est pas un mod" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Mod" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No es un mod" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não é um mod" + } + } + } + }, + "Not a mod: needs a romfs folder.": { + "comment": "ModsPanel.swift:396", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not a mod: needs a romfs folder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è una mod: richiede una cartella romfs." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce n’est pas un mod : un dossier romfs est requis." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Mod: Ein romfs-Ordner ist erforderlich." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No es un mod: necesita una carpeta romfs." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não é um mod: precisa de uma pasta romfs." + } + } + } + }, + "Not a mod: unzip it into its own folder first.": { + "comment": "ModsPanel.swift:397", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not a mod: unzip it into its own folder first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è una mod: prima estraila in una cartella dedicata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce n’est pas un mod : décompressez-le d’abord dans son propre dossier." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kein Mod: Entpacke ihn zuerst in einen eigenen Ordner." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No es un mod: primero descomprímelo en su propia carpeta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não é um mod: primeiro descompacte-o em sua própria pasta." + } + } + } + }, + "Not available yet.": { + "comment": "AddGameView+Rows.swift:179", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not available yet." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non ancora disponibile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas encore disponible." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nicht verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no disponible." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não disponível." + } + } + } + }, + "Not built yet": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not built yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "App non ancora creata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas encore compilé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nicht als App erstellt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún sin compilar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não compilado" + } + } + } + }, + "Not checked": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not checked" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non verificato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non vérifié" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht geprüft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin comprobar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não verificado" + } + } + } + }, + "Not connected": { + "comment": "SettingsControllersSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not connected" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non connesso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non connectée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht verbunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin conectar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não conectado" + } + } + } + }, + "Not enough disk space": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not enough disk space" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spazio su disco insufficiente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Espace disque insuffisant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht genügend Speicherplatz" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Espacio en disco insuficiente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Espaço em disco insuficiente" + } + } + } + }, + "Not enough free space at %@: %@ available, %@ needed.%@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not enough free space at %@: %@ available, %@ needed.%@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spazio libero insufficiente in %@: %@ disponibili, %@ necessari.%@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Espace libre insuffisant à %@ : %@ disponibles, %@ nécessaires.%@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht genügend freier Speicherplatz auf %@: %@ verfügbar, %@ erforderlich.%@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay suficiente espacio libre en %@: %@ disponibles, %@ necesarios.%@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Espaço livre insuficiente em %@: %@ disponíveis, %@ necessários.%@" + } + } + } + }, + "Not found": { + "comment": "SettingsStorageSection.swift:156", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not found" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non trovato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Introuvable" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No encontrado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não encontrado" + } + } + } + }, + "Not needed: the current toolchain is certified.": { + "comment": "SettingsEngineSection.swift:231", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not needed: the current toolchain is certified." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non necessario: la toolchain attuale è certificata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inutile : la chaîne d’outils actuelle est certifiée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht erforderlich: Die aktuelle Toolchain ist zertifiziert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No es necesario: la cadena de herramientas actual está certificada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não é necessário: o conjunto de ferramentas atual é certificado." + } + } + } + }, + "Not now": { + "comment": "WallpaperPlacement.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not now" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non ora" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas maintenant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Jetzt nicht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ahora no" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Agora não" + } + } + } + }, + "Not played yet": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not played yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mai giocati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas encore joués" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nicht gespielt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin jugar aún" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não jogados" + } + } + } + }, + "Not selected": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not selected" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non selezionato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non sélectionné" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht ausgewählt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin seleccionar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não selecionado" + } + } + } + }, + "Not selected · Optional": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not selected · Optional" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non selezionata · Facoltativa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non sélectionné · Facultatif" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht ausgewählt · Optional" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sin seleccionar · Opcional" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não selecionado · Opcional" + } + } + } + }, + "Not supported: %@.": { + "comment": "CheatsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not supported: %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non supportato: %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Non pris en charge : %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht unterstützt: %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No compatible: %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não compatível: %@." + } + } + } + }, + "Not tested with this engine yet": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Not tested with this engine yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non ancora testato con questo motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Pas encore testé avec ce moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nicht mit dieser Engine getestet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no se ha probado con este motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não testado com esta engine" + } + } + } + }, + "Nothing changed yet.": { + "comment": "SettingsSearchSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing changed yet." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ancora nessuna modifica." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune modification pour le moment." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nichts geändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no se ha cambiado nada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nada foi alterado ainda." + } + } + } + }, + "Nothing from the store yet.": { + "comment": "ChangeMetadataView.swift:175", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing from the store yet." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ancora nessun contenuto dallo store." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucune donnée de la boutique pour le moment." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch keine Daten aus dem Shop." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay nada de la tienda." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda não há dados da loja." + } + } + } + }, + "Nothing installed": { + "comment": "SpotlightRowView.swift:192", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing installed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessun gioco installato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu installé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nichts installiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay nada instalado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nada instalado" + } + } + } + }, + "Nothing played lately": { + "comment": "SpotlightRowView.swift:193", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing played lately" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nessuna partita recente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Aucun jeu joué récemment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In letzter Zeit nichts gespielt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No has jugado últimamente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nenhum jogo jogado recentemente" + } + } + } + }, + "Nothing to fix here: this title cannot run.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing to fix here: this title cannot run." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non c’è nulla da correggere qui: questo titolo non può essere eseguito." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien à corriger ici : ce titre ne peut pas être exécuté." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hier lässt sich nichts beheben: Dieser Titel kann nicht ausgeführt werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aquí no hay nada que corregir: este título no puede ejecutarse." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não há o que corrigir aqui: este jogo não pode ser executado." + } + } + } + }, + "Nothing to install.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing to install." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nulla da installare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien à installer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nichts zu installieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay nada que instalar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nada para instalar." + } + } + } + }, + "Nothing yet": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Nothing yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ancora nessun contenuto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rien pour le moment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch keine Einträge" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no hay nada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Nada por enquanto" + } + } + } + }, + "Notify when a porting ends": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Notify when a porting ends" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Notifica al termine della creazione dell’app per Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Notifier à la fin de la création d’une app Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bei Ende der App-Erstellung benachrichtigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Avisar al terminar una compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Notificar ao terminar uma compilação" + } + } + } + }, + "OLDER": { + "comment": "SettingsEngineSection.swift:489", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "OLDER" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "PRECEDENTE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PLUS ANCIEN" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ÄLTER" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ANTERIORES" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "MAIS ANTIGO" + } + } + } + }, + "ONLINE · %@ · %@": { + "comment": "OnlineBrowsePanel.swift:147", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "ONLINE · %@ · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "ONLINE · %@ · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "EN LIGNE · %@ · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ONLINE · %@ · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "EN LÍNEA · %@ · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ON-LINE · %@ · %@" + } + } + } + }, + "Off": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Off" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattivo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactivado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desativado" + } + } + } + }, + "Off keeps every cheat from running, whatever is switched on.": { + "comment": "CheatsPanel.swift:284", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Off keeps every cheat from running, whatever is switched on." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Se disattivato, impedisce l’esecuzione di tutti i trucchi, anche di quelli attivi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivé empêche l’exécution de tous les codes de triche, quels que soient ceux activés individuellement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Aus“ verhindert die Ausführung aller Cheats, unabhängig davon, welche einzeln aktiviert sind." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactivado impide ejecutar cualquier truco, aunque esté activado individualmente." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desativado impede a execução de todas as trapaças, independentemente das que estiverem ativadas." + } + } + } + }, + "Older Metal · override on": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Older Metal · override on" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metal precedente · controllo ignorato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Metal plus ancien · dérogation activée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Älteres Metal · Ausnahme aktiv" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Metal anterior · excepción activada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Metal mais antigo · exceção ativada" + } + } + } + }, + "Older Metal: update it, or use the override in Settings → Engine": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Older Metal: update it, or use the override in Settings → Engine" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Metal precedente: aggiornalo oppure ignora il controllo in Impostazioni → Motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Metal plus ancien : mettez-le à jour ou utilisez la dérogation dans Réglages → Moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Älteres Metal: aktualisieren oder Ausnahme unter Einstellungen → Engine verwenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Metal anterior: actualízalo o usa la excepción en Ajustes → Motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Metal mais antigo: atualize ou use a exceção em Ajustes → Engine" + } + } + } + }, + "On": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "On" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attivo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Activado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ativado" + } + } + } + }, + "On shows the selected game's art in Spotlight and Collection; Off keeps your wallpaper. Details keeps the game's art.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "On shows the selected game's art in Spotlight and Collection; Off keeps your wallpaper. Details keeps the game's art." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Se attivo, mostra le immagini del gioco selezionato in Spotlight e Raccolta; se disattivo, mantiene il tuo sfondo. Dettagli mantiene le immagini del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activé affiche l’illustration du jeu sélectionné dans À la une et Collection ; Désactivé conserve votre fond d’écran. Détails conserve l’illustration du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Ein“ zeigt das Artwork des ausgewählten Spiels in Spotlight und Sammlung; „Aus“ behält dein Hintergrundbild. Details behält das Artwork des Spiels." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Activado muestra las imágenes del juego seleccionado en Destacados y Colección; Desactivado conserva tu fondo. Detalles conserva las imágenes del juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ativado mostra a arte do jogo selecionado em Destaques e Coleção; Desativado mantém seu papel de parede. Detalhes mantém a arte do jogo." + } + } + } + }, + "On shows the selected game's artwork; Off keeps your wallpaper. Details and the launch screen keep their own artwork settings.": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "On shows the selected game's artwork; Off keeps your wallpaper. Details and the launch screen keep their own artwork settings." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Se attivo, mostra le immagini del gioco selezionato; se disattivo, mantiene il tuo sfondo. Dettagli e schermata di avvio conservano le proprie impostazioni per le immagini." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activé affiche l’illustration du jeu sélectionné ; Désactivé conserve votre fond d’écran. Détails et l’écran de lancement gardent leurs propres réglages d’illustration." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Ein“ zeigt das Artwork des ausgewählten Spiels; „Aus“ behält dein Hintergrundbild. Details und Startbildschirm behalten ihre eigenen Artwork-Einstellungen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Al activarlo, muestra las imágenes del juego seleccionado; al desactivarlo, conserva tu fondo de pantalla. Detalles y la pantalla de inicio mantienen sus propios ajustes de imágenes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ativado mostra a arte do jogo selecionado; Desativado mantém seu papel de parede. Detalhes e a tela de abertura mantêm seus próprios ajustes de arte." + } + } + } + }, + "On targets 60 FPS; Off targets 30 FPS. Some games run too fast at 60 FPS.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "On targets 60 FPS; Off targets 30 FPS. Some games run too fast at 60 FPS." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Se attivo, punta a 60 FPS; se disattivo, a 30 FPS. Alcuni giochi girano troppo velocemente a 60 FPS." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activé vise 60 FPS ; Désactivé vise 30 FPS. Certains jeux tournent trop vite à 60 FPS." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Ein“ strebt 60 FPS an; „Aus“ strebt 30 FPS an. Manche Spiele laufen mit 60 FPS zu schnell." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Activado fija un objetivo de 60 FPS; Desactivado, de 30 FPS. Algunos juegos van demasiado rápido a 60 FPS." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ativado visa 60 FPS; Desativado visa 30 FPS. Alguns jogos ficam rápidos demais a 60 FPS." + } + } + } + }, + "On while you use the newest engine.": { + "comment": "SettingsEngineSection.swift:226", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "On while you use the newest engine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attivo mentre usi il motore più recente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activé tant que vous utilisez le dernier moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktiv, solange du die neueste Engine verwendest." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Activado mientras usas el motor más reciente." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ativado enquanto você usa a engine mais recente." + } + } + } + }, + "On your Favorites tab.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "On your Favorites tab." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nella scheda Preferiti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dans votre onglet Favoris." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf deinem Favoriten-Tab." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "En tu pestaña Favoritos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Na sua aba Favoritos." + } + } + } + }, + "One sync choice for all games, applied at next launch: Off, iCloud Drive or a folder you choose.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "One sync choice for all games, applied at next launch: Off, iCloud Drive or a folder you choose." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un’unica scelta di sincronizzazione per tutti i giochi, applicata al prossimo avvio: Disattivo, iCloud Drive o una cartella a tua scelta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Un choix de synchronisation pour tous les jeux, appliqué au prochain lancement : Désactivé, iCloud Drive ou un dossier de votre choix." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eine Synchronisierungswahl für alle Spiele, angewendet beim nächsten Spielstart: Aus, iCloud Drive oder ein selbst gewählter Ordner." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Una opción de sincronización para todos los juegos, aplicada en el próximo inicio: Desactivado, iCloud Drive o una carpeta que elijas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Uma opção de sincronização para todos os jogos, aplicada na próxima abertura: Desativado, iCloud Drive ou uma pasta à sua escolha." + } + } + } + }, + "One update at a time; bundled DLC stay together.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "One update at a time; bundled DLC stay together." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Un aggiornamento alla volta; i DLC inclusi restano insieme." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une seule mise à jour à la fois ; les DLC groupés restent ensemble." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur ein Update gleichzeitig; gebündelte DLC bleiben zusammen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Una actualización a la vez; los DLC incluidos se mantienen juntos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Uma atualização por vez; os DLCs incluídos ficam juntos." + } + } + } + }, + "Only here": { + "comment": "ChangeArtworkView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Only here" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Solo qui" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Uniquement ici" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur hier" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo aquí" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Só aqui" + } + } + } + }, + "Only here changes the image inside NX Library. Use on the Mac also shows it in Finder and the Dock. Automatic uses the game's own icon, then an online image if available. Changing the image keeps this location choice.": { + "comment": "ChangeArtworkView.swift:192", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Only here changes the image inside NX Library. Use on the Mac also shows it in Finder and the Dock. Automatic uses the game's own icon, then an online image if available. Changing the image keeps this location choice." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Solo qui cambia l’immagine all’interno di NX Library. Usa sul Mac la mostra anche nel Finder e nel Dock. Automatico usa l’icona del gioco, poi un’immagine online se disponibile. Cambiare immagine mantiene questa scelta di posizione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Uniquement ici modifie l’image dans NX Library. Utiliser sur le Mac l’affiche aussi dans le Finder et le Dock. Automatique utilise l’icône du jeu, puis une image en ligne si disponible. Changer l’image conserve ce choix d’emplacement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "„Nur hier“ ändert das Bild innerhalb von NX Library. „Auf dem Mac verwenden“ zeigt es auch im Finder und im Dock. „Automatisch“ verwendet das spieleigene Symbol und danach ein Online-Bild, falls verfügbar. Ein Bildwechsel behält diese Ortsauswahl bei." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo aquí cambia la imagen dentro de NX Library. Usar en el Mac también la muestra en el Finder y el Dock. Automático usa el icono propio del juego y, si no está disponible, una imagen en línea. Cambiar la imagen conserva esta elección de ubicación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Só aqui altera a imagem dentro do NX Library. Usar no Mac também a exibe no Finder e no Dock. Automático usa o ícone do próprio jogo e, se não houver, uma imagem on-line disponível. Alterar a imagem mantém esta escolha de local." + } + } + } + }, + "Open": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir" + } + } + } + }, + "Open App Store": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open App Store" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri App Store" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir l’App Store" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App Store öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir App Store" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir App Store" + } + } + } + }, + "Open Engine settings": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Engine settings" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri impostazioni del motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir les réglages du moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine-Einstellungen öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir ajustes del motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir ajustes da engine" + } + } + } + }, + "Open GameTDB": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open GameTDB" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri GameTDB" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir GameTDB" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameTDB öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir GameTDB" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir GameTDB" + } + } + } + }, + "Open GameTDB for box covers.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open GameTDB for box covers." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri GameTDB per le copertine delle confezioni." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir GameTDB pour les jaquettes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameTDB für Spiele-Cover öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre GameTDB para buscar portadas de cajas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra o GameTDB para obter capas de caixas." + } + } + } + }, + "Open GitHub": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open GitHub" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri GitHub" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir GitHub" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GitHub öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir GitHub" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir GitHub" + } + } + } + }, + "Open NX Library in fullscreen. Applies at the next launch.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open NX Library in fullscreen. Applies at the next launch." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri NX Library a schermo intero. Si applica al prossimo avvio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir NX Library en plein écran. S’applique au prochain lancement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "NX Library im Vollbild öffnen. Gilt beim nächsten Start." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre NX Library en pantalla completa. Se aplica en el próximo inicio." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra o NX Library em tela cheia. Aplicado na próxima abertura." + } + } + } + }, + "Open Setup": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Setup" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri configurazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la configuration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einrichtung öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir configuración inicial" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir configuração" + } + } + } + }, + "Open Setup and check the nxnative engine card.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Setup and check the nxnative engine card." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri Configurazione e controlla la scheda Motore nxnative." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez Configuration et vérifiez la carte Moteur nxnative." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffne die Einrichtung und prüfe die Karte nxnative-Engine." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre Configuración inicial y revisa la tarjeta Motor nxnative." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra Configuração e verifique o cartão Engine nxnative." + } + } + } + }, + "Open Setup and install the .NET 10 SDK.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Setup and install the .NET 10 SDK." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri Configurazione e installa l’SDK .NET 10." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez Configuration et installez le SDK .NET 10." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffne die Einrichtung und installiere das .NET 10 SDK." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre Configuración inicial e instala el SDK de .NET 10." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra Configuração e instale o SDK do .NET 10." + } + } + } + }, + "Open Setup and select Xcode.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Setup and select Xcode." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri Configurazione e seleziona Xcode." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez Configuration et sélectionnez Xcode." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffne die Einrichtung und wähle Xcode aus." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre Configuración inicial y selecciona Xcode." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra Configuração e selecione o Xcode." + } + } + } + }, + "Open Storage": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Storage" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri spazio di archiviazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir Stockage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Speicher öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir Almacenamiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir armazenamento" + } + } + } + }, + "Open Xcode": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Xcode" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri Xcode" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir Xcode" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Xcode öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir Xcode" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir Xcode" + } + } + } + }, + "Open Xcode and accept its licence; it then installs its components.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open Xcode and accept its licence; it then installs its components." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri Xcode e accetta la licenza; verranno poi installati i suoi componenti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez Xcode et acceptez sa licence ; il installera ensuite ses composants." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffne Xcode und akzeptiere die Lizenz; anschließend werden die Komponenten installiert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre Xcode y acepta su licencia; después instalará sus componentes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra o Xcode e aceite a licença; ele instalará seus componentes em seguida." + } + } + } + }, + "Open console": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open console" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri console" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la console" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Konsole öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir consola" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir console" + } + } + } + }, + "Open folder": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri cartella" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir le dossier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir carpeta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir pasta" + } + } + } + }, + "Open library": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open library" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothek öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir biblioteca" + } + } + } + }, + "Open log": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open log" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri log" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir le journal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Protokoll öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir registro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir log" + } + } + } + }, + "Open mods folder": { + "comment": "ModsPanel.swift:206", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open mods folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri cartella delle mod" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir le dossier des mods" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mod-Ordner öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir carpeta de mods" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir pasta de mods" + } + } + } + }, + "Open on GameBanana": { + "comment": "ModDetailPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open on GameBanana" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri su GameBanana" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir sur GameBanana" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf GameBanana öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir en GameBanana" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir no GameBanana" + } + } + } + }, + "Open page": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open page" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri pagina" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la page" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Seite öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir página" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir página" + } + } + } + }, + "Open project docs": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open project docs" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri documentazione del progetto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la documentation du projet" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Projektdokumentation öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir documentación del proyecto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir documentação do projeto" + } + } + } + }, + "Open save folder": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open save folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri cartella dei salvataggi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir le dossier des sauvegardes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstandordner öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abrir carpeta de partidas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir pasta de dados salvos" + } + } + } + }, + "Open the Setup check: what NX Library needs and what is ready.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the Setup check: what NX Library needs and what is ready." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri il controllo di configurazione: cosa serve a NX Library e cosa è pronto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la vérification de configuration : ce dont NX Library a besoin et ce qui est prêt." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einrichtungsprüfung öffnen: Was NX Library benötigt und was bereit ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la comprobación inicial: qué necesita NX Library y qué está listo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra a verificação de configuração: o que o NX Library precisa e o que já está pronto." + } + } + } + }, + "Open the commit comparison on GitHub.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the commit comparison on GitHub." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri il confronto dei commit su GitHub." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la comparaison des commits sur GitHub." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Commit-Vergleich auf GitHub öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la comparación de commits en GitHub." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra a comparação de commits no GitHub." + } + } + } + }, + "Open the current save in Finder.": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the current save in Finder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri il salvataggio attuale nel Finder." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la sauvegarde actuelle dans le Finder." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktuellen Spielstand im Finder öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la partida actual en el Finder." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra os dados salvos atuais no Finder." + } + } + } + }, + "Open the folder of verified per-game backups.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the folder of verified per-game backups." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri la cartella dei backup verificati per ogni gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir le dossier des copies de sauvegarde vérifiées de chaque jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner der überprüften Backups pro Spiel öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la carpeta de copias de seguridad verificadas de cada juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra a pasta dos backups verificados de cada jogo." + } + } + } + }, + "Open the game's Nintendo store page.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the game's Nintendo store page." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri la pagina del gioco nel Nintendo Store." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la page Nintendo Store du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nintendo Store-Seite des Spiels öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la página del juego en la tienda de Nintendo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra a página do jogo na loja da Nintendo." + } + } + } + }, + "Open the log, or copy diagnostics for a bug report.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the log, or copy diagnostics for a bug report." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri il log oppure copia i dati diagnostici per segnalare il problema." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez le journal, ou copiez le diagnostic pour signaler un bug." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffne das Protokoll oder kopiere die Diagnosedaten für einen Fehlerbericht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre el registro o copia el diagnóstico para informar de un error." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra o log ou copie o diagnóstico para relatar um erro." + } + } + } + }, + "Open the mods folder and put each mod in its own folder, with a romfs folder inside.": { + "comment": "ModsPanel.swift:395", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the mods folder and put each mod in its own folder, with a romfs folder inside." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri la cartella delle mod e inserisci ogni mod nella propria cartella, con una cartella romfs al suo interno." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrez le dossier des mods et placez chaque mod dans son propre dossier, contenant un dossier romfs." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffne den Mod-Ordner und lege jeden Mod in einem eigenen Ordner mit einem enthaltenen romfs-Ordner ab." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la carpeta de mods y pon cada mod en su propia carpeta, con una carpeta romfs dentro." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra a pasta de mods e coloque cada mod em sua própria pasta, com uma pasta romfs dentro." + } + } + } + }, + "Open the project documentation.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the project documentation." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri la documentazione del progetto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la documentation du projet." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Projektdokumentation öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la documentación del proyecto." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra a documentação do projeto." + } + } + } + }, + "Open the setup checklist.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open the setup checklist." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri l’elenco dei requisiti di configurazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir la liste de vérification de configuration." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einrichtungscheckliste öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre la lista de comprobaciones de configuración." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra a lista de verificação da configuração." + } + } + } + }, + "Open this game's backups in Finder.": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Open this game's backups in Finder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri i backup di questo gioco nel Finder." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir les copies de sauvegarde de ce jeu dans le Finder." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Backups dieses Spiels im Finder öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Abre las copias de seguridad de este juego en el Finder." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abra os backups deste jogo no Finder." + } + } + } + }, + "Opens with the game you had selected last time.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Opens with the game you had selected last time." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Si apre con il gioco selezionato l’ultima volta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ouvrir sur le jeu sélectionné la dernière fois." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Öffnet mit dem zuletzt ausgewählten Spiel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se abre con el juego que seleccionaste la última vez." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abre com o jogo selecionado da última vez." + } + } + } + }, + "Optional: %@ for building.": { + "comment": "GameFoldersCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Optional: %@ for building." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Facoltativo: %@ per compilare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Facultatif : %@ pour compiler." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Optional: %@ zum Erstellen von Apps." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opcional: %@ para compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opcional: %@ para compilar." + } + } + } + }, + "Options": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Options" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Opzioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Options" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Optionen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opciones" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opções" + } + } + } + }, + "Options not saved": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Options not saved" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Opzioni non salvate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Options non enregistrées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Optionen nicht gespeichert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Opciones sin guardar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Opções não salvas" + } + } + } + }, + "Order, folder, Remove": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Order, folder, Remove" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ordine, cartella, Rimuovi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ordre, dossier, Supprimer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Reihenfolge, Ordner, Entfernen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Orden, carpeta, Eliminar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ordem, pasta, remover" + } + } + } + }, + "Other settings this build offers.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Other settings this build offers." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Altre impostazioni offerte da questa compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autres réglages proposés par cette compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weitere Einstellungen, die dieser Build bietet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Otros ajustes que ofrece esta compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Outros ajustes oferecidos por esta compilação." + } + } + } + }, + "Other versions": { + "comment": "CheatsPanel.swift:97", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Other versions" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Altre versioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autres versions" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Andere Versionen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Otras versiones" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Outras versões" + } + } + } + }, + "Out of Spotlight, Collection, Surprise me and search.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Out of Spotlight, Collection, Surprise me and search." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esclusi da Spotlight, Raccolta, Sorprendimi e ricerca." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exclus d’À la une, Collection, Surprenez-moi et de la recherche." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird aus Spotlight, Sammlung, „Überrasch mich“ und der Suche ausgeblendet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fuera de Destacados, Colección, Sorpréndeme y las búsquedas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fora de Destaques, Coleção, Surpreenda-me e busca." + } + } + } + }, + "Output follows the window/display. Render scale changes the internal 3D resolution only; native 1440p/4K is not available in this engine pin.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Output follows the window/display. Render scale changes the internal 3D resolution only; native 1440p/4K is not available in this engine pin." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’output segue la finestra o lo schermo. La scala di rendering modifica solo la risoluzione 3D interna; il 1440p/4K nativo non è disponibile nella versione fissata del motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La sortie s’adapte à la fenêtre ou à l’écran. L’échelle de rendu modifie uniquement la résolution 3D interne ; le rendu natif 1440p/4K n’est pas disponible avec cette version épinglée du moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Ausgabe richtet sich nach Fenster oder Bildschirm. Die Render-Skalierung ändert nur die interne 3D-Auflösung; natives 1440p/4K ist mit dieser festgelegten Engine-Version nicht verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La salida se adapta a la ventana o pantalla. La escala de renderizado solo cambia la resolución 3D interna; la versión fijada del motor no admite 1440p/4K nativo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A saída acompanha a janela ou a tela. A escala de renderização altera apenas a resolução 3D interna; 1440p/4K nativo não está disponível nesta versão fixada da engine." + } + } + } + }, + "Override enabled": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Override enabled" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controllo ignorato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dérogation activée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausnahme aktiviert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Excepción activada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Exceção ativada" + } + } + } + }, + "PICKS FROM": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "PICKS FROM" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "SCEGLIE TRA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CHOISIT PARMI" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "WÄHLT AUS" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ELIGE ENTRE" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ESCOLHE ENTRE" + } + } + } + }, + "PLATFORM": { + "comment": "SeleneStatsRow.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "PLATFORM" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "PIATTAFORMA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PLATEFORME" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "PLATTFORM" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "PLATAFORMA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "PLATAFORMA" + } + } + } + }, + "PLAY TIME": { + "comment": "DetailsBands.swift:66", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "PLAY TIME" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "TEMPO DI GIOCO" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TEMPS DE JEU" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SPIELZEIT" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "TIEMPO DE JUEGO" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "TEMPO DE JOGO" + } + } + } + }, + "PLAYTIME": { + "comment": "DetailsTopBlock.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "PLAYTIME" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "TEMPO DI GIOCO" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "TEMPS DE JEU" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SPIELZEIT" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "TIEMPO DE JUEGO" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "TEMPO DE JOGO" + } + } + } + }, + "PNG or JPEG": { + "comment": "ChangeArtworkView.swift:450", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "PNG or JPEG" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "PNG o JPEG" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "PNG ou JPEG" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "PNG oder JPEG" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "PNG o JPEG" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "PNG ou JPEG" + } + } + } + }, + "PREVIEW": { + "comment": "ThemePreview.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "PREVIEW" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "ANTEPRIMA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "APERÇU" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "VORSCHAU" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "VISTA PREVIA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "PRÉVIA" + } + } + } + }, + "Paste the API key from Preferences → API in your SteamGridDB profile. It stays on this Mac.": { + "comment": "SteamGridDBKeyDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Paste the API key from Preferences → API in your SteamGridDB profile. It stays on this Mac." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Incolla la chiave API da Preferences → API nel tuo profilo SteamGridDB. Resta su questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Collez la clé API depuis Preferences → API dans votre profil SteamGridDB. Elle reste sur ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Füge den API-Schlüssel aus Preferences → API in deinem SteamGridDB-Profil ein. Er bleibt auf diesem Mac." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pega la clave de API de Preferences → API de tu perfil de SteamGridDB. Se guarda en este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cole a chave de API de Preferences → API no seu perfil do SteamGridDB. Ela fica apenas neste Mac." + } + } + } + }, + "Performance": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Performance" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prestazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Performances" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Leistung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Rendimiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desempenho" + } + } + } + }, + "Performance HUD": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Performance HUD" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "HUD delle prestazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "HUD de performances" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Leistungs-HUD" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "HUD de rendimiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "HUD de desempenho" + } + } + } + }, + "Performance overlay": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Performance overlay" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prestazioni in sovrimpressione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affichage des performances" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Leistungsanzeige" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Panel de rendimiento superpuesto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sobreposição de desempenho" + } + } + } + }, + "Permanently remove %@ of build caches for all games? Future rebuilds may take longer. Your games, original game files, saves and logs will be kept. Unavailable folders will be left alone.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Permanently remove %@ of build caches for all games? Future rebuilds may take longer. Your games, original game files, saves and logs will be kept. Unavailable folders will be left alone." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovere definitivamente %@ di cache di compilazione per tutti i giochi? Le prossime ricompilazioni potrebbero richiedere più tempo. I giochi, i file originali dei giochi, i salvataggi e i log verranno conservati. Le cartelle non disponibili non verranno modificate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer définitivement %@ de caches de compilation pour tous les jeux ? Les prochaines recompilations peuvent prendre plus de temps. Vos jeux, fichiers de jeu d’origine, sauvegardes et journaux seront conservés. Les dossiers indisponibles resteront intacts." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Build-Caches für alle Spiele dauerhaft entfernen? Künftige Neuerstellungen können länger dauern. Deine Spiele, ursprünglichen Spieldateien, Spielstände und Protokolle bleiben erhalten. Nicht verfügbare Ordner bleiben unverändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar permanentemente %@ de cachés de compilación de todos los juegos? Las próximas recompilaciones podrían tardar más. Se conservarán tus juegos, archivos originales, partidas guardadas y registros. Las carpetas no disponibles seguirán intactas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover permanentemente %@ de caches de compilação de todos os jogos? As próximas recompilações podem demorar mais. Seus jogos, arquivos originais dos jogos, dados salvos e logs serão mantidos. As pastas indisponíveis não serão alteradas." + } + } + } + }, + "Permits building games when the toolchain differs from the certified engine and Metal combination shown above. Passes --allow-other-metal to nxnative. Builds are recorded as unverified.": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Permits building games when the toolchain differs from the certified engine and Metal combination shown above. Passes --allow-other-metal to nxnative. Builds are recorded as unverified." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Consente di compilare i giochi quando la toolchain differisce dalla combinazione certificata di motore e Metal indicata sopra. Passa --allow-other-metal a nxnative. Le compilazioni sono registrate come non verificate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Autorise la compilation de jeux lorsque la chaîne d’outils diffère de la combinaison certifiée du moteur et de Metal affichée ci-dessus. Transmet --allow-other-metal à nxnative. Les compilations sont enregistrées comme non vérifiées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erlaubt das Erstellen von Spiele-Apps, wenn die Toolchain von der oben gezeigten zertifizierten Kombination aus Engine und Metal abweicht. Übergibt --allow-other-metal an nxnative. Builds werden als nicht verifiziert erfasst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Permite compilar juegos cuando la cadena de herramientas difiere de la combinación certificada de motor y Metal que se muestra arriba. Pasa --allow-other-metal a nxnative. Las compilaciones se registran como no verificadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Permite compilar jogos quando o conjunto de ferramentas difere da combinação certificada de engine e Metal exibida acima. Passa --allow-other-metal ao nxnative. As compilações são registradas como não verificadas." + } + } + } + }, + "Pick": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Pick" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher" + } + } + } + }, + "Pick another game": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Pick another game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli un altro gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir un autre jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anderes Spiel auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir otro juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher outro jogo" + } + } + } + }, + "Pick the game by name": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Pick the game by name" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scegli il gioco per nome" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choisir le jeu par son nom" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel nach Namen auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elegir el juego por nombre" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escolher o jogo pelo nome" + } + } + } + }, + "Picture": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Picture" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Immagine" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Image" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bild" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Imagen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Imagem" + } + } + } + }, + "Picture shape": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Picture shape" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Proporzioni dell’immagine" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Format de l’image" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bildformat" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Proporción de imagen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Proporção da imagem" + } + } + } + }, + "Pinned commit": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Pinned commit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Commit fissato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commit épinglé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Festgelegter Commit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Commit fijado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Commit fixado" + } + } + } + }, + "Pipeline prebuild": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Pipeline prebuild" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Precompilazione delle pipeline" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Précompilation des pipelines" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Pipelines vorab erstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Precompilación de pipelines" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pré-compilação de pipelines" + } + } + } + }, + "Play": { + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Play" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioca" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jouer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Jugar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogar" + } + } + } + }, + "Play Selected Game": { + "comment": "NXLibraryApp.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Play Selected Game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avvia il gioco selezionato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jouer au jeu sélectionné" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ausgewähltes Spiel starten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Jugar al juego seleccionado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogar o jogo selecionado" + } + } + } + }, + "Play time is tracked only for launches from NX Library. Not affiliated with Nintendo.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Play time is tracked only for launches from NX Library. Not affiliated with Nintendo." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il tempo di gioco viene registrato solo per gli avvii da NX Library. Non affiliata a Nintendo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le temps de jeu est comptabilisé uniquement pour les lancements depuis NX Library. Sans affiliation avec Nintendo." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielzeit wird nur bei Starts über NX Library erfasst. Keine Verbindung zu Nintendo." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El tiempo de juego solo se registra al iniciar desde NX Library. Sin afiliación con Nintendo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O tempo de jogo é registrado apenas quando o jogo é aberto pelo NX Library. Sem afiliação com a Nintendo." + } + } + } + }, + "Plays Joy-Con 2 and Pro Controller 2 over Bluetooth: hold Sync on one during the game to pair it. macOS asks once for Bluetooth access.": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Plays Joy-Con 2 and Pro Controller 2 over Bluetooth: hold Sync on one during the game to pair it. macOS asks once for Bluetooth access." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Consente di giocare con Joy-Con 2 e Pro Controller 2 via Bluetooth: tieni premuto Sync su uno di essi durante il gioco per abbinarlo. macOS chiede una sola volta l’accesso al Bluetooth." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Permet de jouer avec les Joy-Con 2 et la Pro Controller 2 en Bluetooth : maintenez Sync sur l’une de ces manettes pendant le jeu pour l’appairer. macOS demande une seule fois l’accès au Bluetooth." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Unterstützt Joy-Con 2 und Pro Controller 2 über Bluetooth: Halte während des Spiels die Sync-Taste am Controller gedrückt, um ihn zu koppeln. macOS fragt einmal nach Bluetooth-Zugriff." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Permite jugar con Joy-Con 2 y Pro Controller 2 por Bluetooth: mantén pulsado Sync en uno durante el juego para enlazarlo. macOS pide acceso a Bluetooth una sola vez." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Permite jogar com Joy-Con 2 e Pro Controller 2 via Bluetooth: mantenha Sync pressionado em um deles durante o jogo para emparelhar. O macOS pede acesso ao Bluetooth uma vez." + } + } + } + }, + "Plays a short opening with the boot sound when the app opens.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Plays a short opening with the boot sound when the app opens." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riproduce una breve introduzione con il suono di avvio all’apertura dell’app." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche une courte introduction accompagnée du son de démarrage à l’ouverture de l’app." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielt beim Öffnen der App ein kurzes Intro mit Startklang ab." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reproduce una breve animación con el sonido de arranque al abrir la app." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reproduz uma breve abertura com o som de inicialização ao abrir o app." + } + } + } + }, + "Plays a short sound when you move, select, go back or launch a game.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Plays a short sound when you move, select, go back or launch a game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riproduce un breve suono quando ti sposti, selezioni, torni indietro o avvii un gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Émet un son bref lorsque vous naviguez, sélectionnez, revenez en arrière ou lancez un jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielt einen kurzen Ton beim Navigieren, Auswählen, Zurückgehen oder Starten eines Spiels." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reproduce un sonido breve al moverte, seleccionar, volver atrás o iniciar un juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reproduz um som curto ao navegar, selecionar, voltar ou abrir um jogo." + } + } + } + }, + "Plus · Minus": { + "comment": "ControlsView.swift:35", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Plus · Minus" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "+ · −" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plus · Moins" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Plus · Minus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "+ · −" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mais (+) · Menos (−)" + } + } + } + }, + "Popular": { + "comment": "ModCatalog.swift:60", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Popular" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Popolari" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Populaires" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beliebt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Populares" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Popular" + } + } + } + }, + "Port another game": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Port another game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea l’app per Mac di un altro gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Créer l’app Mac d’un autre jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weiteres Spiel als App erstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilar otro juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilar outro jogo" + } + } + } + }, + "Port anyway": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Port anyway" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea comunque l’app per Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Créer l’app Mac quand même" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Trotzdem als App erstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilar de todos modos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilar mesmo assim" + } + } + } + }, + "Port anyway to keep the broken shaders out, or wait for an engine update.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Port anyway to keep the broken shaders out, or wait for an engine update." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea comunque l’app per Mac escludendo gli shader non funzionanti, oppure attendi un aggiornamento del motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Créez l’app Mac quand même pour exclure les shaders défectueux, ou attendez une mise à jour du moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erstelle die App trotzdem und lasse defekte Shader aus oder warte auf ein Engine-Update." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compila de todos modos para excluir los shaders defectuosos o espera a una actualización del motor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compile mesmo assim para excluir os shaders com falha ou aguarde uma atualização da engine." + } + } + } + }, + "Port later": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Port later" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Crea l’app per Mac più tardi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Créer l’app Mac plus tard" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App später erstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilar más tarde" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilar depois" + } + } + } + }, + "Porting": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Porting" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Creazione dell’app per Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Création de l’app Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Erstellung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilando" + } + } + } + }, + "Porting cancelled": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Porting cancelled" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Creazione dell’app per Mac annullata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Création de l’app Mac annulée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Erstellung abgebrochen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación cancelada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilação cancelada" + } + } + } + }, + "Porting failed": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Porting failed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Creazione dell’app per Mac non riuscita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec de la création de l’app Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Erstellung fehlgeschlagen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La compilación ha fallado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Falha na compilação" + } + } + } + }, + "Porting in progress": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Porting in progress" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Creazione dell’app per Mac in corso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Création de l’app Mac en cours" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App wird erstellt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilación en curso" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilação em andamento" + } + } + } + }, + "Porting, show progress": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Porting, show progress" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Creazione dell’app per Mac, mostra avanzamento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Création de l’app Mac, afficher la progression" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App wird erstellt, Fortschritt anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilando, mostrar progreso" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilando, mostrar progresso" + } + } + } + }, + "Portings and launches write here.": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Portings and launches write here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Qui viene scritto l’output delle creazioni di app per Mac e degli avvii." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les créations d’apps Mac et les lancements écrivent ici." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Erstellungen und Spielstarts werden hier protokolliert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las compilaciones y los inicios escriben aquí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "As compilações e execuções são registradas aqui." + } + } + } + }, + "Power": { + "comment": "MenuSelectionModel.swift:16", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Power" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alimentazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Alimentation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein/Aus" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sesión" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Energia" + } + } + } + }, + "Preparing": { + "comment": "AddGameStepThreeView.swift:61", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Preparazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird vorbereitet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Preparando" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Preparando" + } + } + } + }, + "Preparing %@": { + "comment": "AppViewModel+AppIcon.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Preparazione di %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparation de %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ wird vorbereitet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Preparando %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Preparando %@" + } + } + } + }, + "Preparing the app icon. The game will start when it is ready. This can take a minute for a large game.": { + "comment": "AppViewModel+AppIcon.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Preparing the app icon. The game will start when it is ready. This can take a minute for a large game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Preparazione dell’icona dell’app. Il gioco si avvierà quando sarà pronta. Per un gioco di grandi dimensioni può servire un minuto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Préparation de l’icône de l’app. Le jeu démarrera dès qu’elle sera prête. Cela peut prendre une minute pour un jeu volumineux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das App-Symbol wird vorbereitet. Sobald es fertig ist, startet das Spiel. Bei großen Spielen kann das eine Minute dauern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Preparando el icono de la app. El juego se iniciará cuando esté listo. Si el juego es grande, puede tardar un minuto." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Preparando o ícone do app. O jogo será iniciado assim que estiver pronto. Isso pode levar um minuto para jogos grandes." + } + } + } + }, + "Press a button to join": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Press a button to join" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Premi un pulsante per partecipare" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appuyer sur un bouton pour rejoindre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zum Beitreten eine Taste drücken" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pulsa un botón para unirte" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pressione um botão para entrar" + } + } + } + }, + "Press left / right stick": { + "comment": "ControlsView.swift:34", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Press left / right stick" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Premi lo stick sinistro / destro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appuyer sur le stick gauche / droit" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Linken / rechten Stick drücken" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pulsa el stick izquierdo / derecho" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pressione o analógico esquerdo / direito" + } + } + } + }, + "Pressing View flips between Spotlight and Collection; holding it opens Find a game.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Pressing View flips between Spotlight and Collection; holding it opens Find a game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Premendo View si alternano Spotlight e Raccolta; tenendolo premuto si apre Trova un gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Appuyer sur View alterne entre À la une et Collection ; un appui prolongé ouvre Trouver un jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Ansicht-Taste wechselt zwischen Spotlight und Sammlung; gedrückt halten öffnet „Spiel finden“." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pulsar View alterna entre Destacados y Colección; mantenerlo pulsado abre Buscar un juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pressionar View alterna entre Destaques e Coleção; mantê-lo pressionado abre Encontrar um jogo." + } + } + } + }, + "Previous": { + "comment": "ChangeMetadataView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Previous" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Precedente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Précédent" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurück" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Anterior" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Anterior" + } + } + } + }, + "Previous %@": { + "comment": "SeleneSettingsRow.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Previous %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ precedente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Choix précédent pour %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorheriger Wert für %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "%@ anterior" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ anterior" + } + } + } + }, + "Project documentation is not installed on this Mac.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Project documentation is not installed on this Mac." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La documentazione del progetto non è installata su questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La documentation du projet n’est pas installée sur ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Projektdokumentation ist auf diesem Mac nicht installiert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La documentación del proyecto no está instalada en este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A documentação do projeto não está instalada neste Mac." + } + } + } + }, + "Publisher": { + "comment": "MetadataPicker.swift:13", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Publisher" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Editore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éditeur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Publisher" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Editora" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Publicadora" + } + } + } + }, + "Put each mod in its own folder here.": { + "comment": "ModsPanel.swift:206", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Put each mod in its own folder here." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Inserisci qui ogni mod nella propria cartella." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Placez chaque mod dans son propre dossier ici." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Lege hier jeden Mod in einem eigenen Ordner ab." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pon aquí cada mod en su propia carpeta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Coloque cada mod em sua própria pasta aqui." + } + } + } + }, + "Python 3.14 with its modules, zstd and glslang, inside NX Library.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Python 3.14 with its modules, zstd and glslang, inside NX Library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Python 3.14 con i suoi moduli, zstd e glslang, all’interno di NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Python 3.14 avec ses modules, zstd et glslang, intégrés à NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Python 3.14 mit seinen Modulen, zstd und glslang, in NX Library enthalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Python 3.14 con sus módulos, zstd y glslang, dentro de NX Library." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Python 3.14 com seus módulos, zstd e glslang, dentro do NX Library." + } + } + } + }, + "Python and build tools": { + "comment": "SettingsEngineSection.swift:85", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Python and build tools" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Python e strumenti di compilazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Python et outils de compilation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Python und Build-Werkzeuge" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Python y herramientas de compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Python e ferramentas de compilação" + } + } + } + }, + "Python and tools": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Python and tools" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Python e strumenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Python et outils" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Python und Werkzeuge" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Python y herramientas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Python e ferramentas" + } + } + } + }, + "Python included": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Python included" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Python incluso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Python intégré" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Python enthalten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Python incluido" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Python incluído" + } + } + } + }, + "Python missing": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Python missing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Python non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Python manquant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Python fehlt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta Python" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Python ausente" + } + } + } + }, + "Python venv": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Python venv" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Venv Python" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Environnement venv Python" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Python-venv" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Entorno virtual de Python" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "venv do Python" + } + } + } + }, + "Python venv ready": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Python venv ready" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Venv Python pronto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Environnement venv Python prêt" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Python-venv bereit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Entorno virtual de Python listo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "venv do Python pronto" + } + } + } + }, + "Q": { + "comment": "ButtonPrompts.swift:39", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Q" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Q" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Q" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Q" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Q" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Q" + } + } + } + }, + "Quiet launch screen": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Quiet launch screen" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermata di avvio silenziosa" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écran de lancement silencieux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Leiser Startbildschirm" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pantalla de inicio silenciosa" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tela de abertura silenciosa" + } + } + } + }, + "Quit": { + "comment": "MenuSelectionModel.swift:40", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Quit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esci" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quitter" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Salir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encerrar" + } + } + } + }, + "Quit the game": { + "comment": "ControlsView.swift:42", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Quit the game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Esci dal gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quitter le jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel beenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Salir del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encerrar o jogo" + } + } + } + }, + "Quit the game to install mods.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Quit the game to install mods." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiudi il gioco per installare le mod." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quittez le jeu pour installer des mods." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beende das Spiel, um Mods zu installieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cierra el juego para instalar mods." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Encerre o jogo para instalar mods." + } + } + } + }, + "RANK": { + "comment": "DetailsBands.swift:72", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "RANK" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "POSIZIONE" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "RANG" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "RANG" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "POSICIÓN" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "POSIÇÃO" + } + } + } + }, + "RELEASED": { + "comment": "DetailsTopBlock.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "RELEASED" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "DATA DI USCITA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "DATE DE SORTIE" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "VERÖFFENTLICHT" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "LANZAMIENTO" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "LANÇAMENTO" + } + } + } + }, + "Re-check": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Re-check" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricontrolla" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Revérifier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut prüfen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volver a comprobar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificar novamente" + } + } + } + }, + "Re-check the environment and build requirements.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Re-check the environment and build requirements." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricontrolla l’ambiente e i requisiti di compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Revérifier l’environnement et les prérequis de compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Umgebung und Build-Voraussetzungen erneut prüfen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a comprobar el entorno y los requisitos de compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifique novamente o ambiente e os requisitos de compilação." + } + } + } + }, + "Re-run the environment and compatibility checks.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Re-run the environment and compatibility checks." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripeti i controlli dell’ambiente e della compatibilità." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Relancer les vérifications de l’environnement et de la compatibilité." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Umgebungs- und Kompatibilitätsprüfung erneut ausführen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a ejecutar las comprobaciones del entorno y de compatibilidad." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Execute novamente as verificações de ambiente e compatibilidade." + } + } + } + }, + "Read more": { + "comment": "SpotlightPageSections.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Read more" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Leggi di più" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lire la suite" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mehr lesen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Leer más" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ler mais" + } + } + } + }, + "Readiness": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Readiness" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Stato di preparazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "État de préparation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bereitschaft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estado de preparación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Prontidão" + } + } + } + }, + "Reading the folder…": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reading the folder…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lettura della cartella…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lecture du dossier…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ordner wird gelesen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Leyendo la carpeta…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Lendo a pasta…" + } + } + } + }, + "Ready": { + "comment": "SetupCheckView.swift:221", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pronto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bereit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Listo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pronto" + } + } + } + }, + "Ready to build and play": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready to build and play" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pronto per compilare e giocare" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt à compiler et à jouer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bereit zum Erstellen und Spielen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Listo para compilar y jugar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pronto para compilar e jogar" + } + } + } + }, + "Ready to find game sources.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready to find game sources." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pronto a trovare le sorgenti dei giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt à rechercher les sources des jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bereit zur Suche nach Spielquellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Listo para buscar archivos de origen de juegos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pronto para encontrar arquivos de origem dos jogos." + } + } + } + }, + "Ready to play": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready to play" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pronto per giocare" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt à jouer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielbereit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Listo para jugar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pronto para jogar" + } + } + } + }, + "Ready when you quit": { + "comment": "PendingInstall.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Ready when you quit" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pronta alla chiusura del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Prêt à la fermeture du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bereit nach Spielende" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Listo cuando cierres el juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pronto ao encerrar o jogo" + } + } + } + }, + "Rebuild": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rebuild" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricompila" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recompiler" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut erstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Recompilar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recompilar" + } + } + } + }, + "Recalculate": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Recalculate" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricalcola" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recalculer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neu berechnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volver a calcular" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recalcular" + } + } + } + }, + "Recalculate storage and choose the game again. Nothing was deleted.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Recalculate storage and choose the game again. Nothing was deleted." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricalcola lo spazio occupato e scegli di nuovo il gioco. Non è stato eliminato nulla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recalculez l’espace de stockage et choisissez à nouveau le jeu. Rien n’a été supprimé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Berechne den Speicherbedarf neu und wähle das Spiel erneut aus. Es wurde nichts gelöscht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a calcular el almacenamiento y elige el juego de nuevo. No se ha eliminado nada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recalcule o armazenamento e escolha o jogo novamente. Nada foi apagado." + } + } + } + }, + "Recently played": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Recently played" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giocati di recente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Joués récemment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kürzlich gespielt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Jugados recientemente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogados recentemente" + } + } + } + }, + "Recently updated": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Recently updated" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiornate di recente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mis à jour récemment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kürzlich aktualisiert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizados recientemente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizados recentemente" + } + } + } + }, + "Recommended for your newer Metal compiler: builds are recorded as untested.": { + "comment": "SettingsEngineSection.swift:234", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Recommended for your newer Metal compiler: builds are recorded as untested." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Consigliato per il tuo compilatore Metal più recente: le compilazioni sono registrate come non testate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recommandé pour votre compilateur Metal plus récent : les compilations sont enregistrées comme non testées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für deinen neueren Metal-Compiler empfohlen: Builds werden als ungetestet erfasst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Recomendado para tu compilador Metal más reciente: las compilaciones se registran como no probadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recomendado para seu compilador Metal mais recente: as compilações são registradas como não testadas." + } + } + } + }, + "Reconnect this folder or choose another. Existing game apps still play.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reconnect this folder or choose another. Existing game apps still play." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricollega questa cartella o scegline un’altra. Le app dei giochi esistenti funzionano comunque." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reconnectez ce dossier ou choisissez-en un autre. Les apps de jeux existantes restent jouables." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbinde diesen Ordner erneut oder wähle einen anderen. Vorhandene Spiele-Apps bleiben spielbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a conectar esta carpeta o elige otra. Las apps de juegos existentes siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reconecte esta pasta ou escolha outra. Apps de jogos existentes continuam funcionando." + } + } + } + }, + "Recovery needed": { + "comment": "SavesPanel.swift:135", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Recovery needed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Recupero necessario" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupération nécessaire" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wiederherstellung erforderlich" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Es necesario recuperar las partidas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recuperação necessária" + } + } + } + }, + "Reduce motion": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reduce motion" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riduci movimento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réduire les animations" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bewegung reduzieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reducir movimiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reduzir movimento" + } + } + } + }, + "Reduced transitions": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reduced transitions" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Transizioni ridotte" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Transitions réduites" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Reduzierte Übergänge" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Transiciones reducidas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Transições reduzidas" + } + } + } + }, + "Refresh": { + "comment": "SavesPanel.swift:144", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Refresh" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiorna" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actualiser" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktualisieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizar" + } + } + } + }, + "Refresh library at launch": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Refresh library at launch" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiorna la libreria all’avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Actualiser la bibliothèque au lancement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothek beim Start aktualisieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizar la biblioteca al abrir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizar biblioteca ao iniciar" + } + } + } + }, + "Region": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Region" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Zona" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Région" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Region" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Región" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Região" + } + } + } + }, + "Regional formats": { + "comment": "Presentation only. Genre IDs and original metadata stay unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Regional formats" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Formati regionali" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Formats régionaux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Regionale Formate" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Formatos regionales" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Formatos regionais" + } + } + } + }, + "Reinstall NX Library so the porting uses its own arm64 Python.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reinstall NX Library so the porting uses its own arm64 Python." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Reinstalla NX Library affinché la creazione dell’app per Mac usi il Python arm64 incluso." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinstallez NX Library pour que la création de l’app Mac utilise son propre Python arm64." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installiere NX Library erneut, damit zur App-Erstellung das eigene arm64-Python verwendet wird." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reinstala NX Library para que la compilación use su propio Python arm64." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reinstale o NX Library para que a compilação use o Python arm64 incluído no app." + } + } + } + }, + "Release date": { + "comment": "MetadataPicker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Release date" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Data di uscita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Date de sortie" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Veröffentlichungsdatum" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fecha de lanzamiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Data de lançamento" + } + } + } + }, + "Release notes · %@": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Release notes · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Note di rilascio · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Notes de version · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Versionshinweise · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Notas de la versión · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Notas da versão · %@" + } + } + } + }, + "Remaining build data": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remaining build data" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dati di build rimanenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Données de compilation restantes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Verbleibende Build-Daten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Datos de compilación restantes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dados de compilação restantes" + } + } + } + }, + "Remove": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Entfernen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover" + } + } + } + }, + "Remove %@ of logs for all games? Your games, sources and saves will be kept.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove %@ of logs for all games? Your games, sources and saves will be kept." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovere %@ di log per tutti i giochi? I giochi, le sorgenti e i salvataggi verranno conservati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer %@ de journaux pour tous les jeux ? Vos jeux, sources et sauvegardes seront conservés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ Protokolle für alle Spiele entfernen? Deine Spiele, Quellen und Spielstände bleiben erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar %@ de registros de todos los juegos? Se conservarán tus juegos, archivos de origen y partidas guardadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover %@ de logs de todos os jogos? Seus jogos, arquivos de origem e dados salvos serão mantidos." + } + } + } + }, + "Remove %@?": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove %@?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovere %@?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retirer %@ ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ entfernen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Quitar %@?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover %@?" + } + } + } + }, + "Remove cheats": { + "comment": "CheatsPanel.swift:298", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove cheats" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovi trucchi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les codes de triche" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cheats entfernen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminar trucos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover trapaças" + } + } + } + }, + "Remove from favorites": { + "comment": "SpotlightInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove from favorites" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovi dai preferiti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retirer des favoris" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus Favoriten entfernen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Quitar de favoritos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover dos favoritos" + } + } + } + }, + "Remove game": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovi gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retirer le jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiel entfernen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminar juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover jogo" + } + } + } + }, + "Remove key": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove key" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovi chiave" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer la clé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel entfernen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminar clave" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover chave" + } + } + } + }, + "Remove sources & build cache": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove sources & build cache" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovi sorgenti e cache di compilazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les sources et le cache de compilation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen & Build-Cache entfernen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Eliminar archivos de origen y caché de compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover arquivos de origem e cache de compilação" + } + } + } + }, + "Remove sources and build cache?": { + "comment": "GameOptionsView.swift:434", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove sources and build cache?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovere sorgenti e cache di compilazione?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer les sources et le cache de compilation ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen und Build-Cache entfernen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar los archivos de origen y la caché de compilación?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover arquivos de origem e cache de compilação?" + } + } + } + }, + "Remove this %@?": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove this %@?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovere questo elemento (%@)?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer cet élément (%@) ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ entfernen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar este %@?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover este item: %@?" + } + } + } + }, + "Remove this picture?": { + "comment": "ThemePickerView.swift:828", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove this picture?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovere questa immagine?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer cette image ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Bild entfernen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar esta imagen?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover esta imagem?" + } + } + } + }, + "Remove this video?": { + "comment": "ThemePickerView.swift:828", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Remove this video?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rimuovere questo video?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprimer cette vidéo ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Video entfernen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Eliminar este vídeo?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Remover este vídeo?" + } + } + } + }, + "Removed %@ from the library; its source folders were left in place.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Removed %@ from the library; its source folders were left in place." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "%@ rimosso dalla libreria; le cartelle sorgente sono state lasciate al loro posto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ a été retiré de la bibliothèque ; ses dossiers sources ont été laissés en place." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ aus der Bibliothek entfernt; die Quellordner bleiben an ihrem Platz." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha quitado %@ de la biblioteca; sus carpetas de origen siguen en su ubicación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "%@ foi removido da biblioteca; suas pastas de origem foram mantidas no local." + } + } + } + }, + "Removed %lld source files (%@).": { + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Removed %lld source file (%@)." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Removed %lld source files (%@)." + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Rimosso %lld file sorgente (%@)." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Rimossi %lld file sorgenti (%@)." + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld fichier source supprimé (%@)." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld fichiers sources supprimés (%@)." + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld Quelldatei entfernt (%@)." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld Quelldateien entfernt (%@)." + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "Se eliminó %lld archivo de origen (%@)." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "Se eliminaron %lld archivos de origen (%@)." + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "%lld arquivo de origem removido (%@)." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "%lld arquivos de origem removidos (%@)." + } + } + } + } + } + } + }, + "Rename this folder: its name can't contain ':'.": { + "comment": "ModsPanel.swift:398", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rename this folder: its name can't contain ':'." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Rinomina questa cartella: il nome non può contenere ':'." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Renommez ce dossier : son nom ne peut pas contenir « : »." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Benenne diesen Ordner um: Sein Name darf kein ':' enthalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambia el nombre de esta carpeta: no puede contener «:»." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Renomeie esta pasta: o nome não pode conter ':'." + } + } + } + }, + "Render scale": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Render scale" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scala di rendering" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échelle de rendu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Render-Skalierung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Escala de renderizado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escala de renderização" + } + } + } + }, + "Renders about a quarter more pixels: a little slower and warmer.": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Renders about a quarter more pixels: a little slower and warmer." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Renderizza circa un quarto di pixel in più: leggermente più lento e più caldo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Calcule environ un quart de pixels en plus : un peu plus lent et plus chaud." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Rendert etwa ein Viertel mehr Pixel: etwas langsamer und wärmer." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Renderiza aproximadamente un cuarto más de píxeles: algo más lento y con más calor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Renderiza cerca de um quarto a mais de pixels: um pouco mais lento e mais quente." + } + } + } + }, + "Reopen the game that was selected when you quit.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reopen the game that was selected when you quit." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Seleziona di nuovo il gioco che era selezionato alla chiusura." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réafficher le jeu sélectionné à la fermeture de l’app." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das beim Beenden ausgewählte Spiel wieder auswählen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a seleccionar el juego que estaba seleccionado al salir." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reabra o jogo que estava selecionado ao encerrar o app." + } + } + } + }, + "Replace": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Replace" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sostituisci" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplacer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ersetzen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reemplazar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Substituir" + } + } + } + }, + "Replace Key...": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Replace Key..." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sostituisci chiave..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplacer la clé..." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schlüssel ersetzen..." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reemplazar clave..." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Substituir chave..." + } + } + } + }, + "Replace cheats?": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Replace cheats?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sostituire i trucchi?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplacer les codes de triche ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Cheats ersetzen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Reemplazar los trucos?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Substituir trapaças?" + } + } + } + }, + "Replaces slides and zooms with fades.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Replaces slides and zooms with fades." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sostituisce scorrimenti e zoom con dissolvenze." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Remplace les glissements et les zooms par des fondus." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ersetzt Schiebe- und Zoombewegungen durch Überblendungen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sustituye los deslizamientos y el zoom por fundidos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Substitui deslizamentos e zooms por transições de opacidade." + } + } + } + }, + "Requires: ": { + "comment": "ModDetailPanel.swift:298", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Requires: " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Richiede: " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Nécessite : " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Benötigt: " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Requiere: " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Requer: " + } + } + } + }, + "Rescan the app library for new or removed builds.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rescan the app library for new or removed builds." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca di nuovo nella libreria delle app le compilazioni nuove o rimosse." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Analyser à nouveau la bibliothèque d’apps pour détecter les compilations ajoutées ou supprimées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Bibliothek erneut nach neuen oder entfernten Builds durchsuchen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vuelve a analizar la biblioteca de apps para detectar compilaciones nuevas o eliminadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Examina novamente a biblioteca de apps em busca de compilações novas ou removidas." + } + } + } + }, + "Reset": { + "comment": "SettingsSpotlightSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reset" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Reimposta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialiser" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zurücksetzen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restablecer" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Redefinir" + } + } + } + }, + "Reset group": { + "comment": "SettingsView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reset group" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Reimposta gruppo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réinitialiser le groupe" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Gruppe zurücksetzen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restablecer grupo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Redefinir grupo" + } + } + } + }, + "Restart app": { + "comment": "MenuSelectionModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restart app" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riavvia app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Redémarrer l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App neu starten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Reiniciar app" + } + } + } + }, + "Restore": { + "comment": "SavesPanel.swift:194", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restore" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristina" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wiederherstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restaurar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Restaurar" + } + } + } + }, + "Restore last game": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restore last game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristina l’ultimo gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer le dernier jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Letztes Spiel wiederherstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restaurar último juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Restaurar último jogo" + } + } + } + }, + "Restore last selected game": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restore last selected game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristina l’ultimo gioco selezionato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer le dernier jeu sélectionné" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zuletzt ausgewähltes Spiel wiederherstellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Restaurar el último juego seleccionado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Restaurar último jogo selecionado" + } + } + } + }, + "Restore the backup from %@? The current save will be backed up first.\n\nBackup game version: %@ · Installed: %@. Different versions may be incompatible.\n\nCloud saves may replace the restored local save if sync is enabled later.": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restore the backup from %@? The current save will be backed up first.\n\nBackup game version: %@ · Installed: %@. Different versions may be incompatible.\n\nCloud saves may replace the restored local save if sync is enabled later." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristinare il backup del %@? Prima verrà eseguito un backup del salvataggio attuale.\n\nVersione del gioco nel backup: %@ · Installata: %@. Versioni diverse potrebbero essere incompatibili.\n\nSe attivi la sincronizzazione cloud in seguito, il salvataggio ripristinato potrebbe essere sostituito." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer la sauvegarde du %@ ? Une copie de la sauvegarde actuelle sera d’abord créée.\n\nVersion du jeu dans la sauvegarde : %@ · Installée : %@. Des versions différentes peuvent être incompatibles.\n\nLes sauvegardes cloud peuvent remplacer les données restaurées si vous activez la synchronisation plus tard." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Backup vom %@ wiederherstellen? Der aktuelle Spielstand wird zuerst gesichert.\n\nSpielversion im Backup: %@ · Installiert: %@. Unterschiedliche Versionen können inkompatibel sein.\n\nWenn du die Cloud-Synchronisierung später aktivierst, kann sie den wiederhergestellten Spielstand ersetzen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Restaurar la copia de seguridad del %@? Primero se hará una copia de la partida actual.\n\nVersión del juego en la copia: %@ · Instalada: %@. Las distintas versiones podrían ser incompatibles.\n\nLas partidas de la nube podrían reemplazar la restaurada si activas la sincronización más adelante." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Restaurar o backup de %@? Primeiro será feito um backup do salvamento atual.\n\nVersão do jogo no backup: %@ · Instalada: %@. Versões diferentes podem ser incompatíveis.\n\nOs salvamentos na nuvem poderão substituir o restaurado se a sincronização for ativada depois." + } + } + } + }, + "Restore this backup?": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Restore this backup?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristinare questo backup?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Restaurer cette copie de sauvegarde ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Backup wiederherstellen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Restaurar esta copia de seguridad?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Restaurar este backup?" + } + } + } + }, + "Resume": { + "comment": "DetailsTopBlock.swift:174", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Resume" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprendi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reprendre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fortsetzen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reanudar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Retomar" + } + } + } + }, + "Retry": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Retry" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprova" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réessayer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut versuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reintentar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tentar novamente" + } + } + } + }, + "Retry installation": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Retry installation" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprova installazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réessayer l’installation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Installation wiederholen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reintentar instalación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tentar instalar novamente" + } + } + } + }, + "Retry recovery": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Retry recovery" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprova recupero" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réessayer la récupération" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wiederherstellung wiederholen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reintentar recuperación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tentar recuperar novamente" + } + } + } + }, + "Return / Space · ": { + "comment": "ControlsView.swift:47", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Return / Space · " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Invio / Spazio · " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour / Espace · " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Return / Leertaste · " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Retorno / Espacio · " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Return / Espaço · " + } + } + } + }, + "Return · Tab": { + "comment": "ControlsView.swift:35", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Return · Tab" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Invio · Tab" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour · Tab" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Return · Tab" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Retorno · Tabulador" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Return · Tab" + } + } + } + }, + "Returning to the tested engine…": { + "comment": "SetupCheckModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Returning to the tested engine…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ritorno al motore testato…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour au moteur testé…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zur getesteten Engine wird zurückgekehrt …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volviendo al motor probado…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Voltando à engine testada…" + } + } + } + }, + "Reveal": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reveal" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra nel Finder" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Finder zeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar" + } + } + } + }, + "Reveal app in Finder": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reveal app in Finder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra app nel Finder" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher l’app dans le Finder" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App im Finder zeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar app en el Finder" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar app no Finder" + } + } + } + }, + "Reveal in Finder": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reveal in Finder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra nel Finder" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher dans le Finder" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Finder zeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar en el Finder" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar no Finder" + } + } + } + }, + "Reveal the app in Finder.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reveal the app in Finder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra l’app nel Finder." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher l’app dans le Finder." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App im Finder zeigen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra la app en el Finder." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostre o app no Finder." + } + } + } + }, + "Reveal the latest build log.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reveal the latest build log." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra il log dell’ultima compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher le dernier journal de compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neuestes Build-Protokoll im Finder zeigen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra el último registro de compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostre o log da compilação mais recente." + } + } + } + }, + "Revert to the Mac’s region.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Revert to the Mac’s region." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ripristina la zona del Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rétablir la région du Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zur Region des Macs zurückkehren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volver a la región del Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Voltar à região do Mac." + } + } + } + }, + "Review": { + "comment": "AddGameView.swift:180", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Review" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riepilogo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Überprüfen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Revisión" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Revisão" + } + } + } + }, + "Right stick": { + "comment": "ControlsView.swift:37", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Right stick" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Stick destro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stick droit" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Rechter Stick" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Stick derecho" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Analógico direito" + } + } + } + }, + "Row": { + "comment": "SettingsSpotlightSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Row" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riga" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rangée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Reihe" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fila" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fileira" + } + } + } + }, + "Rumble": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rumble" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Vibrazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vibrations" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vibration" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Vibración" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Vibração" + } + } + } + }, + "Rumble cutoff": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rumble cutoff" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Soglia della vibrazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Seuil de coupure des vibrations" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vibrationsschwelle" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Umbral de vibración" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Limiar da vibração" + } + } + } + }, + "Rumble strength": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Rumble strength" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Intensità della vibrazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intensité des vibrations" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vibrationsstärke" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Intensidad de vibración" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Intensidade da vibração" + } + } + } + }, + "Running the compatibility check...": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Running the compatibility check..." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controllo di compatibilità in corso..." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification de la compatibilité..." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kompatibilität wird geprüft..." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ejecutando la comprobación de compatibilidad..." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Executando a verificação de compatibilidade..." + } + } + } + }, + "Runs once: use it in the game's Cheats panel (⌘⇧C).": { + "comment": "CheatsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Runs once: use it in the game's Cheats panel (⌘⇧C)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Eseguito una sola volta: usalo nel pannello Trucchi del gioco (⌘⇧C)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Exécution unique : utilisez-le dans le panneau Codes de triche du jeu (⌘⇧C)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird einmal ausgeführt: Verwende den Cheat im Cheats-Bereich des Spiels (⌘⇧C)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ejecuta una vez: úsalo en el panel de trucos del juego (⌘⇧C)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Executa uma vez: use no painel Trapaças do jogo (⌘⇧C)." + } + } + } + }, + "SAVES": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "SAVES" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "SALVATAGGI" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SAUVEGARDES" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SPIELSTÄNDE" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "PARTIDAS GUARDADAS" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "DADOS SALVOS" + } + } + } + }, + "SPIRV-Cross Metal Shading Language compiler": { + "comment": "SettingsAboutSection.swift:19", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "SPIRV-Cross Metal Shading Language compiler" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Compilatore SPIRV-Cross per Metal Shading Language" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Compilateur Metal Shading Language de SPIRV-Cross" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SPIRV-Cross-Compiler für Metal Shading Language" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compilador de Metal Shading Language de SPIRV-Cross" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Compilador SPIRV-Cross para Metal Shading Language" + } + } + } + }, + "Safe for work, 2K and up. Every picture belongs to its owner.": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Safe for work, 2K and up. Every picture belongs to its owner." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Contenuti adatti al lavoro, da 2K in su. Ogni immagine appartiene al rispettivo proprietario." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Images tout public, en 2K et plus. Chaque image appartient à son propriétaire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Für die Anzeige am Arbeitsplatz geeignet, ab 2K. Alle Bilder gehören ihren jeweiligen Rechteinhabern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Apto para todos los públicos, 2K o más. Cada imagen pertenece a su propietario." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Conteúdo adequado para o trabalho, a partir de 2K. Cada imagem pertence ao seu titular." + } + } + } + }, + "Same as app": { + "comment": "ThemeStore.swift:92", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Same as app" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Come l’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comme l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wie in der App" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Igual que la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Igual ao app" + } + } + } + }, + "Save": { + "comment": "SteamGridDBKeyDialog.swift:28", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salva" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Speichern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Guardar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Salvar" + } + } + } + }, + "Save backups": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save backups" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Backup dei salvataggi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copies de sauvegarde" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstand-Backups" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Copias de seguridad de partidas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Backups de dados salvos" + } + } + } + }, + "Save changes": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save changes" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salva modifiche" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistrer les modifications" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Änderungen speichern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Guardar cambios" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Salvar alterações" + } + } + } + }, + "Save recovery completed for %@.": { + "comment": "AppViewModel+Saves.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save recovery completed for %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Recupero dei salvataggi di %@ completato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupération des sauvegardes de %@ terminée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstandwiederherstellung für %@ abgeschlossen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha completado la recuperación de partidas de %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recuperação de dados salvos de %@ concluída." + } + } + } + }, + "Save recovery for %@: %@ Backups: %@": { + "comment": "AppViewModel+Saves.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save recovery for %@: %@ Backups: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Recupero dei salvataggi di %@: %@ Backup: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Récupération des sauvegardes de %@ : %@ Copies de sauvegarde : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstandwiederherstellung für %@: %@ Backups: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Recuperación de partidas de %@: %@ Copias de seguridad: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recuperação de dados salvos de %@: %@ Backups: %@" + } + } + } + }, + "Save recovery is required before Play. Your backup has been kept.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save recovery is required before Play. Your backup has been kept." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "È necessario recuperare i salvataggi prima di giocare. Il backup è stato conservato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une récupération des sauvegardes est nécessaire avant de jouer. Votre copie de sauvegarde a été conservée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vor dem Spielen ist eine Spielstandwiederherstellung erforderlich. Dein Backup bleibt erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Es necesario recuperar las partidas guardadas antes de jugar. Se ha conservado tu copia de seguridad." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "É necessário recuperar os dados salvos antes de Jogar. Seu backup foi mantido." + } + } + } + }, + "Save type": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Save type" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tipo di salvataggio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Type de sauvegarde" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstandtyp" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tipo de partida guardada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tipo de dados salvos" + } + } + } + }, + "Saved on this Mac": { + "comment": "ChangeArtworkView.swift:441", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Saved on this Mac" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salvata su questo Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Enregistré sur ce Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf diesem Mac gespeichert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Guardado en este Mac" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Salvo neste Mac" + } + } + } + }, + "Saved settings for %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Saved settings for %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impostazioni di %@ salvate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réglages de %@ enregistrés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen für %@ gespeichert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajustes de %@ guardados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajustes de %@ salvos" + } + } + } + }, + "Saves": { + "comment": "GameOptionsPresentation.swift:47", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Saves" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salvataggi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegardes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spielstände" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Partidas guardadas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Dados salvos" + } + } + } + }, + "Scales internal 3D scene resolution from 50% to 100% (DM_RENDER_SCALE). Decreasing internal resolution boosts framerates on demanding titles while UI overlays remain crisp.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scales internal 3D scene resolution from 50% to 100% (DM_RENDER_SCALE). Decreasing internal resolution boosts framerates on demanding titles while UI overlays remain crisp." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ridimensiona la risoluzione interna della scena 3D dal 50% al 100% (DM_RENDER_SCALE). Ridurre la risoluzione interna aumenta la frequenza dei fotogrammi nei titoli più esigenti, mentre l’interfaccia in sovrimpressione resta nitida." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajuste la résolution 3D interne de 50 % à 100 % (DM_RENDER_SCALE). Réduire la résolution interne augmente la fréquence d’images des titres exigeants, tandis que les éléments d’interface restent nets." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Skaliert die interne 3D-Szenenauflösung von 50% bis 100% (DM_RENDER_SCALE). Eine geringere interne Auflösung erhöht bei anspruchsvollen Titeln die Bildrate, während UI-Einblendungen scharf bleiben." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Escala la resolución interna de la escena 3D del 50% al 100% (DM_RENDER_SCALE). Reducirla aumenta los fotogramas por segundo en títulos exigentes y mantiene nítida la interfaz superpuesta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajusta a resolução interna da cena 3D de 50% a 100% (DM_RENDER_SCALE). Reduzir a resolução interna aumenta a taxa de quadros em jogos exigentes, enquanto a interface sobreposta permanece nítida." + } + } + } + }, + "Scales the internal 3D rendering buffer relative to native target (50% to 100%). Lowering this reduces GPU heat on battery; UI remains full resolution.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scales the internal 3D rendering buffer relative to native target (50% to 100%). Lowering this reduces GPU heat on battery; UI remains full resolution." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ridimensiona il buffer di rendering 3D interno rispetto alla risoluzione nativa prevista (dal 50% al 100%). Ridurlo limita il calore della GPU quando usi la batteria; l’interfaccia resta a piena risoluzione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajuste la résolution du tampon de rendu 3D interne par rapport à la cible native (50 % à 100 %). La réduire limite l’échauffement du GPU sur batterie ; l’interface reste en pleine résolution." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Skaliert den internen 3D-Renderpuffer relativ zur nativen Zielauflösung (50% bis 100%). Ein niedrigerer Wert reduziert im Akkubetrieb die GPU-Wärmeentwicklung; die Benutzeroberfläche bleibt in voller Auflösung." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Escala el búfer de renderizado 3D interno respecto al objetivo nativo (del 50% al 100%). Reducirlo disminuye el calor de la GPU al usar batería; la interfaz mantiene la resolución completa." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajusta a escala do buffer de renderização 3D interno em relação à resolução nativa (50% a 100%). Reduzir esse valor diminui o aquecimento da GPU ao usar a bateria; a interface mantém a resolução total." + } + } + } + }, + "Scan": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scan" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Analyser" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Durchsuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Examinar" + } + } + } + }, + "Scan game apps": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scan game apps" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca app dei giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Analyser les apps de jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiele-Apps suchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar apps de juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar apps de jogos" + } + } + } + }, + "Scanning game apps…": { + "comment": "SettingsLibrarySection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scanning game apps…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca delle app dei giochi…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Analyse des apps de jeux…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spiele-Apps werden gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando apps de juegos…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando apps de jogos…" + } + } + } + }, + "Scans your library folders for new or removed games at start.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scans your library folders for new or removed games at start." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "All’avvio cerca nelle cartelle della libreria i giochi nuovi o rimossi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Analyse les dossiers de votre bibliothèque au démarrage pour détecter les jeux ajoutés ou supprimés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Durchsucht beim Start deine Bibliotheksordner nach neuen oder entfernten Spielen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Analiza las carpetas de la biblioteca al iniciar para detectar juegos nuevos o eliminados." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Examina suas pastas da biblioteca em busca de jogos novos ou removidos ao iniciar." + } + } + } + }, + "Screen": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Screen" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bildschirm" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tela" + } + } + } + }, + "Screenshot %@": { + "comment": "SpotlightScreenshotStrip.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Screenshot %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermata %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Capture d’écran %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Screenshot %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Captura %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Captura de tela %@" + } + } + } + }, + "Screenshots": { + "comment": "DetailsBands.swift:41", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Screenshots" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Captures d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Screenshots" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Capturas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capturas de tela" + } + } + } + }, + "Screenshots · %@": { + "comment": "Compact playtime units, metadata heading or accessible duration.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Screenshots · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermate · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Captures d’écran · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Screenshots · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Capturas · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capturas de tela · %@" + } + } + } + }, + "Search": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Search" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Suchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar" + } + } + } + }, + "Search %@": { + "comment": "OnlineBrowsePanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Search %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ durchsuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar %@" + } + } + } + }, + "Search GameBanana by name": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Search GameBanana by name" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca su GameBanana per nome" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher par nom sur GameBanana" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameBanana nach Namen durchsuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar en GameBanana por nombre" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar no GameBanana pelo nome" + } + } + } + }, + "Search Nintendo Store": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Search Nintendo Store" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca nel Nintendo Store" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher sur le Nintendo Store" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Nintendo Store suchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar en Nintendo Store" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar na Nintendo Store" + } + } + } + }, + "Search settings": { + "comment": "SettingsSearchSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Search settings" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cerca nelle impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rechercher dans les réglages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen durchsuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscar ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscar ajustes" + } + } + } + }, + "Searching %@ for “%@”…": { + "comment": "ChangeArtworkView.swift:295", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Searching %@ for “%@”…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca su %@ di “%@”…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche sur %@ pour « %@ »…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ wird nach „%@“ durchsucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando en %@ «%@»…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando em %@ por “%@”…" + } + } + } + }, + "Searching GameBanana…": { + "comment": "GameBananaFindPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Searching GameBanana…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca su GameBanana…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche sur GameBanana…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "GameBanana wird durchsucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando en GameBanana…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando no GameBanana…" + } + } + } + }, + "Searching…": { + "comment": "ChangeArtworkView.swift:273", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Searching…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ricerca…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recherche…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird gesucht …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Buscando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Buscando…" + } + } + } + }, + "See every mod for this game": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "See every mod for this game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Vedi tutte le mod per questo gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir tous les mods de ce jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle Mods für dieses Spiel ansehen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ver todos los mods de este juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ver todos os mods deste jogo" + } + } + } + }, + "See how this NX Library's engine compares with seedds/nxnative main.": { + "comment": "SettingsEngineSection.swift:256", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "See how this NX Library's engine compares with seedds/nxnative main." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Confronta il motore di questa NX Library con seedds/nxnative main." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Comparer le moteur de cette version de NX Library avec main de seedds/nxnative." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vergleiche die Engine dieser NX Library-Version mit seedds/nxnative main." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Compara el motor de este NX Library con main de seedds/nxnative." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Veja como a engine deste NX Library se compara ao main de seedds/nxnative." + } + } + } + }, + "See per-game app, cache and log sizes and free disk space.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "See per-game app, cache and log sizes and free disk space." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Visualizza le dimensioni di app, cache e log per ogni gioco e lo spazio libero su disco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir la taille de l’app, du cache et des journaux par jeu, ainsi que l’espace disque libre." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-, Cache- und Protokollgrößen pro Spiel sowie freien Speicherplatz anzeigen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Consulta el tamaño de la app, la caché y los registros de cada juego y el espacio libre en disco." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Veja o tamanho do app, do cache e dos logs de cada jogo e o espaço livre em disco." + } + } + } + }, + "Select": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Select" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Seleziona" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sélectionner" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auswählen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Seleccionar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Selecionar" + } + } + } + }, + "Select a base game. Updates and DLC alone cannot be built.": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Select a base game. Updates and DLC alone cannot be built." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Seleziona un gioco base. Non è possibile creare un’app solo con aggiornamenti e DLC." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sélectionnez un jeu de base. Les mises à jour et les DLC seuls ne peuvent pas être compilés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle ein Basisspiel. Updates und DLC allein lassen sich nicht als App erstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Selecciona un juego base. Las actualizaciones y los DLC por sí solos no se pueden compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Selecione um jogo base. Não é possível compilar apenas atualizações e DLCs." + } + } + } + }, + "Select a game first.": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Select a game first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Seleziona prima un gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sélectionnez d’abord un jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wähle zuerst ein Spiel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Selecciona un juego primero." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Selecione um jogo primeiro." + } + } + } + }, + "Select to move these files to the Trash.": { + "comment": "CheatsPanel.swift:300", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Select to move these files to the Trash." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Seleziona per spostare questi file nel Cestino." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sélectionnez pour placer ces fichiers dans la corbeille." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auswählen, um diese Dateien in den Papierkorb zu legen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Selecciona para mover estos archivos a la Papelera." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Selecione para mover estes arquivos para o Lixo." + } + } + } + }, + "Selene is a Playnite theme for Windows. No Selene name, logo, wallpaper, avatar or sound is used as NX Library branding; this is a visual adaptation in SwiftUI.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Selene is a Playnite theme for Windows. No Selene name, logo, wallpaper, avatar or sound is used as NX Library branding; this is a visual adaptation in SwiftUI." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Selene è un tema di Playnite per Windows. Il nome, il logo, gli sfondi, gli avatar e i suoni di Selene non sono usati come identità di NX Library; si tratta di un adattamento visivo in SwiftUI." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Selene est un thème Playnite pour Windows. NX Library n’utilise ni le nom, ni le logo, ni les fonds d’écran, avatars ou sons de Selene comme identité de marque ; il s’agit d’une adaptation visuelle en SwiftUI." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Selene ist ein Playnite-Theme für Windows. Name, Logo, Hintergrundbilder, Avatare und Sounds von Selene werden nicht als Markenauftritt von NX Library verwendet; dies ist eine visuelle Adaption in SwiftUI." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Selene es un tema de Playnite para Windows. NX Library no usa el nombre, el logotipo, los fondos, los avatares ni los sonidos de Selene como identidad de marca; es una adaptación visual en SwiftUI." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Selene é um tema do Playnite para Windows. Nenhum nome, logotipo, papel de parede, avatar ou som do Selene é usado como identidade do NX Library; esta é uma adaptação visual em SwiftUI." + } + } + } + }, + "Separate with commas.": { + "comment": "ChangeMetadataView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Separate with commas." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Separa con virgole." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Séparez par des virgules." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mit Kommas trennen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Separa con comas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Separe por vírgulas." + } + } + } + }, + "Session card": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Session card" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scheda della sessione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fiche de session" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sitzungskarte" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tarjeta de sesión" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cartão da sessão" + } + } + } + }, + "Session ended": { + "comment": "SessionCard.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Session ended" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sessione terminata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Session terminée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sitzung beendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sesión finalizada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sessão encerrada" + } + } + } + }, + "Session ended, %@: %@ this session, %@ total": { + "comment": "SessionCard.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Session ended, %@: %@ this session, %@ total" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sessione terminata, %@: %@ in questa sessione, %@ in totale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Session terminée, %@ : %@ cette session, %@ au total" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sitzung beendet, %@: %@ in dieser Sitzung, %@ insgesamt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sesión finalizada, %@: %@ en esta sesión, %@ en total" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sessão encerrada, %@: %@ nesta sessão, %@ no total" + } + } + } + }, + "Set in env.txt": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Set in env.txt" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impostato in env.txt" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Défini dans env.txt" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In env.txt festgelegt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Configurar en env.txt" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Definido em env.txt" + } + } + } + }, + "Settings": { + "comment": "MenuSelectionModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réglages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajustes" + } + } + } + }, + "Settings background": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings background" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondo delle impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrière-plan des réglages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungshintergrund" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondo de Ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Fundo dos ajustes" + } + } + } + }, + "Settings not saved": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings not saved" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impostazioni non salvate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réglages non enregistrés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen nicht gespeichert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajustes sin guardar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajustes não salvos" + } + } + } + }, + "Settings only": { + "comment": "WallpaperPlacement.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings only" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Solo impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réglages uniquement" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nur Einstellungen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo Ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Só nos Ajustes" + } + } + } + }, + "Settings that differ from the defaults.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings that differ from the defaults." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le impostazioni che differiscono dai valori predefiniti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réglages qui diffèrent des valeurs par défaut." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen, die von den Standards abweichen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajustes que difieren de los valores predeterminados." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajustes que diferem dos padrões." + } + } + } + }, + "Settings unavailable": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impostazioni non disponibili" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réglages indisponibles" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajustes no disponibles" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajustes indisponíveis" + } + } + } + }, + "Settings wallpaper": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings wallpaper" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondo delle impostazioni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fond d’écran des réglages" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundbild der Einstellungen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondo de pantalla de Ajustes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Papel de parede dos ajustes" + } + } + } + }, + "Settings…": { + "comment": "NXLibraryApp.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Settings…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Impostazioni…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réglages…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einstellungen …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ajustes…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ajustes…" + } + } + } + }, + "Setup": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Setup" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Configurazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Configuration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einrichtung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Configuración inicial" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Configuração" + } + } + } + }, + "Setup check": { + "comment": "MenuSelectionModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Setup check" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controllo di configurazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification de configuration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einrichtungsprüfung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Comprobación inicial" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificação de configuração" + } + } + } + }, + "Setup completed.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Setup completed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Configurazione completata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Configuration terminée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einrichtung abgeschlossen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Configuración completada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Configuração concluída." + } + } + } + }, + "Setup needed or builds blocked": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Setup needed or builds blocked" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Configurazione necessaria o compilazioni bloccate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Configuration requise ou compilations bloquées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einrichtung erforderlich oder Builds gesperrt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta configurar o la compilación está bloqueada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Configuração necessária ou compilações bloqueadas" + } + } + } + }, + "Several files could be these cheats: add the one you want.": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Several files could be these cheats: add the one you want." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Più file potrebbero contenere questi trucchi: aggiungi quello che vuoi usare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Plusieurs fichiers pourraient contenir ces codes de triche : ajoutez celui que vous souhaitez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Mehrere Dateien könnten diese Cheats enthalten: Füge die gewünschte hinzu." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Varios archivos podrían contener estos trucos: añade el que quieras." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Vários arquivos podem conter estas trapaças: adicione o desejado." + } + } + } + }, + "Shaders that translated before fail with the engine's current translator.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shaders that translated before fail with the engine's current translator." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Shader che prima venivano tradotti ora non funzionano con il traduttore attuale del motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Des shaders qui étaient traduits auparavant échouent avec le traducteur actuel du moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Shader, die sich zuvor übersetzen ließen, schlagen mit dem aktuellen Übersetzer der Engine fehl." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los shaders que antes se traducían fallan con el traductor actual del motor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Shaders que eram traduzidos antes falham com o tradutor atual da engine." + } + } + } + }, + "Shading behind library text, over game artwork or your wallpaper.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shading behind library text, over game artwork or your wallpaper." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ombreggiatura dietro al testo della libreria, sopra le immagini del gioco o il tuo sfondo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Assombrissement derrière le texte de la bibliothèque, sur les illustrations des jeux ou votre fond d’écran." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Abdunklung hinter Bibliothekstext über Spiel-Artwork oder deinem Hintergrundbild." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Oscurecimiento detrás del texto de la biblioteca, sobre las imágenes del juego o tu fondo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sombra atrás do texto da biblioteca, sobre a arte do jogo ou seu papel de parede." + } + } + } + }, + "Shake": { + "comment": "ControlsView.swift:36", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shake" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scuoti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Secouer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schütteln" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Agitar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sacudir" + } + } + } + }, + "Sharper scaling of the whole picture to your screen": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sharper scaling of the whole picture to your screen" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ridimensionamento più nitido dell’intera immagine sullo schermo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une image entière plus nette, adaptée à votre écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schärfere Skalierung des gesamten Bildes auf deinen Bildschirm" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ampliación más nítida de toda la imagen a tu pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ampliação mais nítida da imagem inteira para sua tela" + } + } + } + }, + "Shift + Arrow keys · Controller": { + "comment": "ControlsView.swift:37", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shift + Arrow keys · Controller" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Maiuscole + tasti freccia · Controller" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Maj + touches fléchées · Manette" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Umschalt + Pfeiltasten · Controller" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mayúsculas + Teclas de dirección · Mando" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Shift + Teclas de seta · Controle" + } + } + } + }, + "Show": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar" + } + } + } + }, + "Show Log": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Log" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra log" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher le journal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Protokoll anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar registro" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar log" + } + } + } + }, + "Show file": { + "comment": "SavesPanel.swift:205", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show file" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra file" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher le fichier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar archivo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar arquivo" + } + } + } + }, + "Show in Finder": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show in Finder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra nel Finder" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher dans le Finder" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Finder zeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar en el Finder" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar no Finder" + } + } + } + }, + "Show in library": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show in library" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra nella libreria" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher dans la bibliothèque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In Bibliothek anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar en la biblioteca" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar na biblioteca" + } + } + } + }, + "Show less": { + "comment": "SpotlightPageSections.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Show less" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra meno" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher moins" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weniger anzeigen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar menos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostrar menos" + } + } + } + }, + "Shows the button prompts along the bottom of the screen.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shows the button prompts along the bottom of the screen." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra i suggerimenti dei pulsanti lungo il bordo inferiore dello schermo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche les indications des boutons en bas de l’écran." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt Tastenhinweise am unteren Bildschirmrand." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra las indicaciones de botones en la parte inferior de la pantalla." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostra as indicações dos botões na parte inferior da tela." + } + } + } + }, + "Shows the time in the top bar.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Shows the time in the top bar." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra l’ora nella barra superiore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Affiche l’heure dans la barre supérieure." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zeigt die Uhrzeit in der oberen Leiste." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Muestra la hora en la barra superior." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mostra a hora na barra superior." + } + } + } + }, + "Simulates the Nintendo Switch docked TV profile (1080p target, higher clocks and memory limits). Disabling uses handheld profile (720p target, lower GPU load).": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Simulates the Nintendo Switch docked TV profile (1080p target, higher clocks and memory limits). Disabling uses handheld profile (720p target, lower GPU load)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Simula il profilo TV di Nintendo Switch nel dock (obiettivo 1080p, frequenze e limiti di memoria più alti). Se disattivata, usa il profilo portatile (obiettivo 720p, carico GPU inferiore)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Simule le profil téléviseur de la Nintendo Switch sur sa station d’accueil (cible 1080p, fréquences et limites de mémoire plus élevées). Le désactiver utilise le profil portable (cible 720p, charge GPU réduite)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Simuliert das TV-Profil der Nintendo Switch im Dock (Zielauflösung 1080p, höhere Taktraten und Speichergrenzen). Deaktiviert wird das Handheld-Profil verwendet (Zielauflösung 720p, geringere GPU-Last)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Simula el perfil de Nintendo Switch conectada al televisor (objetivo de 1080p, frecuencias y límites de memoria más altos). Al desactivarlo, se usa el perfil portátil (objetivo de 720p, menor carga de GPU)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Simula o perfil do Nintendo Switch conectado à TV (resolução alvo de 1080p, frequências mais altas e limites maiores de memória). Ao desativar, usa o perfil portátil (resolução alvo de 720p, menor carga na GPU)." + } + } + } + }, + "Skip for now": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Skip for now" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salta per ora" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ignorer pour le moment" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vorerst überspringen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Omitir por ahora" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pular por enquanto" + } + } + } + }, + "Sky": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sky" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cielo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ciel" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Himmel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cielo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Céu" + } + } + } + }, + "Soft": { + "comment": "AppSettings.swift:104", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Soft" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lieve" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Léger" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sanft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Suave" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Suave" + } + } + } + }, + "Some Switch 2 games, such as Donkey Kong Bananza, enlarge their 3D picture inside the frame with an upscaler of their own before drawing the HUD. Game's own keeps it; MetalFX does the same step with Apple's scaler in a fraction of the GPU time, with Apple's look instead of the game's. MetalFX upscaling is a later step: it enlarges the finished picture to your screen. Both can be on. (DM_UPSCALER)": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Some Switch 2 games, such as Donkey Kong Bananza, enlarge their 3D picture inside the frame with an upscaler of their own before drawing the HUD. Game's own keeps it; MetalFX does the same step with Apple's scaler in a fraction of the GPU time, with Apple's look instead of the game's. MetalFX upscaling is a later step: it enlarges the finished picture to your screen. Both can be on. (DM_UPSCALER)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alcuni giochi Switch 2, come Donkey Kong Bananza, ingrandiscono l’immagine 3D all’interno del fotogramma con un proprio upscaler prima di disegnare l’HUD. Del gioco lo mantiene; MetalFX esegue lo stesso passaggio con lo scaler di Apple in una frazione del tempo GPU, con l’aspetto di Apple al posto di quello del gioco. Upscaling MetalFX interviene dopo: ingrandisce l’immagine finale fino alle dimensioni dello schermo. Entrambi possono essere attivi. (DM_UPSCALER)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certains jeux Switch 2, comme Donkey Kong Bananza, agrandissent leur image 3D dans le cadre avec leur propre système de mise à l’échelle avant de dessiner l’interface. Celui du jeu le conserve ; MetalFX effectue la même étape avec le système d’Apple en une fraction du temps GPU, avec le rendu d’Apple plutôt que celui du jeu. Mise à l’échelle MetalFX intervient ensuite : elle agrandit l’image finale à la taille de votre écran. Les deux peuvent être activés. (DM_UPSCALER)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Manche Switch 2-Spiele wie Donkey Kong Bananza vergrößern ihr 3D-Bild innerhalb des Frames mit einem eigenen Upscaler, bevor sie das HUD zeichnen. „Spieleigener“ behält ihn bei; „MetalFX“ führt denselben Schritt mit Apples Skalierung in einem Bruchteil der GPU-Zeit aus, mit Apples Bildwirkung statt der des Spiels. „MetalFX-Upscaling“ erfolgt später und vergrößert das fertige Bild auf deinen Bildschirm. Beide können aktiviert sein. (DM_UPSCALER)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Algunos juegos de Switch 2, como Donkey Kong Bananza, amplían su imagen 3D dentro del fotograma con su propio reescalador antes de dibujar el HUD. «Propio del juego» lo conserva; MetalFX hace ese mismo paso con el reescalador de Apple en una fracción del tiempo de GPU, con el aspecto de Apple en lugar del del juego. El reescalado con MetalFX es un paso posterior: amplía la imagen final a tu pantalla. Se pueden activar ambos. (DM_UPSCALER)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alguns jogos de Switch 2, como Donkey Kong Bananza, ampliam a imagem 3D dentro do quadro com um ampliador próprio antes de desenhar o HUD. Próprio do jogo mantém esse ampliador; MetalFX faz a mesma etapa com o ampliador da Apple em uma fração do tempo de GPU, com a aparência da Apple no lugar da do jogo. Ampliação com MetalFX é uma etapa posterior: amplia a imagem final para sua tela. Ambos podem ficar ativados. (DM_UPSCALER)" + } + } + } + }, + "Some files of %@ fail their hash check: an incomplete copy or a broken download.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Some files of %@ fail their hash check: an incomplete copy or a broken download." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alcuni file di %@ non superano la verifica dell’hash: una copia incompleta o un download danneggiato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certains fichiers de %@ échouent à la vérification d’empreinte : copie incomplète ou téléchargement endommagé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einige Dateien von %@ bestehen die Hash-Prüfung nicht: eine unvollständige Kopie oder ein fehlerhafter Download." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Algunos archivos de %@ no superan la comprobación de hash: una copia incompleta o una descarga dañada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alguns arquivos de %@ falham na verificação de hash: uma cópia incompleta ou um download corrompido." + } + } + } + }, + "Some files were not deleted": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Some files were not deleted" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alcuni file non sono stati eliminati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certains fichiers n’ont pas été supprimés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einige Dateien wurden nicht gelöscht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han eliminado algunos archivos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alguns arquivos não foram apagados" + } + } + } + }, + "Some files were skipped": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Some files were skipped" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alcuni file sono stati ignorati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certains fichiers ont été ignorés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einige Dateien wurden übersprungen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se han omitido algunos archivos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alguns arquivos foram ignorados" + } + } + } + }, + "Some folders are unavailable. Totals include readable files only. Connect their drives or check folder access, then recalculate.": { + "comment": "SettingsStorageSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Some folders are unavailable. Totals include readable files only. Connect their drives or check folder access, then recalculate." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alcune cartelle non sono disponibili. I totali includono solo i file leggibili. Collega le relative unità o controlla l’accesso alle cartelle, poi ricalcola." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certains dossiers sont indisponibles. Les totaux incluent uniquement les fichiers lisibles. Connectez leurs disques ou vérifiez l’accès aux dossiers, puis recalculez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einige Ordner sind nicht verfügbar. Die Summen berücksichtigen nur lesbare Dateien. Verbinde ihre Laufwerke oder prüfe den Ordnerzugriff und berechne dann neu." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Algunas carpetas no están disponibles. Los totales solo incluyen archivos legibles. Conecta sus unidades o comprueba el acceso a las carpetas y vuelve a calcular." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Algumas pastas estão indisponíveis. Os totais incluem apenas arquivos legíveis. Conecte as unidades ou verifique o acesso às pastas e recalcule." + } + } + } + }, + "Some game files are damaged": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Some game files are damaged" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alcuni file del gioco sono danneggiati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certains fichiers du jeu sont endommagés" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einige Spieldateien sind beschädigt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Algunos archivos del juego están dañados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alguns arquivos do jogo estão danificados" + } + } + } + }, + "Some games tie their physics, animation speeds, or game loops to 30 FPS. If gameplay runs twice as fast or audio desyncs, keep this at 30.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Some games tie their physics, animation speeds, or game loops to 30 FPS. If gameplay runs twice as fast or audio desyncs, keep this at 30." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alcuni giochi legano fisica, velocità delle animazioni o cicli di gioco ai 30 FPS. Se il gioco gira al doppio della velocità o l’audio perde la sincronizzazione, mantieni il valore a 30." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certains jeux lient leur physique, la vitesse des animations ou leurs boucles de jeu à 30 FPS. Si le jeu tourne deux fois trop vite ou si le son se décale, conservez 30." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bei manchen Spielen sind Physik, Animationsgeschwindigkeit oder Spielablauf an 30 FPS gebunden. Wenn das Spiel doppelt so schnell läuft oder der Ton nicht synchron ist, belasse den Wert bei 30." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Algunos juegos vinculan la física, la velocidad de las animaciones o sus ciclos a 30 FPS. Si el juego va al doble de velocidad o el audio se desincroniza, mantenlo en 30." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alguns jogos vinculam a física, a velocidade das animações ou os ciclos do jogo a 30 FPS. Se o jogo rodar duas vezes mais rápido ou o áudio perder a sincronia, mantenha em 30." + } + } + } + }, + "Some shaders broke in this engine": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Some shaders broke in this engine" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Alcuni shader non funzionano con questo motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Certains shaders ont échoué avec ce moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Einige Shader funktionieren mit dieser Engine nicht mehr" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Algunos shaders han fallado con este motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Alguns shaders falharam nesta engine" + } + } + } + }, + "Sort": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sort" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ordina" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trier" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sortieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ordenar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ordenar" + } + } + } + }, + "Sort by %@": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sort by %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ordina per %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Trier par %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sortieren nach %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ordenar por %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ordenar por %@" + } + } + } + }, + "Sound & music": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sound & music" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Suoni e musica" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Son et musique" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sound & Musik" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sonido y música" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Som e música" + } + } + } + }, + "Sound volume": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sound volume" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Volume dei suoni" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Volume sonore" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Klanglautstärke" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Volumen de sonido" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Volume dos sons" + } + } + } + }, + "Sounds/%@.wav": { + "comment": "SeleneImporter.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sounds/%@.wav" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sounds/%@.wav" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sounds/%@.wav" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sounds/%@.wav" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sounds/%@.wav" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sounds/%@.wav" + } + } + } + }, + "Sources": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sources" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sorgenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sources" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos de origen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos de origem" + } + } + } + }, + "Sources attached": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sources attached" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sorgenti collegate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sources associées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen zugeordnet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos de origen vinculados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos de origem vinculados" + } + } + } + }, + "Sources attached to %@; rebuild is now available if the toolchain gate allows it.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sources attached to %@; rebuild is now available if the toolchain gate allows it." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sorgenti collegate a %@; ora puoi ricompilare se il controllo della toolchain lo consente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sources associées à %@ ; la recompilation est désormais disponible si le contrôle de la chaîne d’outils l’autorise." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen für %@ zugeordnet; die App kann jetzt erneut erstellt werden, sofern die Toolchain-Prüfung dies zulässt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos de origen vinculados a %@; ya se puede recompilar si lo permite el control de la cadena de herramientas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos de origem vinculados a %@; agora é possível recompilar se a verificação do conjunto de ferramentas permitir." + } + } + } + }, + "Sources attached. Rebuild without touching saves.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sources attached. Rebuild without touching saves." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sorgenti collegate. Ricompila senza modificare i salvataggi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sources associées. Recompiler sans toucher aux sauvegardes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen zugeordnet. App erneut erstellen, ohne Spielstände zu verändern." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos de origen vinculados. Recompila sin modificar las partidas guardadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos de origem vinculados. Recompile sem alterar os dados salvos." + } + } + } + }, + "Sources detached": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sources detached" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sorgenti scollegate" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sources dissociées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellen getrennt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivos de origen desvinculados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivos de origem desvinculados" + } + } + } + }, + "Sources folder kept": { + "comment": "SettingsView.swift:282", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Sources folder kept" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cartella delle sorgenti conservata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dossier des sources conservé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Quellordner beibehalten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Carpeta de archivos de origen conservada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Pasta de arquivos de origem mantida" + } + } + } + }, + "Space": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Space" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spazio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Espace" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Weltraum" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Espacio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Espaço" + } + } + } + }, + "Spotlight": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Spotlight" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spotlight" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À la une" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spotlight" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Destacados" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Destaques" + } + } + } + }, + "Spotlight & Collection": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Spotlight & Collection" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spotlight e Raccolta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À la une et Collection" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spotlight & Sammlung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Destacados y Colección" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Destaques e Coleção" + } + } + } + }, + "Spotlight uses its own order.": { + "comment": "SettingsSpotlightSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Spotlight uses its own order." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spotlight usa un ordine proprio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À la une utilise son propre ordre." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spotlight verwendet eine eigene Reihenfolge." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Destacados usa su propio orden." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Destaques usa sua própria ordem." + } + } + } + }, + "Stage %@: %@": { + "comment": "BuildStageStepperView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stage %@: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Fase %@: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étape %@ : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Phase %@: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Etapa %@: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Etapa %@: %@" + } + } + } + }, + "Standard": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Standard" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Standard" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Standard" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Standard" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estándar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Padrão" + } + } + } + }, + "Start in fullscreen": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start in fullscreen" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avvia a schermo intero" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrer en plein écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Im Vollbild starten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Iniciar en pantalla completa" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Iniciar em tela cheia" + } + } + } + }, + "Start on": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start on" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Apri su" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écran de démarrage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Startansicht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pantalla inicial" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Abrir em" + } + } + } + }, + "Start-up": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start-up" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Start" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Inicialização" + } + } + } + }, + "Starting %@": { + "comment": "LaunchScreenView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Starting %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avvio di %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrage de %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ wird gestartet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Iniciando %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Iniciando %@" + } + } + } + }, + "Starting language": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Starting language" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lingua all’avvio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Langue au démarrage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sprache beim Start" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Idioma inicial" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Idioma inicial" + } + } + } + }, + "Starting tab": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Starting tab" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scheda iniziale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Onglet initial" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Startfilter" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pestaña inicial" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aba inicial" + } + } + } + }, + "Starting the porting…": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Starting the porting…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avvio della creazione dell’app per Mac…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrage de la création de l’app Mac…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Erstellung wird gestartet …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Iniciando la compilación…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Iniciando a compilação…" + } + } + } + }, + "Startup, setup and readiness.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Startup, setup and readiness." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Avvio, configurazione e stato di preparazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Démarrage, configuration et état de préparation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Start, Einrichtung und Bereitschaft." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Inicio, configuración y estado de preparación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Inicialização, configuração e prontidão." + } + } + } + }, + "SteamGridDB": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB" + } + } + } + }, + "SteamGridDB could not be reached. Check the connection and try again.": { + "comment": "SteamGridDBArt.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB could not be reached. Check the connection and try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile raggiungere SteamGridDB. Controlla la connessione e riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Impossible de joindre SteamGridDB. Vérifiez la connexion et réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB konnte nicht erreicht werden. Prüfe die Verbindung und versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido conectar con SteamGridDB. Comprueba la conexión e inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível acessar o SteamGridDB. Verifique a conexão e tente novamente." + } + } + } + }, + "SteamGridDB did not accept that key. Check it in Preferences → API in your SteamGridDB profile.": { + "comment": "SteamGridDBArt.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB did not accept that key. Check it in Preferences → API in your SteamGridDB profile." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB non ha accettato la chiave. Controllala in Preferences → API nel tuo profilo SteamGridDB." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB n’a pas accepté cette clé. Vérifiez-la dans Preferences → API dans votre profil SteamGridDB." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB hat diesen Schlüssel nicht akzeptiert. Prüfe ihn unter Preferences → API in deinem SteamGridDB-Profil." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB no ha aceptado esa clave. Compruébala en Preferences → API de tu perfil de SteamGridDB." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O SteamGridDB não aceitou essa chave. Verifique em Preferences → API no seu perfil do SteamGridDB." + } + } + } + }, + "SteamGridDB key": { + "comment": "SteamGridDBKeyDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB key" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiave SteamGridDB" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé SteamGridDB" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB-Schlüssel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clave de SteamGridDB" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chave do SteamGridDB" + } + } + } + }, + "SteamGridDB key saved; Change artwork searches SteamGridDB.": { + "comment": "AppViewModel+SteamGridDB.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB key saved; Change artwork searches SteamGridDB." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Chiave SteamGridDB salvata; Cambia grafica cerca su SteamGridDB." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Clé SteamGridDB enregistrée ; Changer l’illustration effectue une recherche sur SteamGridDB." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SteamGridDB-Schlüssel gespeichert; „Artwork ändern“ durchsucht SteamGridDB." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Clave de SteamGridDB guardada; «Cambiar imágenes» busca en SteamGridDB." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Chave do SteamGridDB salva; Alterar arte agora busca no SteamGridDB." + } + } + } + }, + "Step %@ of 3": { + "comment": "AddGameView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Step %@ of 3" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Passaggio %@ di 3" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étape %@ sur 3" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schritt %@ von 3" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Paso %@ de 3" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Etapa %@ de 3" + } + } + } + }, + "Step 1 of 2: Get ready": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Step 1 of 2: Get ready" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Passaggio 1 di 2: Preparazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étape 1 sur 2 : Préparation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schritt 1 von 2: Vorbereiten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Paso 1 de 2: Preparación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Etapa 1 de 2: Preparação" + } + } + } + }, + "Step 2 of 2: Your games": { + "comment": "YourGamesSetupView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Step 2 of 2: Your games" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Passaggio 2 di 2: I tuoi giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étape 2 sur 2 : Vos jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schritt 2 von 2: Deine Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Paso 2 de 2: Tus juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Etapa 2 de 2: Seus jogos" + } + } + } + }, + "Stick navigation deadzone": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stick navigation deadzone" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Zona morta dello stick per la navigazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Zone morte de navigation du stick" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Totzone für Stick-Navigation" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Zona muerta de navegación del stick" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Zona morta do analógico na navegação" + } + } + } + }, + "Still to do: %@.": { + "comment": "SetupCheckView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Still to do: %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ancora da fare: %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Reste à faire : %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch zu tun: %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pendiente: %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ainda falta: %@." + } + } + } + }, + "Stop following": { + "comment": "ConsoleView.swift:400", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stop following" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Smetti di seguire" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Arrêter le suivi" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht mehr folgen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Dejar de seguir" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Parar de acompanhar" + } + } + } + }, + "Storage": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Storage" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spazio di archiviazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Stockage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Speicher" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Almacenamiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Armazenamento" + } + } + } + }, + "Storage usage": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Storage usage" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Spazio occupato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisation du stockage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Speicherbelegung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Uso de almacenamiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Uso do armazenamento" + } + } + } + }, + "Storage warning: this build needs ~%@ free (twice the selected sources plus 10 GB), but only %@ is available. Free at least %@ before adding it.": { + "comment": "App-owned presentation copy; preserve data identifiers separately.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Storage warning: this build needs ~%@ free (twice the selected sources plus 10 GB), but only %@ is available. Free at least %@ before adding it." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attenzione allo spazio: questa build richiede circa %@ liberi (il doppio delle sorgenti selezionate più 10 GB), ma sono disponibili solo %@. Libera almeno %@ prima di aggiungerla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Attention au stockage : cette compilation nécessite environ %@ libres (deux fois les sources sélectionnées plus 10 Go), mais seuls %@ sont disponibles. Libérez au moins %@ avant de l’ajouter." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Speicherwarnung: Dieser Build benötigt etwa %@ freien Speicher (das Doppelte der ausgewählten Quelldateien plus 10 GB), aber nur %@ sind verfügbar. Gib mindestens %@ frei, bevor du ihn hinzufügst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Advertencia de espacio: esta compilación necesita unos %@ libres (el doble de los archivos seleccionados más 10 GB), pero solo hay %@ disponibles. Libera al menos %@ antes de añadirla." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aviso de espaço: esta compilação precisa de cerca de %@ livres (o dobro dos arquivos selecionados mais 10 GB), mas só há %@ disponíveis. Libere pelo menos %@ antes de adicioná-la." + } + } + } + }, + "Store page": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Store page" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Pagina nello store" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Page de la boutique" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Shop-Seite" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Página de la tienda" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Página na loja" + } + } + } + }, + "Stored prod.keys is invalid: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stored prod.keys is invalid: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file prod.keys memorizzato non è valido: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier prod.keys enregistré n’est pas valide : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die gespeicherte prod.keys ist ungültig: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El archivo prod.keys guardado no es válido: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O arquivo prod.keys armazenado é inválido: %@" + } + } + } + }, + "Stretch": { + "comment": "AppSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Stretch" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Allunga" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Étirer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Strecken" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estirar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esticar" + } + } + } + }, + "Summarizes the 4 essential gates: valid prod.keys, cloned nxnative repository, Python build environment, and certified Metal compiler version.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Summarizes the 4 essential gates: valid prod.keys, cloned nxnative repository, Python build environment, and certified Metal compiler version." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riepiloga i 4 requisiti essenziali: prod.keys valido, repository nxnative clonato, ambiente di compilazione Python e versione certificata del compilatore Metal." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Résume les 4 contrôles essentiels : prod.keys valide, dépôt nxnative cloné, environnement de compilation Python et version du compilateur Metal certifiée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fasst die 4 grundlegenden Voraussetzungen zusammen: gültige prod.keys, geklontes nxnative-Repository, Python-Build-Umgebung und zertifizierte Metal-Compiler-Version." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Resume los 4 requisitos esenciales: prod.keys válido, repositorio de nxnative clonado, entorno de compilación Python y versión del compilador Metal certificada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Resume as 4 verificações essenciais: prod.keys válido, repositório nxnative clonado, ambiente Python de compilação e versão certificada do compilador Metal." + } + } + } + }, + "Surprise me": { + "comment": "SpotlightRow.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Surprise me" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sorprendimi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Surprenez-moi" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Überrasch mich" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sorpréndeme" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Surpreenda-me" + } + } + } + }, + "Surprise me pool": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Surprise me pool" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Giochi per Sorprendimi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sélection pour Surprenez-moi" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auswahl für „Überrasch mich“" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Selección de Sorpréndeme" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Seleção do Surpreenda-me" + } + } + } + }, + "Switch": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Switch" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambia" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wechseln" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Trocar" + } + } + } + }, + "Switch 2 controllers": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Switch 2 controllers" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controller Switch 2" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Manettes Switch 2" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Switch 2-Controller" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mandos de Switch 2" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Controles de Switch 2" + } + } + } + }, + "Switch File": { + "comment": "GameFileItem.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Switch File" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "File Switch" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fichier Switch" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Switch-Datei" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Archivo de Switch" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Arquivo de Switch" + } + } + } + }, + "Switch box covers": { + "comment": "SettingsAboutSection.swift:20", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Switch box covers" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Copertine delle confezioni Switch" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jaquettes Switch" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Switch-Spiele-Cover" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Portadas de cajas de Switch" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Capas de caixas de jogos de Switch" + } + } + } + }, + "Switch game": { + "comment": "ChangeArtworkView.swift:297", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Switch game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "gioco Switch" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jeu Switch" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Switch-Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "juego de Switch" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo de Switch" + } + } + } + }, + "Switching the engine…": { + "comment": "SettingsEngineSection.swift:317", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Switching the engine…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cambio del motore…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changement de moteur…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine wird gewechselt …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiando el motor…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Trocando a engine…" + } + } + } + }, + "Switching…": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Switching…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Passaggio in corso…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Changement…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wird gewechselt …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cambiando…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Trocando…" + } + } + } + }, + "THIS WEEK": { + "comment": "DetailsBands.swift:70", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "THIS WEEK" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "QUESTA SETTIMANA" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CETTE SEMAINE" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "DIESE WOCHE" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ESTA SEMANA" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "NESTA SEMANA" + } + } + } + }, + "TYPE WHAT YOU'RE LOOKING FOR, THEN RETURN": { + "comment": "OnlineBrowsePanel.swift:145", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "TYPE WHAT YOU'RE LOOKING FOR, THEN RETURN" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "DIGITA CIÒ CHE CERCHI, POI PREMI INVIO" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "SAISISSEZ VOTRE RECHERCHE, PUIS APPUYEZ SUR RETOUR" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "SUCHBEGRIFF EINGEBEN, DANN RETURN DRÜCKEN" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ESCRIBE LO QUE BUSCAS Y PULSA RETORNO" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "DIGITE O QUE PROCURA E PRESSIONE RETURN" + } + } + } + }, + "Text readability": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Text readability" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Leggibilità del testo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lisibilité du texte" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Textlesbarkeit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Legibilidad del texto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Legibilidade do texto" + } + } + } + }, + "Text shadow": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Text shadow" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ombra del testo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ombre du texte" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Textschatten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sombra del texto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sombra do texto" + } + } + } + }, + "That file could not be read.": { + "comment": "ChangeArtworkView.swift:664", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "That file could not be read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile leggere il file." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce fichier n’a pas pu être lu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Datei konnte nicht gelesen werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido leer ese archivo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível ler esse arquivo." + } + } + } + }, + "That file is not an image NX Library can read.": { + "comment": "ArtworkPicker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "That file is not an image NX Library can read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file non è un’immagine leggibile da NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce fichier n’est pas une image lisible par NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Datei ist kein für NX Library lesbares Bild." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ese archivo no es una imagen que NX Library pueda leer." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esse arquivo não é uma imagem que o NX Library consiga ler." + } + } + } + }, + "That folder is not a Selene checkout. Pick the folder that contains theme/theme.yaml.": { + "comment": "SeleneImporter.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "That folder is not a Selene checkout. Pick the folder that contains theme/theme.yaml." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Quella cartella non è un checkout di Selene. Scegli la cartella che contiene theme/theme.yaml." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce dossier n’est pas une copie de travail de Selene. Choisissez le dossier contenant theme/theme.yaml." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Ordner ist kein Selene-Checkout. Wähle den Ordner, der theme/theme.yaml enthält." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esa carpeta no es una copia local de Selene. Elige la carpeta que contiene theme/theme.yaml." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Essa pasta não é um checkout do Selene. Escolha a pasta que contém theme/theme.yaml." + } + } + } + }, + "That is not a SteamGridDB API key. Copy it from Preferences → API in your SteamGridDB profile.": { + "comment": "SteamGridDBArt.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "That is not a SteamGridDB API key. Copy it from Preferences → API in your SteamGridDB profile." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è una chiave API di SteamGridDB. Copiala da Preferences → API nel tuo profilo SteamGridDB." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce n’est pas une clé API SteamGridDB. Copiez-la depuis Preferences → API dans votre profil SteamGridDB." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das ist kein SteamGridDB-API-Schlüssel. Kopiere ihn aus Preferences → API in deinem SteamGridDB-Profil." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esa no es una clave de API de SteamGridDB. Cópiala de Preferences → API de tu perfil de SteamGridDB." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Essa não é uma chave de API do SteamGridDB. Copie-a de Preferences → API no seu perfil do SteamGridDB." + } + } + } + }, + "The Game apps folder is unavailable. Reconnect it or choose another in Settings → Library.": { + "comment": "GameAppDestination.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The Game apps folder is unavailable. Reconnect it or choose another in Settings → Library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella App dei giochi non è disponibile. Ricollegala o scegline un’altra in Impostazioni → Libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier Apps de jeux est indisponible. Reconnectez-le ou choisissez-en un autre dans Réglages → Bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Spiele-App-Ordner ist nicht verfügbar. Verbinde ihn erneut oder wähle unter Einstellungen → Bibliothek einen anderen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta Apps de juegos no está disponible. Vuelve a conectarla o elige otra en Ajustes → Biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta de Apps de jogos está indisponível. Reconecte ou escolha outra em Ajustes → Biblioteca." + } + } + } + }, + "The Nintendo Store URL is invalid.": { + "comment": "NintendoStoreMetadata.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The Nintendo Store URL is invalid." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’URL del Nintendo Store non è valido." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’URL du Nintendo Store n’est pas valide." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Nintendo Store-URL ist ungültig." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La URL de Nintendo Store no es válida." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O URL da Nintendo Store é inválido." + } + } + } + }, + "The app bundle is missing: %@": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The app bundle is missing: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il bundle dell’app non è disponibile: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le paquet de l’app est introuvable : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das App-Bundle fehlt: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta el paquete de la app: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O pacote do app não foi encontrado: %@" + } + } + } + }, + "The app could not be signed again, so it was left as it was: %@": { + "comment": "AppIconWriter.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The app could not be signed again, so it was left as it was: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile firmare di nuovo l’app, che è stata quindi lasciata invariata: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’app n’a pas pu être signée à nouveau et a donc été laissée en l’état : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App konnte nicht erneut signiert werden und bleibt daher unverändert: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido volver a firmar la app, por lo que sigue como estaba: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível assinar o app novamente, então ele foi mantido como estava: %@" + } + } + } + }, + "The app folder is read-only: %@. Allow writing here before building.": { + "comment": "GameAppDestination.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The app folder is read-only: %@. Allow writing here before building." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella delle app è di sola lettura: %@. Consenti la scrittura qui prima di compilare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier de l’app est en lecture seule : %@. Autorisez l’écriture avant de compiler." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der App-Ordner ist schreibgeschützt: %@. Erlaube vor dem Erstellen von Apps den Schreibzugriff." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta de apps es de solo lectura: %@. Permite escribir en ella antes de compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta do app é somente leitura: %@. Permita a gravação aqui antes de compilar." + } + } + } + }, + "The app icon was not changed: %@": { + "comment": "AppIconProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The app icon was not changed: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’icona dell’app non è stata modificata: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’icône de l’app n’a pas été modifiée : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das App-Symbol wurde nicht geändert: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha cambiado el icono de la app: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O ícone do app não foi alterado: %@" + } + } + } + }, + "The app ships no keys, ROMs, game art or Nintendo UI assets.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The app ships no keys, ROMs, game art or Nintendo UI assets." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’app non include chiavi, ROM, immagini dei giochi o risorse dell’interfaccia Nintendo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’app ne contient aucune clé, ROM, illustration de jeu ou ressource d’interface Nintendo." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App enthält keine Schlüssel, ROMs, Spielgrafiken oder Nintendo-UI-Assets." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La app no incluye claves, ROM, imágenes de juegos ni recursos de interfaz de Nintendo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O app não inclui chaves, ROMs, artes de jogos ou elementos da interface da Nintendo." + } + } + } + }, + "The app signature could not be verified; nothing was changed.": { + "comment": "AppUpdater.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The app signature could not be verified; nothing was changed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile verificare la firma dell’app; non è stato modificato nulla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La signature de l’app n’a pas pu être vérifiée ; aucune modification n’a été effectuée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App-Signatur konnte nicht überprüft werden; es wurde nichts geändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido verificar la firma de la app; no se ha cambiado nada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível verificar a assinatura do app; nada foi alterado." + } + } + } + }, + "The app was installed atomically. Your library now points at the new build.": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The app was installed atomically. Your library now points at the new build." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’app è stata installata con un’operazione atomica. La libreria ora punta alla nuova compilazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’app a été installée de façon atomique. Votre bibliothèque pointe désormais vers la nouvelle compilation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App wurde atomar installiert. Deine Bibliothek verweist jetzt auf den neuen Build." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La app se ha instalado de forma atómica. La biblioteca ahora apunta a la nueva compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O app foi instalado de forma atômica. Sua biblioteca agora aponta para a nova compilação." + } + } + } + }, + "The backup folder could not be opened.": { + "comment": "AppViewModel+Saves.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The backup folder could not be opened." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile aprire la cartella dei backup." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier des copies de sauvegarde n’a pas pu être ouvert." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Backup-Ordner konnte nicht geöffnet werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido abrir la carpeta de copias de seguridad." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível abrir a pasta de backups." + } + } + } + }, + "The base game is required. Updates and DLC are optional.": { + "comment": "DropZoneView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The base game is required. Updates and DLC are optional." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il gioco base è obbligatorio. Aggiornamenti e DLC sono facoltativi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le jeu de base est requis. Les mises à jour et les DLC sont facultatifs." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Basisspiel ist erforderlich. Updates und DLC sind optional." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se requiere el juego base. Las actualizaciones y los DLC son opcionales." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O jogo base é obrigatório. Atualizações e DLCs são opcionais." + } + } + } + }, + "The base stays in. One update at a time; bundled DLC stay together.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The base stays in. One update at a time; bundled DLC stay together." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il gioco base resta incluso. Un aggiornamento alla volta; i DLC inclusi restano insieme." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le jeu de base reste inclus. Une seule mise à jour à la fois ; les DLC groupés restent ensemble." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Basisspiel bleibt ausgewählt. Nur ein Update gleichzeitig; gebündelte DLC bleiben zusammen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El juego base se mantiene. Una actualización a la vez; los DLC incluidos se mantienen juntos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O jogo base é mantido. Uma atualização por vez; os DLCs incluídos ficam juntos." + } + } + } + }, + "The build cache was also removed.": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The build cache was also removed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "È stata rimossa anche la cache di build." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le cache de compilation a également été supprimé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Build-Cache wurde ebenfalls entfernt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "También se eliminó la caché de compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O cache de compilação também foi removido." + } + } + } + }, + "The build environment is incomplete.": { + "comment": "AddGameView+Rows.swift:177", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The build environment is incomplete." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’ambiente di compilazione è incompleto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’environnement de compilation est incomplet." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Build-Umgebung ist unvollständig." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El entorno de compilación está incompleto." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O ambiente de compilação está incompleto." + } + } + } + }, + "The build environment is incomplete: %@": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The build environment is incomplete: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’ambiente di compilazione è incompleto: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’environnement de compilation est incomplet : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Build-Umgebung ist unvollständig: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El entorno de compilación está incompleto: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O ambiente de compilação está incompleto: %@" + } + } + } + }, + "The build ran as Intel code": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The build ran as Intel code" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La compilazione ha eseguito codice Intel" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La compilation a été exécutée en code Intel" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Build lief als Intel-Code" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La compilación se ha ejecutado como código Intel" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A compilação foi executada como código Intel" + } + } + } + }, + "The built app is invalid: %@": { + "comment": "BuildOutputInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The built app is invalid: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’app compilata non è valida: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’app compilée n’est pas valide : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die erstellte App ist ungültig: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La app compilada no es válida: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O app compilado é inválido: %@" + } + } + } + }, + "The cache folder could not be safely identified. Nothing was deleted.": { + "comment": "StorageManager.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The cache folder could not be safely identified. Nothing was deleted." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile identificare in sicurezza la cartella della cache. Non è stato eliminato nulla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier du cache n’a pas pu être identifié de manière sûre. Rien n’a été supprimé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Cache-Ordner konnte nicht sicher identifiziert werden. Es wurde nichts gelöscht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido identificar la carpeta de caché de forma segura. No se ha eliminado nada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível identificar a pasta de cache com segurança. Nada foi apagado." + } + } + } + }, + "The cheat files for build %@ go to the Trash.": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The cheat files for build %@ go to the Trash." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file dei trucchi per la build %@ vengono spostati nel Cestino." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers de codes de triche du build %@ sont placés dans la corbeille." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Cheat-Dateien für Build %@ werden in den Papierkorb gelegt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos de trucos de la compilación %@ se mueven a la Papelera." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos de trapaças da build %@ vão para o Lixo." + } + } + } + }, + "The cheat files were not changed: %@.": { + "comment": "CheatsPanel.swift:387", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The cheat files were not changed: %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file dei trucchi non sono stati modificati: %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers de codes de triche n’ont pas été modifiés : %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Cheat-Dateien wurden nicht geändert: %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han cambiado los archivos de trucos: %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos de trapaças não foram alterados: %@." + } + } + } + }, + "The chosen folder for save sync; choosing a folder enables it.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The chosen folder for save sync; choosing a folder enables it." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella scelta per sincronizzare i salvataggi; scegliere una cartella attiva la sincronizzazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier choisi pour la synchronisation des sauvegardes ; choisir un dossier l’active." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der gewählte Ordner für die Spielstandsynchronisierung; die Ordnerauswahl aktiviert sie." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta elegida para sincronizar partidas; elegir una carpeta activa la sincronización." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta escolhida para sincronizar dados salvos; escolher uma pasta ativa a sincronização." + } + } + } + }, + "The connected controller and how its buttons map.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The connected controller and how its buttons map." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il controller connesso e la mappatura dei suoi pulsanti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La manette connectée et la correspondance de ses boutons." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der verbundene Controller und seine Tastenbelegung." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El mando conectado y la asignación de sus botones." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O controle conectado e o mapeamento dos seus botões." + } + } + } + }, + "The disk filled up during the porting.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The disk filled up during the porting." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il disco si è riempito durante la creazione dell’app per Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le disque s’est rempli pendant la création de l’app Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Während der App-Erstellung war der Datenträger voll." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El disco se ha llenado durante la compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O disco ficou cheio durante a compilação." + } + } + } + }, + "The download did not match the release; nothing was changed.": { + "comment": "AppUpdater.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The download did not match the release; nothing was changed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file scaricato non corrisponde alla versione pubblicata; non è stato modificato nulla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le téléchargement ne correspond pas à la version publiée ; aucune modification n’a été effectuée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Download stimmt nicht mit der Veröffentlichung überein; es wurde nichts geändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La descarga no coincide con la versión publicada; no se ha cambiado nada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O download não corresponde à versão publicada; nada foi alterado." + } + } + } + }, + "The download didn't finish.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The download didn't finish." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il download non è terminato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le téléchargement ne s’est pas terminé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Download wurde nicht abgeschlossen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La descarga no ha terminado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O download não foi concluído." + } + } + } + }, + "The download is not a valid NX Library %@; nothing was changed.": { + "comment": "AppUpdater.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The download is not a valid NX Library %@; nothing was changed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file scaricato non è una copia valida di NX Library %@; non è stato modificato nulla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le téléchargement n’est pas une copie valide de NX Library %@ ; aucune modification n’a été effectuée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Download ist keine gültige Version von NX Library %@; es wurde nichts geändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La descarga no es una copia válida de NX Library %@; no se ha cambiado nada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O download não é uma cópia válida do NX Library %@; nada foi alterado." + } + } + } + }, + "The download was damaged. Try again.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The download was damaged. Try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file scaricato è danneggiato. Riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le téléchargement est endommagé. Réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Download war beschädigt. Versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La descarga está dañada. Inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O download está corrompido. Tente novamente." + } + } + } + }, + "The engine and compiler do not match the certified combination. Builds may fail or show incorrect graphics and will be marked as untested.": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine and compiler do not match the certified combination. Builds may fail or show incorrect graphics and will be marked as untested." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il motore e il compilatore non corrispondono alla combinazione certificata. Le compilazioni potrebbero non riuscire o mostrare errori grafici e saranno contrassegnate come non testate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le moteur et le compilateur ne correspondent pas à la combinaison certifiée. Les compilations peuvent échouer ou présenter des erreurs graphiques et seront marquées comme non testées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Engine und Compiler entsprechen nicht der zertifizierten Kombination. Builds können fehlschlagen oder fehlerhafte Grafik anzeigen und werden als ungetestet markiert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El motor y el compilador no coinciden con la combinación certificada. Las compilaciones podrían fallar o mostrar gráficos incorrectos y se marcarán como no probadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A engine e o compilador não correspondem à combinação certificada. As compilações podem falhar ou exibir gráficos incorretos e serão marcadas como não testadas." + } + } + } + }, + "The engine commit is not a valid hash.": { + "comment": "UpstreamChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine commit is not a valid hash." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il commit del motore non è un hash valido." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le commit du moteur n’est pas une empreinte valide." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Engine-Commit ist kein gültiger Hash." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El commit del motor no es un hash válido." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O commit da engine não é um hash válido." + } + } + } + }, + "The engine could not be switched: %@": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine could not be switched: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile cambiare il motore: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le moteur n’a pas pu être changé : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Engine konnte nicht gewechselt werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido cambiar de motor: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível trocar a engine: %@" + } + } + } + }, + "The engine folder has changed files. NX Library never touches them: put them aside, then try again.": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine folder has changed files. NX Library never touches them: put them aside, then try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella del motore contiene file modificati. NX Library non li tocca mai: mettili da parte, poi riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier du moteur contient des fichiers modifiés. NX Library n’y touche jamais : mettez-les de côté, puis réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Engine-Ordner enthält geänderte Dateien. NX Library verändert sie niemals: Lege sie beiseite und versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta del motor contiene archivos modificados. NX Library nunca los modifica: apártalos e inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta da engine tem arquivos alterados. O NX Library nunca os modifica: guarde-os em outro local e tente novamente." + } + } + } + }, + "The engine has no profile for this game, so it makes a provisional one. ": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine has no profile for this game, so it makes a provisional one. " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il motore non ha un profilo per questo gioco, quindi ne crea uno provvisorio. " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le moteur n’a pas de profil pour ce jeu ; il en crée donc un provisoire. " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Engine hat kein Profil für dieses Spiel und erstellt daher ein vorläufiges. " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El motor no tiene un perfil para este juego, así que crea uno provisional. " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A engine não tem um perfil para este jogo, então cria um provisório. " + } + } + } + }, + "The engine here is at %@, not the %@ this NX Library is tested with. Switch moves it to %@. Games you built keep playing.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine here is at %@, not the %@ this NX Library is tested with. Switch moves it to %@. Games you built keep playing." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il motore qui è al commit %@, diverso da %@ con cui è testata questa NX Library. Con Cambia lo porti a %@. I giochi già compilati continuano a funzionare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le moteur présent est au commit %@, et non au commit %@ avec lequel cette version de NX Library a été testée. Changer le fait passer au commit %@. Les jeux déjà compilés restent jouables." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Engine hier steht auf %@ statt auf %@, womit diese NX Library-Version getestet wurde. „Wechseln“ setzt sie auf %@. Bereits erstellte Spiele bleiben spielbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El motor local está en %@, en lugar de %@, con el que se ha probado este NX Library. Cambiar lo mueve a %@. Los juegos ya compilados siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A engine aqui está em %@, e não em %@, com o qual este NX Library foi testado. Trocar a move para %@. Seus jogos compilados continuam funcionando." + } + } + } + }, + "The engine needs the zbic package to read this game's code.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine needs the zbic package to read this game's code." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il motore richiede il pacchetto zbic per leggere il codice di questo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le moteur nécessite le paquet zbic pour lire le code de ce jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Engine benötigt das Paket zbic, um den Code dieses Spiels zu lesen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El motor necesita el paquete zbic para leer el código de este juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A engine precisa do pacote zbic para ler o código deste jogo." + } + } + } + }, + "The engine stopped without a message NX Library recognises.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine stopped without a message NX Library recognises." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il motore si è interrotto senza un messaggio riconosciuto da NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le moteur s’est arrêté sans message reconnu par NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Engine wurde ohne eine für NX Library erkennbare Meldung gestoppt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El motor se ha detenido sin un mensaje que NX Library reconozca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A engine parou sem uma mensagem que o NX Library reconheça." + } + } + } + }, + "The engine's shader translator is missing or damaged.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The engine's shader translator is missing or damaged." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il traduttore degli shader del motore manca o è danneggiato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le traducteur de shaders du moteur est absent ou endommagé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Shader-Übersetzer der Engine fehlt oder ist beschädigt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta el traductor de shaders del motor o está dañado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O tradutor de shaders da engine está ausente ou danificado." + } + } + } + }, + "The files changed since the preview. Choose the folder again.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The files changed since the preview. Choose the folder again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file sono cambiati dall’anteprima. Scegli di nuovo la cartella." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers ont changé depuis l’aperçu. Choisissez à nouveau le dossier." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Dateien wurden seit der Vorschau geändert. Wähle den Ordner erneut aus." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos han cambiado desde la vista previa. Vuelve a elegir la carpeta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos mudaram desde a prévia. Escolha a pasta novamente." + } + } + } + }, + "The files on disk are not changed.": { + "comment": "AddGameView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The files on disk are not changed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file sul disco non vengono modificati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers sur le disque ne sont pas modifiés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Dateien auf dem Datenträger werden nicht geändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se modifican los archivos del disco." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos no disco não serão alterados." + } + } + } + }, + "The filter Spotlight and Collection open on.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The filter Spotlight and Collection open on." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il filtro con cui si aprono Spotlight e Raccolta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le filtre à l’ouverture d’À la une et de Collection." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Filter, mit dem Spotlight und Sammlung geöffnet werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El filtro con el que se abren Destacados y Colección." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O filtro inicial de Destaques e Coleção." + } + } + } + }, + "The first start waits a few minutes for it, once.": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The first start waits a few minutes for it, once." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Al primo avvio richiede qualche minuto di attesa, una sola volta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le premier démarrage attend quelques minutes, une seule fois." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der erste Start wartet dafür einmalig einige Minuten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El primer inicio espera unos minutos para completarla, una sola vez." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Na primeira abertura, aguarda alguns minutos por isso, uma única vez." + } + } + } + }, + "The folder holds more than one game": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The folder holds more than one game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella contiene più di un gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier contient plusieurs jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Ordner enthält mehrere Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta contiene más de un juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta contém mais de um jogo" + } + } + } + }, + "The folder is unavailable. Connect its drive and check folder access, then recalculate storage.": { + "comment": "StorageManager.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The folder is unavailable. Connect its drive and check folder access, then recalculate storage." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella non è disponibile. Collega la relativa unità e controlla l’accesso alla cartella, poi ricalcola lo spazio occupato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier est indisponible. Connectez son disque et vérifiez l’accès au dossier, puis recalculez le stockage." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Ordner ist nicht verfügbar. Verbinde sein Laufwerk, prüfe den Ordnerzugriff und berechne den Speicherbedarf erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta no está disponible. Conecta su unidad y comprueba el acceso a la carpeta; después vuelve a calcular el almacenamiento." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta está indisponível. Conecte a unidade e verifique o acesso à pasta, depois recalcule o armazenamento." + } + } + } + }, + "The folder the game reads its cheats from.": { + "comment": "CheatsPanel.swift:296", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The folder the game reads its cheats from." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella da cui il gioco legge i trucchi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier dans lequel le jeu lit ses codes de triche." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Ordner, aus dem das Spiel seine Cheats liest." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta de la que el juego lee sus trucos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta de onde o jogo lê as trapaças." + } + } + } + }, + "The full image could not be downloaded from %@. Try again.": { + "comment": "ChangeArtworkView.swift:628", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The full image could not be downloaded from %@. Try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile scaricare l’immagine completa da %@. Riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’image complète n’a pas pu être téléchargée depuis %@. Réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das vollständige Bild konnte nicht von %@ geladen werden. Versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido descargar la imagen completa de %@. Inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível baixar a imagem completa de %@. Tente novamente." + } + } + } + }, + "The game does not have this language": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game does not have this language" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il gioco non include questa lingua" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le jeu ne propose pas cette langue" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Spiel bietet diese Sprache nicht an" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El juego no incluye este idioma" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O jogo não tem este idioma" + } + } + } + }, + "The game has no title ID. Choose a valid NSP, XCI, NSZ or XCZ source.": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game has no title ID. Choose a valid NSP, XCI, NSZ or XCZ source." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il gioco non ha un title ID. Scegli una sorgente NSP, XCI, NSZ o XCZ valida." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le jeu n’a pas de title ID. Choisissez une source NSP, XCI, NSZ ou XCZ valide." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Spiel hat keine title ID. Wähle eine gültige NSP-, XCI-, NSZ- oder XCZ-Quelle." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El juego no tiene title ID. Elige un archivo de origen NSP, XCI, NSZ o XCZ válido." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O jogo não tem title ID. Escolha uma origem NSP, XCI, NSZ ou XCZ válida." + } + } + } + }, + "The game prepares its shaders the first time it needs them, so its first start can take longer. ": { + "comment": "BuildWarningCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game prepares its shaders the first time it needs them, so its first start can take longer. " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il gioco prepara gli shader quando servono per la prima volta, quindi il primo avvio può richiedere più tempo. " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le jeu prépare ses shaders lorsqu’il en a besoin pour la première fois ; son premier démarrage peut donc prendre plus de temps. " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Spiel bereitet seine Shader vor, sobald es sie zum ersten Mal benötigt. Daher kann der erste Start länger dauern. " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El juego prepara los shaders la primera vez que los necesita, por lo que el primer inicio puede tardar más. " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O jogo prepara os shaders quando precisa deles pela primeira vez, então a primeira abertura pode demorar mais. " + } + } + } + }, + "The game stops loading it. You can put it back from the Trash.": { + "comment": "ModsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game stops loading it. You can put it back from the Trash." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il gioco smetterà di caricarla. Puoi ripristinarla dal Cestino." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le jeu cessera de le charger. Vous pourrez le récupérer dans la corbeille." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Spiel lädt diesen Mod dann nicht mehr. Du kannst ihn aus dem Papierkorb zurückholen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El juego dejará de cargarlo. Puedes recuperarlo de la Papelera." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O jogo deixará de carregá-lo. Você pode restaurá-lo do Lixo." + } + } + } + }, + "The game's app was not found.": { + "comment": "AppIconWriter.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game's app was not found." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’app del gioco non è stata trovata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’app du jeu est introuvable." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App des Spiels wurde nicht gefunden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha encontrado la app del juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O app do jogo não foi encontrado." + } + } + } + }, + "The game's own 3D upscaler, or MetalFX instead": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game's own 3D upscaler, or MetalFX instead" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’upscaler 3D del gioco oppure MetalFX al suo posto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le système de mise à l’échelle 3D du jeu, ou MetalFX à sa place" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der eigene 3D-Upscaler des Spiels oder stattdessen MetalFX" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El reescalador 3D propio del juego o MetalFX en su lugar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O ampliador 3D do próprio jogo ou o MetalFX" + } + } + } + }, + "The game's own art, then Title DB": { + "comment": "ChangeArtworkView.swift:436", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game's own art, then Title DB" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le immagini del gioco, poi Title DB" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’illustration du jeu, puis Title DB" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieleigenes Artwork, danach Title DB" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las imágenes propias del juego y, si no están disponibles, Title DB" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A arte do próprio jogo e, se não houver, Title DB" + } + } + } + }, + "The game's own icon, then Title DB": { + "comment": "ChangeArtworkView.swift:437", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game's own icon, then Title DB" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’icona del gioco, poi Title DB" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’icône du jeu, puis Title DB" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Spieleigenes Symbol, danach Title DB" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El icono propio del juego y, si no está disponible, Title DB" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O ícone do próprio jogo e, se não houver, Title DB" + } + } + } + }, + "The game's save configuration could not be read.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The game's save configuration could not be read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile leggere la configurazione dei salvataggi del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La configuration des sauvegardes du jeu n’a pas pu être lue." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Spielstandkonfiguration des Spiels konnte nicht gelesen werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido leer la configuración de partidas guardadas del juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível ler a configuração de dados salvos do jogo." + } + } + } + }, + "The icon appearance in the Dock.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The icon appearance in the Dock." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’aspetto dell’icona nel Dock." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’apparence de l’icône dans le Dock." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Erscheinungsbild des Symbols im Dock." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El aspecto del icono en el Dock." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A aparência do ícone no Dock." + } + } + } + }, + "The icon could not be made: %@": { + "comment": "AppIconWriter.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The icon could not be made: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile creare l’icona: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’icône n’a pas pu être créée : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Symbol konnte nicht erstellt werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido crear el icono: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível criar o ícone: %@" + } + } + } + }, + "The icon is not an image NX Library can read.": { + "comment": "AppIconWriter.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The icon is not an image NX Library can read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’icona non è un’immagine leggibile da NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’icône n’est pas une image lisible par NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Symbol ist kein für NX Library lesbares Bild." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El icono no es una imagen que NX Library pueda leer." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O ícone não é uma imagem que o NX Library consiga ler." + } + } + } + }, + "The key could not be saved on this Mac.": { + "comment": "SteamGridDBArt.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The key could not be saved on this Mac." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile salvare la chiave su questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La clé n’a pas pu être enregistrée sur ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Schlüssel konnte auf diesem Mac nicht gespeichert werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido guardar la clave en este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível salvar a chave neste Mac." + } + } + } + }, + "The key file could not be imported. Choose a valid prod.keys file.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The key file could not be imported. Choose a valid prod.keys file." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile importare il file delle chiavi. Scegli un file prod.keys valido." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier de clés n’a pas pu être importé. Choisissez un fichier prod.keys valide." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Schlüsseldatei konnte nicht importiert werden. Wähle eine gültige prod.keys-Datei." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido importar el archivo de claves. Elige un archivo prod.keys válido." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível importar o arquivo de chaves. Escolha um arquivo prod.keys válido." + } + } + } + }, + "The key file could not be imported. Your previous copy is kept.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The key file could not be imported. Your previous copy is kept." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile importare il file delle chiavi. La copia precedente viene conservata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier de clés n’a pas pu être importé. Votre copie précédente est conservée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Schlüsseldatei konnte nicht importiert werden. Deine bisherige Kopie bleibt erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido importar el archivo de claves. Se conserva la copia anterior." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível importar o arquivo de chaves. A cópia anterior foi mantida." + } + } + } + }, + "The key file could not be read.": { + "comment": "KeyStore.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The key file could not be read." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile leggere il file delle chiavi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier de clés n’a pas pu être lu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Schlüsseldatei konnte nicht gelesen werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido leer el archivo de claves." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível ler o arquivo de chaves." + } + } + } + }, + "The key file has no header_key.": { + "comment": "KeyStore.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The key file has no header_key." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file delle chiavi non contiene header_key." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier de clés ne contient pas de header_key." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Schlüsseldatei enthält keinen header_key." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El archivo de claves no contiene header_key." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O arquivo de chaves não contém header_key." + } + } + } + }, + "The last porting failed": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The last porting failed" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’ultima creazione dell’app per Mac non è riuscita" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La dernière création de l’app Mac a échoué" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die letzte App-Erstellung ist fehlgeschlagen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La última compilación ha fallado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A última compilação falhou" + } + } + } + }, + "The last session closed unexpectedly with cheats on. Next start, cheats begin off; ": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The last session closed unexpectedly with cheats on. Next start, cheats begin off; " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’ultima sessione si è chiusa in modo imprevisto con i trucchi attivi. Al prossimo avvio i trucchi saranno disattivati; " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La dernière session s’est fermée inopinément avec des codes de triche activés. Au prochain démarrage, ils seront désactivés ; " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die letzte Sitzung wurde mit aktiven Cheats unerwartet beendet. Beim nächsten Start sind Cheats zunächst deaktiviert; " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La última sesión se cerró inesperadamente con trucos activados. En el próximo inicio, los trucos empezarán desactivados; " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A última sessão foi encerrada inesperadamente com trapaças ativadas. Na próxima abertura, as trapaças começam desativadas; " + } + } + } + }, + "The library is in a cloud folder": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The library is in a cloud folder" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La libreria si trova in una cartella cloud" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La bibliothèque se trouve dans un dossier cloud" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Bibliothek liegt in einem Cloud-Ordner" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La biblioteca está en una carpeta en la nube" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A biblioteca está em uma pasta na nuvem" + } + } + } + }, + "The log file on disk is kept; only the lines shown here go.": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The log file on disk is kept; only the lines shown here go." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file di log sul disco viene conservato; vengono eliminate solo le righe mostrate qui." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier journal sur le disque est conservé ; seules les lignes affichées ici sont effacées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Protokolldatei auf dem Datenträger bleibt erhalten; nur die hier angezeigten Zeilen werden entfernt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se conserva el archivo de registro del disco; solo se borran las líneas que se muestran aquí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O arquivo de log no disco será mantido; apenas as linhas exibidas aqui serão removidas." + } + } + } + }, + "The log has the reason; rebuilding tries again.": { + "comment": "BuildWarningCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The log has the reason; rebuilding tries again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il log riporta il motivo; ricompilando si riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le journal en indique la raison ; recompiler réessaie l’opération." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Grund steht im Protokoll; beim erneuten Erstellen wird es noch einmal versucht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El registro indica el motivo; al recompilar se vuelve a intentar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O motivo está no log; recompilar faz uma nova tentativa." + } + } + } + }, + "The mod couldn't be copied into the game's folder. Try again.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The mod couldn't be copied into the game's folder. Try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile copiare la mod nella cartella del gioco. Riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le mod n’a pas pu être copié dans le dossier du jeu. Réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Mod konnte nicht in den Spielordner kopiert werden. Versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido copiar el mod en la carpeta del juego. Inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível copiar o mod para a pasta do jogo. Tente novamente." + } + } + } + }, + "The moonlight's beam and motes move and a live background plays; off, they hold still.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The moonlight's beam and motes move and a live background plays; off, they hold still." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il fascio di luce lunare e le particelle si muovono e lo sfondo animato viene riprodotto; se disattivato, restano fermi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le faisceau du clair de lune et les particules bougent, et l’arrière-plan animé est lu ; désactivé, ils restent immobiles." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Mondlichtstrahl und die Partikel bewegen sich und ein Live-Hintergrund wird abgespielt; deaktiviert bleiben sie still." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El haz y las partículas de luz de luna se mueven y el fondo animado se reproduce; al desactivarlo, quedan inmóviles." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O feixe e as partículas do luar se movem e o fundo animado é reproduzido; ao desativar, ficam parados." + } + } + } + }, + "The new build of %@ has been kept.": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The new build of %@ has been kept." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La nuova compilazione di %@ è stata conservata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La nouvelle compilation de %@ a été conservée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der neue Build von %@ bleibt erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha conservado la nueva compilación de %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A nova compilação de %@ foi mantida." + } + } + } + }, + "The new build of %@ that waited for the game to quit is gone. Port it again.": { + "comment": "AppViewModel+PendingInstall.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The new build of %@ that waited for the game to quit is gone. Port it again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La nuova compilazione di %@ in attesa della chiusura del gioco non è più disponibile. Crea di nuovo l’app per Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La nouvelle compilation de %@, en attente de la fermeture du jeu, a disparu. Créez à nouveau l’app Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der neue Build von %@, dessen Installation auf das Beenden des Spiels wartete, ist nicht mehr vorhanden. Erstelle die App erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ya no está la nueva compilación de %@ que esperaba a que se cerrara el juego. Vuelve a compilarlo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A nova compilação de %@ que aguardava o encerramento do jogo não está mais disponível. Compile novamente." + } + } + } + }, + "The newest nxnative could not be downloaded. Check the connection, then try again.": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The newest nxnative could not be downloaded. Check the connection, then try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile scaricare la versione più recente di nxnative. Controlla la connessione, poi riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dernier nxnative n’a pas pu être téléchargé. Vérifiez la connexion, puis réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das neueste nxnative konnte nicht geladen werden. Prüfe die Verbindung und versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido descargar el nxnative más reciente. Comprueba la conexión e inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível baixar o nxnative mais recente. Verifique a conexão e tente novamente." + } + } + } + }, + "The newest nxnative needs Metal %@, so it needs a newer NX Library.": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The newest nxnative needs Metal %@, so it needs a newer NX Library." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La versione più recente di nxnative richiede Metal %@, quindi serve una NX Library più recente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dernier nxnative nécessite Metal %@ et requiert donc une version plus récente de NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das neueste nxnative benötigt Metal %@ und daher eine neuere NX Library-Version." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El nxnative más reciente necesita Metal %@, por lo que requiere una versión más reciente de NX Library." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O nxnative mais recente precisa do Metal %@, por isso requer um NX Library mais recente." + } + } + } + }, + "The nxnative checkout is missing.": { + "comment": "AddGameView+Rows.swift:176", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The nxnative checkout is missing." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il checkout di nxnative non è disponibile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La copie de travail de nxnative est introuvable." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der nxnative-Checkout fehlt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta la copia local de nxnative." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O checkout do nxnative não foi encontrado." + } + } + } + }, + "The nxnative checkout was not found. Choose it in Settings → Engine.": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The nxnative checkout was not found. Choose it in Settings → Engine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il checkout di nxnative non è stato trovato. Selezionalo in Impostazioni → Motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La copie de travail de nxnative est introuvable. Choisissez-la dans Réglages → Moteur." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der nxnative-Checkout wurde nicht gefunden. Wähle ihn unter Einstellungen → Engine aus." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha encontrado la copia local de nxnative. Selecciónala en Ajustes → Motor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O checkout do nxnative não foi encontrado. Selecione-o em Ajustes → Engine." + } + } + } + }, + "The original file is untouched. The stored copy is mode 0600 and key values are never shown or logged.": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The original file is untouched. The stored copy is mode 0600 and key values are never shown or logged." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file originale non viene modificato. La copia memorizzata ha permessi 0600 e i valori delle chiavi non vengono mai mostrati né registrati nei log." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier d’origine reste intact. La copie enregistrée utilise les permissions 0600 et les valeurs des clés ne sont jamais affichées ni inscrites dans les journaux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Originaldatei bleibt unverändert. Die gespeicherte Kopie hat Zugriffsrechte 0600; Schlüsselwerte werden nie angezeigt oder protokolliert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El archivo original sigue intacto. La copia guardada tiene permisos 0600 y los valores de las claves nunca se muestran ni se escriben en los registros." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O arquivo original não é alterado. A cópia armazenada tem permissões 0600, e os valores das chaves nunca são exibidos nem registrados em logs." + } + } + } + }, + "The original sources are attached. Rebuild writes a new app and keeps the previous one if it fails.": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The original sources are attached. Rebuild writes a new app and keeps the previous one if it fails." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le sorgenti originali sono collegate. La ricompilazione crea una nuova app e conserva quella precedente in caso di errore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les sources d’origine sont associées. La recompilation crée une nouvelle app et conserve la précédente en cas d’échec." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die ursprünglichen Quellen sind zugeordnet. Beim erneuten Erstellen wird eine neue App geschrieben; schlägt dies fehl, bleibt die bisherige erhalten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos de origen originales están vinculados. Al recompilar, se crea una nueva app y se conserva la anterior si hay un fallo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos de origem originais estão vinculados. A recompilação cria um novo app e mantém o anterior em caso de falha." + } + } + } + }, + "The package contents could not be read. Check the files and imported keys.": { + "comment": "EnginePackageReader.swift:34", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The package contents could not be read. Check the files and imported keys." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile leggere il contenuto del pacchetto. Controlla i file e le chiavi importate." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le contenu du paquet n’a pas pu être lu. Vérifiez les fichiers et les clés importées." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Paketinhalt konnte nicht gelesen werden. Prüfe die Dateien und die importierten Schlüssel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido leer el contenido del paquete. Comprueba los archivos y las claves importadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível ler o conteúdo do pacote. Verifique os arquivos e as chaves importadas." + } + } + } + }, + "The package has no common ticket needed to open %@.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The package has no common ticket needed to open %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Nel pacchetto manca il ticket comune necessario per aprire %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le paquet ne contient pas le ticket commun nécessaire pour ouvrir %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dem Paket fehlt das Common-Ticket, das zum Öffnen von %@ benötigt wird." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El paquete no contiene el ticket común necesario para abrir %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O pacote não contém o ticket comum necessário para abrir %@." + } + } + } + }, + "The palette for focus, text and backgrounds.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The palette for focus, text and backgrounds." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La palette per la selezione, il testo e gli sfondi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La palette pour la sélection, le texte et les arrière-plans." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Farbpalette für Fokus, Text und Hintergründe." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La paleta de colores para el foco, el texto y los fondos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A paleta para foco, texto e fundos." + } + } + } + }, + "The picture behind Settings: the same as the app's, or one of its own.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The picture behind Settings: the same as the app's, or one of its own." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’immagine dietro alle impostazioni: la stessa dell’app oppure una dedicata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’image derrière les Réglages : la même que celle de l’app, ou une image distincte." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Bild hinter den Einstellungen: dasselbe wie in der App oder ein eigenes." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La imagen de fondo de Ajustes: la misma que la de la app o una propia." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A imagem atrás dos Ajustes: a mesma do app ou uma própria." + } + } + } + }, + "The picture behind the base gradient, per theme or none.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The picture behind the base gradient, per theme or none." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’immagine dietro al gradiente di base: quella del tema o nessuna." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’image derrière le dégradé de base, selon le thème ou aucune." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Bild hinter dem grundlegenden Farbverlauf: je nach Design oder keines." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La imagen sobre el degradado base: según el tema o ninguna." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A imagem atrás do gradiente de base, definida por tema ou nenhuma." + } + } + } + }, + "The pinned commit matches upstream main.": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The pinned commit matches upstream main." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il commit fissato corrisponde a main upstream." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le commit épinglé correspond à main dans le dépôt amont." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der festgelegte Commit entspricht Upstream-main." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El commit fijado coincide con main del proyecto original." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O commit fixado corresponde ao main do repositório original." + } + } + } + }, + "The pipeline archive could not be made, so the app was packed without one. ": { + "comment": "BuildWarningCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The pipeline archive could not be made, so the app was packed without one. " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile creare l’archivio delle pipeline, quindi l’app è stata confezionata senza. " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’archive de pipelines n’a pas pu être créée ; l’app a donc été assemblée sans elle. " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Pipeline-Archiv konnte nicht erstellt werden. Die App wurde daher ohne Archiv gepackt. " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido crear el archivo de pipelines, así que la app se ha empaquetado sin él. " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível criar o arquivo de pipelines, então o app foi empacotado sem ele. " + } + } + } + }, + "The porting may stop, or the game may not start.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The porting may stop, or the game may not start." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La creazione dell’app per Mac potrebbe interrompersi o il gioco potrebbe non avviarsi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La création de l’app Mac peut s’interrompre, ou le jeu peut ne pas démarrer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App-Erstellung kann abbrechen oder das Spiel startet möglicherweise nicht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La compilación podría detenerse o el juego podría no iniciarse." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A compilação pode ser interrompida ou o jogo pode não iniciar." + } + } + } + }, + "The porting needs at least %@ GB free on the work volume.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The porting needs at least %@ GB free on the work volume." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La creazione dell’app per Mac richiede almeno %@ GB liberi sul volume di lavoro." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La création de l’app Mac nécessite au moins %@ Go libres sur le volume de travail." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App-Erstellung benötigt mindestens %@ GB freien Speicher auf dem Arbeitsvolume." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La compilación necesita al menos %@ GB libres en el volumen de trabajo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A compilação precisa de pelo menos %@ GB livres no volume de trabalho." + } + } + } + }, + "The porting started an Intel Python, so its tools ran under Rosetta.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The porting started an Intel Python, so its tools ran under Rosetta." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La creazione dell’app per Mac ha avviato un Python Intel, quindi i relativi strumenti sono stati eseguiti tramite Rosetta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La création de l’app Mac a lancé un Python Intel ; ses outils se sont donc exécutés sous Rosetta." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bei der App-Erstellung wurde ein Intel-Python gestartet. Die Werkzeuge liefen deshalb unter Rosetta." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La compilación ha iniciado un Python para Intel, por lo que sus herramientas se han ejecutado con Rosetta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A compilação iniciou um Python Intel, então as ferramentas foram executadas com o Rosetta." + } + } + } + }, + "The porting stopped": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The porting stopped" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La creazione dell’app per Mac si è interrotta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La création de l’app Mac s’est arrêtée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die App-Erstellung wurde gestoppt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La compilación se ha detenido" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A compilação foi interrompida" + } + } + } + }, + "The previous app was left untouched. The log has the failing command and output.": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The previous app was left untouched. The log has the failing command and output." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’app precedente non è stata modificata. Il log contiene il comando non riuscito e il relativo output." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’app précédente n’a pas été modifiée. Le journal contient la commande en échec et sa sortie." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die bisherige App bleibt unverändert. Das Protokoll enthält den fehlgeschlagenen Befehl und seine Ausgabe." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La app anterior sigue intacta. El registro contiene el comando que ha fallado y su salida." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O app anterior não foi alterado. O log contém o comando que falhou e sua saída." + } + } + } + }, + "The previous app was left untouched. You can retry or go back to the review step.": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The previous app was left untouched. You can retry or go back to the review step." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’app precedente non è stata modificata. Puoi riprovare o tornare al riepilogo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’app précédente n’a pas été modifiée. Vous pouvez réessayer ou revenir à l’étape de vérification." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die bisherige App bleibt unverändert. Du kannst es erneut versuchen oder zur Überprüfung zurückkehren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La app anterior sigue intacta. Puedes reintentar o volver al paso de revisión." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O app anterior não foi alterado. Você pode tentar novamente ou voltar à etapa de revisão." + } + } + } + }, + "The screen NX Library opens on.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The screen NX Library opens on." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La schermata iniziale di NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’écran affiché à l’ouverture de NX Library." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Ansicht, mit der NX Library geöffnet wird." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La pantalla que muestra NX Library al abrirse." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A tela inicial do NX Library." + } + } + } + }, + "The screen NX Library opens on: Spotlight, Collection, or where you left it.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The screen NX Library opens on: Spotlight, Collection, or where you left it." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La schermata iniziale di NX Library: Spotlight, Raccolta o l’ultima aperta." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’écran affiché à l’ouverture de NX Library : À la une, Collection ou le dernier écran utilisé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Ansicht, mit der NX Library geöffnet wird: Spotlight, Sammlung oder die zuletzt verwendete." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La pantalla que muestra NX Library al abrirse: Destacados, Colección o donde lo dejaste." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A tela inicial do NX Library: Destaques, Coleção ou onde você parou." + } + } + } + }, + "The selected developer folder is the Command Line Tools, which cannot build the app.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The selected developer folder is the Command Line Tools, which cannot build the app." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella sviluppatore selezionata è quella dei Command Line Tools, che non possono compilare l’app." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier de développement sélectionné est celui des Command Line Tools, qui ne permettent pas de compiler l’app." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Als Entwicklerordner sind die Command Line Tools ausgewählt. Damit lässt sich die App nicht erstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta de desarrollo seleccionada es la de Command Line Tools, que no permite compilar la app." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta de desenvolvimento selecionada é a Command Line Tools, que não pode compilar o app." + } + } + } + }, + "The selected update is played as the whole game. If the game stops on ": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The selected update is played as the whole game. If the game stops on " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’aggiornamento selezionato viene eseguito come gioco completo. Se il gioco si interrompe per " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La mise à jour sélectionnée est exécutée comme le jeu complet. Si le jeu s’interrompt en raison de " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das ausgewählte Update wird als vollständiges Spiel ausgeführt. Wenn das Spiel abbricht wegen " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La actualización seleccionada se ejecuta como el juego completo. Si el juego se detiene en " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A atualização selecionada é usada como jogo completo. Se o jogo parar em " + } + } + } + }, + "The selected update is played as the whole game. If the game stops on missing files, add the base game’s dump (base/exefs and base/romfs).": { + "comment": "AddGameView+Rows.swift:72", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The selected update is played as the whole game. If the game stops on missing files, add the base game’s dump (base/exefs and base/romfs)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’aggiornamento selezionato viene eseguito come gioco completo. Se il gioco si interrompe perché mancano dei file, aggiungi il dump del gioco base (base/exefs e base/romfs)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La mise à jour sélectionnée est exécutée comme le jeu complet. Si le jeu s’arrête en raison de fichiers manquants, ajoutez la copie extraite du jeu de base (base/exefs et base/romfs)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das ausgewählte Update wird als vollständiges Spiel ausgeführt. Falls das Spiel wegen fehlender Dateien abbricht, füge den Dump des Basisspiels hinzu (base/exefs und base/romfs)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La actualización seleccionada se ejecuta como el juego completo. Si el juego se detiene por falta de archivos, añade el volcado del juego base (base/exefs y base/romfs)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A atualização selecionada é usada como jogo completo. Se o jogo parar por falta de arquivos, adicione a cópia extraída do jogo base (base/exefs e base/romfs)." + } + } + } + }, + "The selected update is played as the whole game. If the game stops on missing files, use a NUP that includes the base game.": { + "comment": "AddGameView+Rows.swift:73", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The selected update is played as the whole game. If the game stops on missing files, use a NUP that includes the base game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’aggiornamento selezionato viene eseguito come gioco completo. Se il gioco si interrompe perché mancano dei file, usa un NUP che includa il gioco base." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La mise à jour sélectionnée est exécutée comme le jeu complet. Si le jeu s’arrête en raison de fichiers manquants, utilisez un NUP qui inclut le jeu de base." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das ausgewählte Update wird als vollständiges Spiel ausgeführt. Falls das Spiel wegen fehlender Dateien abbricht, verwende eine NUP-Datei, die das Basisspiel enthält." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La actualización seleccionada se ejecuta como el juego completo. Si el juego se detiene por falta de archivos, usa un NUP que incluya el juego base." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A atualização selecionada é usada como jogo completo. Se o jogo parar por falta de arquivos, use um NUP que inclua o jogo base." + } + } + } + }, + "The shader translator did not run": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The shader translator did not run" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il traduttore degli shader non è stato eseguito" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le traducteur de shaders ne s’est pas exécuté" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Shader-Übersetzer wurde nicht ausgeführt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El traductor de shaders no se ha ejecutado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O tradutor de shaders não foi executado" + } + } + } + }, + "The shader translator needs the .NET 10 runtime for this Mac.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The shader translator needs the .NET 10 runtime for this Mac." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il traduttore degli shader richiede il runtime .NET 10 per questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le traducteur de shaders nécessite l’environnement d’exécution .NET 10 pour ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Shader-Übersetzer benötigt die .NET 10-Laufzeit für diesen Mac." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El traductor de shaders necesita el entorno de ejecución de .NET 10 para este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O tradutor de shaders precisa do runtime do .NET 10 para este Mac." + } + } + } + }, + "The shaders were tested with another Metal compiler than the one on this Mac.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The shaders were tested with another Metal compiler than the one on this Mac." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gli shader sono stati testati con un compilatore Metal diverso da quello di questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les shaders ont été testés avec un autre compilateur Metal que celui de ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Shader wurden mit einem anderen Metal-Compiler getestet als dem auf diesem Mac." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los shaders se han probado con un compilador Metal distinto al de este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os shaders foram testados com um compilador Metal diferente do instalado neste Mac." + } + } + } + }, + "The source files are missing. Attach or locate the original base, update and DLC files first.": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The source files are missing. Attach or locate the original base, update and DLC files first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file sorgente non sono disponibili. Prima collega o individua i file originali del gioco base, degli aggiornamenti e dei DLC." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers sources sont introuvables. Associez ou localisez d’abord les fichiers d’origine du jeu de base, des mises à jour et des DLC." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Quelldateien fehlen. Ordne zuerst die ursprünglichen Dateien für Basisspiel, Update und DLC zu oder suche sie." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Faltan los archivos de origen. Primero vincula o localiza los archivos originales del juego base, las actualizaciones y los DLC." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos de origem não foram encontrados. Primeiro, vincule ou localize os arquivos originais do jogo base, das atualizações e dos DLCs." + } + } + } + }, + "The sources are detached: attach them again to rebuild.": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The sources are detached: attach them again to rebuild." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le sorgenti sono scollegate: collegale di nuovo per ricompilare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les sources sont dissociées : associez-les à nouveau pour recompiler." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Quellen sind getrennt: Ordne sie erneut zu, um die App erneut zu erstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos de origen están desvinculados: vuelve a vincularlos para recompilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos de origem estão desvinculados: vincule-os novamente para recompilar." + } + } + } + }, + "The sources were removed, but the library could not be saved: %@": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The sources were removed, but the library could not be saved: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le sorgenti sono state rimosse, ma non è stato possibile salvare la libreria: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les sources ont été supprimées, mais la bibliothèque n’a pas pu être enregistrée : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Quelldateien wurden entfernt, aber die Bibliothek konnte nicht gespeichert werden: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se eliminaron los archivos de origen, pero no se pudo guardar la biblioteca: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos de origem foram removidos, mas não foi possível salvar a biblioteca: %@" + } + } + } + }, + "The store logo, then Steam": { + "comment": "ChangeArtworkView.swift:438", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The store logo, then Steam" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il logo dello store, poi Steam" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le logo de la boutique, puis Steam" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Shop-Logo, danach Steam" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El logotipo de la tienda y, si no está disponible, Steam" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O logotipo da loja e, se não houver, Steam" + } + } + } + }, + "The sync folder is unavailable. Choose another folder.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The sync folder is unavailable. Choose another folder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La cartella di sincronizzazione non è disponibile. Scegli un’altra cartella." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le dossier de synchronisation est indisponible. Choisissez un autre dossier." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Synchronisierungsordner ist nicht verfügbar. Wähle einen anderen Ordner." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La carpeta de sincronización no está disponible. Elige otra carpeta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A pasta de sincronização está indisponível. Escolha outra pasta." + } + } + } + }, + "The toolchain is not certified: %@": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The toolchain is not certified: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La toolchain non è certificata: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La chaîne d’outils n’est pas certifiée : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Toolchain ist nicht zertifiziert: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La cadena de herramientas no está certificada: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O conjunto de ferramentas não é certificado: %@" + } + } + } + }, + "The toolchain is not certified; enable the override in Settings.": { + "comment": "AddGameView+Rows.swift:178", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The toolchain is not certified; enable the override in Settings." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La toolchain non è certificata; ignora il controllo in Impostazioni." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La chaîne d’outils n’est pas certifiée ; activez la dérogation dans les Réglages." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Toolchain ist nicht zertifiziert; aktiviere die Ausnahme in den Einstellungen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La cadena de herramientas no está certificada; activa la excepción en Ajustes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O conjunto de ferramentas não é certificado; ative a exceção nos Ajustes." + } + } + } + }, + "The update archive could not be verified; nothing was changed.": { + "comment": "AppUpdater.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The update archive could not be verified; nothing was changed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile verificare l’archivio dell’aggiornamento; non è stato modificato nulla." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’archive de mise à jour n’a pas pu être vérifiée ; aucune modification n’a été effectuée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Update-Archiv konnte nicht überprüft werden; es wurde nichts geändert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se ha podido verificar el archivo de actualización; no se ha cambiado nada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível verificar o arquivo de atualização; nada foi alterado." + } + } + } + }, + "The update can install %@.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The update can install %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’aggiornamento può installare %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La mise à jour peut installer %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das Update kann zu folgendem Zeitpunkt installiert werden: %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La actualización puede instalar %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A atualização pode instalar %@." + } + } + } + }, + "The upstream checkout used for porting; updates stay manual.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The upstream checkout used for porting; updates stay manual." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il checkout upstream usato per creare le app per Mac; gli aggiornamenti restano manuali." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La copie de travail du dépôt amont utilisée pour créer les apps Mac ; les mises à jour restent manuelles." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der Upstream-Checkout zum Erstellen von Apps; Updates erfolgen weiterhin manuell." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La copia local del proyecto original usada para compilar; las actualizaciones siguen siendo manuales." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O checkout do repositório original usado para compilar; as atualizações continuam manuais." + } + } + } + }, + "The upstream nxnative checkout used for builds. Updates stay manual.": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "The upstream nxnative checkout used for builds. Updates stay manual." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il checkout upstream di nxnative usato per le compilazioni. Gli aggiornamenti restano manuali." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La copie de travail du dépôt amont nxnative utilisée pour les compilations. Les mises à jour restent manuelles." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Der nxnative-Upstream-Checkout für Builds. Updates erfolgen weiterhin manuell." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La copia local del proyecto original nxnative que se usa para compilar. Las actualizaciones siguen siendo manuales." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O checkout original do nxnative usado nas compilações. As atualizações continuam manuais." + } + } + } + }, + "Theme": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Theme" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tema" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Thème" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Design" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tema" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tema" + } + } + } + }, + "Theme (%@)": { + "comment": "ThemeStore.swift:102", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Theme (%@)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tema (%@)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Thème (%@)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Design (%@)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tema (%@)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tema (%@)" + } + } + } + }, + "Theme (none)": { + "comment": "ThemeStore.swift:102", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Theme (none)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Tema (nessuno)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Thème (aucun)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Design (keines)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tema (ninguno)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tema (nenhum)" + } + } + } + }, + "Theme assets": { + "comment": "SettingsSoundSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Theme assets" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Risorse del tema" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ressources du thème" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Design-Assets" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Recursos del tema" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Recursos do tema" + } + } + } + }, + "Theme's own": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Theme's own" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Del tema" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Celui du thème" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Designvorgabe" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Del tema" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Do próprio tema" + } + } + } + }, + "There is no cheat file (.txt) there.": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "There is no cheat file (.txt) there." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non c’è alcun file di trucchi (.txt) in quella posizione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Il n’y a aucun fichier de codes de triche (.txt) à cet emplacement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dort gibt es keine Cheat-Datei (.txt)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay ningún archivo de trucos (.txt) ahí." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não há arquivo de trapaças (.txt) nesse local." + } + } + } + }, + "There isn't enough space to install this mod.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "There isn't enough space to install this mod." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Lo spazio non è sufficiente per installare questa mod." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’espace disponible est insuffisant pour installer ce mod." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht genügend Speicherplatz zum Installieren dieses Mods." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No hay espacio suficiente para instalar este mod." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não há espaço suficiente para instalar este mod." + } + } + } + }, + "These cheats are for another game (title ID %@).": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "These cheats are for another game (title ID %@)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questi trucchi sono per un altro gioco (title ID %@)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ces codes de triche sont destinés à un autre jeu (title ID %@)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Cheats sind für ein anderes Spiel (title ID %@)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estos trucos son para otro juego (title ID %@)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Estas trapaças são para outro jogo (title ID %@)." + } + } + } + }, + "These cheats are for another version of %@ (build %@). Yours is build %@.": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "These cheats are for another version of %@ (build %@). Yours is build %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questi trucchi sono per un’altra versione di %@ (build %@). La tua è la build %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ces codes de triche sont destinés à une autre version de %@ (build %@). La vôtre est le build %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Cheats sind für eine andere Version von %@ (Build %@). Deine Version ist Build %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estos trucos son para otra versión de %@ (compilación %@). La tuya es la compilación %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Estas trapaças são para outra versão de %@ (build %@). A sua é a build %@." + } + } + } + }, + "These keys do not open the game": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "These keys do not open the game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Queste chiavi non aprono il gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ces clés ne permettent pas de déchiffrer le jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Schlüssel können das Spiel nicht öffnen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estas claves no abren el juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Estas chaves não abrem o jogo" + } + } + } + }, + "These sources need a newer engine.": { + "comment": "AddGameView+Rows.swift:173", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "These sources need a newer engine." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Queste sorgenti richiedono un motore più recente." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ces sources nécessitent un moteur plus récent." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Quellen benötigen eine neuere Engine." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estos archivos de origen necesitan un motor más reciente." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Estes arquivos de origem precisam de uma engine mais recente." + } + } + } + }, + "These sources need an engine with extracted-folder or multi-title support.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "These sources need an engine with extracted-folder or multi-title support." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Queste sorgenti richiedono un motore che supporti le cartelle estratte o più titoli." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ces sources nécessitent un moteur prenant en charge les dossiers extraits ou les sources contenant plusieurs titres." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Quellen benötigen eine Engine mit Unterstützung für entpackte Ordner oder mehrere Titel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estos archivos de origen requieren un motor compatible con carpetas extraídas o varios títulos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Estes arquivos de origem exigem uma engine compatível com pastas extraídas ou múltiplos títulos." + } + } + } + }, + "They may have been moved or removed.": { + "comment": "GameResourceState.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "They may have been moved or removed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Potrebbero essere stati spostati o rimossi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ils ont peut-être été déplacés ou supprimés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sie wurden möglicherweise verschoben oder entfernt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Puede que se hayan movido o eliminado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Eles podem ter sido movidos ou removidos." + } + } + } + }, + "This NX Library uses nxnative %@; the engine here is at %@. Update downloads it and switches to it. Games you built keep playing.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This NX Library uses nxnative %@; the engine here is at %@. Update downloads it and switches to it. Games you built keep playing." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa NX Library usa nxnative %@; il motore qui è al commit %@. Con Aggiorna lo scarichi e passi a quella versione. I giochi già compilati continuano a funzionare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette version de NX Library utilise nxnative %@ ; le moteur présent est au commit %@. Mettre à jour le télécharge et bascule dessus. Les jeux déjà compilés restent jouables." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese NX Library-Version verwendet nxnative %@; die Engine hier steht auf %@. „Aktualisieren“ lädt die Version und wechselt zu ihr. Bereits erstellte Spiele bleiben spielbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este NX Library usa nxnative %@; el motor local está en %@. Actualizar lo descarga y cambia a esa versión. Los juegos ya compilados siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este NX Library usa o nxnative %@; a engine aqui está em %@. Atualizar baixa a versão e muda para ela. Seus jogos compilados continuam funcionando." + } + } + } + }, + "This NX Library uses the newest nxnative.": { + "comment": "UpstreamCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This NX Library uses the newest nxnative." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa NX Library usa la versione più recente di nxnative." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette version de NX Library utilise le dernier nxnative." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese NX Library-Version verwendet das neueste nxnative." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este NX Library usa el nxnative más reciente." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este NX Library usa o nxnative mais recente." + } + } + } + }, + "This app is not affiliated with Nintendo.": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This app is not affiliated with Nintendo." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa app non è affiliata a Nintendo." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette app n’est pas affiliée à Nintendo." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese App steht in keiner Verbindung zu Nintendo." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta app no está afiliada a Nintendo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este app não tem afiliação com a Nintendo." + } + } + } + }, + "This archive can't be opened.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This archive can't be opened." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è possibile aprire questo archivio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette archive ne peut pas être ouverte." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Archiv kann nicht geöffnet werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se puede abrir este archivo comprimido." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não é possível abrir este arquivo compactado." + } + } + } + }, + "This archive has files outside its folder, so it wasn't installed.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This archive has files outside its folder, so it wasn't installed." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo archivio contiene file esterni alla propria cartella, quindi non è stato installato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette archive contient des fichiers hors de son dossier ; elle n’a donc pas été installée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Archiv enthält Dateien außerhalb seines Ordners und wurde daher nicht installiert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este archivo comprimido contiene archivos fuera de su carpeta, por lo que no se ha instalado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este arquivo compactado contém arquivos fora da própria pasta, por isso não foi instalado." + } + } + } + }, + "This backup is incomplete, changed, or belongs to another game.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This backup is incomplete, changed, or belongs to another game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo backup è incompleto, è stato modificato o appartiene a un altro gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette copie de sauvegarde est incomplète, a été modifiée ou appartient à un autre jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Backup ist unvollständig, wurde geändert oder gehört zu einem anderen Spiel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta copia de seguridad está incompleta, ha cambiado o pertenece a otro juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este backup está incompleto, foi alterado ou pertence a outro jogo." + } + } + } + }, + "This build does not take %@=%@.": { + "comment": "NXNativeGameSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This build does not take %@=%@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa compilazione non accetta %@=%@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette compilation n’accepte pas %@=%@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Build akzeptiert %@=%@ nicht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta compilación no acepta %@=%@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esta compilação não aceita %@=%@." + } + } + } + }, + "This build has no %@ setting.": { + "comment": "NXNativeGameSettings.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This build has no %@ setting." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa compilazione non ha l’impostazione %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette compilation ne propose pas le réglage %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Build bietet keine Einstellung für %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta compilación no tiene un ajuste %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esta compilação não tem o ajuste %@." + } + } + } + }, + "This changes NX Library, not the language inside your games.": { + "comment": "Language/region preferences and existing library filters.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This changes NX Library, not the language inside your games." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Modifica NX Library, non la lingua all’interno dei giochi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce réglage modifie NX Library, pas la langue dans vos jeux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dies ändert NX Library, nicht die Sprache in deinen Spielen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esto cambia NX Library, no el idioma de tus juegos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Isso altera o NX Library, não o idioma dentro dos seus jogos." + } + } + } + }, + "This deletes %@. Sources, saves and the build cache are not deleted.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This deletes %@. Sources, saves and the build cache are not deleted." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Viene eliminato %@. Sorgenti, salvataggi e cache di compilazione non vengono eliminati." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette action supprime %@. Les sources, les sauvegardes et le cache de compilation ne sont pas supprimés." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dadurch wird %@ gelöscht. Quellen, Spielstände und Build-Cache werden nicht gelöscht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esto elimina %@. No se eliminan los archivos de origen, las partidas guardadas ni la caché de compilación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Isso apaga %@. Os arquivos de origem, os dados salvos e o cache de compilação não serão apagados." + } + } + } + }, + "This development copy can check releases but cannot replace itself.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This development copy can check releases but cannot replace itself." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa copia di sviluppo può verificare le versioni disponibili, ma non può sostituire se stessa." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette copie de développement peut rechercher les versions publiées, mais ne peut pas se remplacer elle-même." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Entwicklungsversion kann nach Veröffentlichungen suchen, sich aber nicht selbst ersetzen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta copia de desarrollo puede buscar versiones, pero no puede reemplazarse a sí misma." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esta cópia de desenvolvimento pode buscar versões, mas não pode substituir a si mesma." + } + } + } + }, + "This development copy can check releases. Install a packaged release to use automatic updates.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This development copy can check releases. Install a packaged release to use automatic updates." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa copia di sviluppo può verificare le versioni disponibili. Installa una versione distribuita per usare gli aggiornamenti automatici." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette copie de développement peut rechercher les versions publiées. Installez une version distribuée pour bénéficier des mises à jour automatiques." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Entwicklungsversion kann nach Veröffentlichungen suchen. Installiere eine paketierte Veröffentlichung, um automatische Updates zu nutzen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta copia de desarrollo puede buscar versiones. Instala una versión empaquetada para usar las actualizaciones automáticas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esta cópia de desenvolvimento pode buscar versões. Instale uma versão distribuída para usar as atualizações automáticas." + } + } + } + }, + "This engine does not support NUP files.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This engine does not support NUP files." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo motore non supporta i file NUP." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce moteur ne prend pas en charge les fichiers NUP." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Engine unterstützt keine NUP-Dateien." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este motor no admite archivos NUP." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esta engine não é compatível com arquivos NUP." + } + } + } + }, + "This engine does not support extracted folders.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This engine does not support extracted folders." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo motore non supporta le cartelle estratte." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce moteur ne prend pas en charge les dossiers extraits." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Engine unterstützt keine entpackten Ordner." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este motor no admite carpetas extraídas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esta engine não é compatível com pastas extraídas." + } + } + } + }, + "This file has no romfs folder, so it can't work.": { + "comment": "ModInstaller.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This file has no romfs folder, so it can't work." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo file non contiene una cartella romfs, quindi non può funzionare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce fichier ne contient aucun dossier romfs et ne peut donc pas fonctionner." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Diese Datei enthält keinen romfs-Ordner und kann daher nicht funktionieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este archivo no contiene una carpeta romfs, por lo que no puede funcionar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este arquivo não contém uma pasta romfs, por isso não pode funcionar." + } + } + } + }, + "This folder has an update or DLC but no base game.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This folder has an update or DLC but no base game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa cartella contiene un aggiornamento o DLC, ma nessun gioco base." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce dossier contient une mise à jour ou un DLC, mais aucun jeu de base." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Ordner enthält ein Update oder DLC, aber kein Basisspiel." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta carpeta contiene una actualización o un DLC, pero no el juego base." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Esta pasta contém uma atualização ou DLC, mas não contém o jogo base." + } + } + } + }, + "This game is not built yet. Build it first.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This game is not built yet. Build it first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’app di questo gioco non è ancora stata creata. Creala prima di continuare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce jeu n’a pas encore été compilé. Compilez-le d’abord." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Spiel wurde noch nicht als App erstellt. Erstelle zuerst die App." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este juego aún no se ha compilado. Compílalo primero." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este jogo ainda não foi compilado. Compile-o primeiro." + } + } + } + }, + "This game is not supported": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This game is not supported" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo gioco non è supportato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce jeu n’est pas pris en charge" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Spiel wird nicht unterstützt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este juego no es compatible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este jogo não é compatível" + } + } + } + }, + "This game is not supported yet": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This game is not supported yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo gioco non è ancora supportato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce jeu n’est pas encore pris en charge" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Spiel wird noch nicht unterstützt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este juego aún no es compatible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este jogo ainda não é compatível" + } + } + } + }, + "This game needs a valid title ID to manage saves.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This game needs a valid title ID to manage saves." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo gioco richiede un title ID valido per gestire i salvataggi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce jeu nécessite un title ID valide pour gérer les sauvegardes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Spiel benötigt eine gültige title ID zur Spielstandverwaltung." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este juego necesita un title ID válido para gestionar las partidas guardadas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este jogo precisa de um title ID válido para gerenciar os dados salvos." + } + } + } + }, + "This game uses a custom save location. Manage it outside this panel.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This game uses a custom save location. Manage it outside this panel." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questo gioco usa una posizione dei salvataggi personalizzata. Gestiscila fuori da questo pannello." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce jeu utilise un emplacement de sauvegarde personnalisé. Gérez-le en dehors de ce panneau." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieses Spiel verwendet einen eigenen Spielstandort. Verwalte ihn außerhalb dieses Bereichs." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este juego usa una ubicación personalizada para las partidas guardadas. Gestiónala fuera de este panel." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este jogo usa um local personalizado de dados salvos. Gerencie-o fora deste painel." + } + } + } + }, + "This is the game": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This is the game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "È questo il gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "C’est ce jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das ist das Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Este es el juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este é o jogo" + } + } + } + }, + "This permanently deletes: %@. The installed app, saves and logs stay. Rebuilds will be disabled until you attach the sources again. This cannot be undone.": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This permanently deletes: %@. The installed app, saves and logs stay. Rebuilds will be disabled until you attach the sources again. This cannot be undone." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Elimina definitivamente: %@. L’app installata, i salvataggi e i log vengono conservati. Non potrai ricompilare finché non colleghi di nuovo le sorgenti. Questa operazione è irreversibile." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Supprime définitivement : %@. L’app installée, les sauvegardes et les journaux sont conservés. La recompilation sera désactivée jusqu’à ce que vous rattachiez les sources. Cette action est irréversible." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Folgendes wird endgültig gelöscht: %@. Die installierte App, Spielstände und Protokolle bleiben erhalten. Neue Builds sind erst wieder möglich, wenn du die Quelldateien erneut verknüpfst. Dies kann nicht rückgängig gemacht werden." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Elimina de forma permanente: %@. Se conservan la app instalada, las partidas guardadas y los registros. No podrás recompilar hasta que vuelvas a vincular los archivos de origen. Esta acción no se puede deshacer." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Exclui permanentemente: %@. O app instalado, os saves e os logs são mantidos. Novas compilações ficam desativadas até você vincular os arquivos de origem novamente. Esta ação não pode ser desfeita." + } + } + } + }, + "This removes the library record only; %@ stays on disk.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This removes the library record only; %@ stays on disk." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Viene rimossa solo la voce della libreria; %@ resta sul disco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cette action retire uniquement l’entrée de la bibliothèque ; %@ reste sur le disque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dadurch wird nur der Bibliothekseintrag entfernt; %@ bleibt auf dem Datenträger." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Solo se quita la entrada de la biblioteca; %@ permanece en el disco." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Isso remove apenas o registro na biblioteca; %@ permanece no disco." + } + } + } + }, + "This save location contains a link or an unsafe path. Manage it in Finder.": { + "comment": "SaveLocation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This save location contains a link or an unsafe path. Manage it in Finder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Questa posizione dei salvataggi contiene un collegamento o un percorso non sicuro. Gestiscila nel Finder." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Cet emplacement de sauvegarde contient un lien ou un chemin non sûr. Gérez-le dans le Finder." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dieser Spielstandort enthält einen Link oder einen unsicheren Pfad. Verwalte ihn im Finder." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esta ubicación de partidas contiene un enlace o una ruta no segura. Gestiónala en el Finder." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Este local de dados salvos contém um link ou um caminho inseguro. Gerencie-o no Finder." + } + } + } + }, + "This version's cheat files go to the Trash.": { + "comment": "CheatsPanel.swift:298", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "This version's cheat files go to the Trash." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I file dei trucchi di questa versione vengono spostati nel Cestino." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les fichiers de codes de triche de cette version sont placés dans la corbeille." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Cheat-Dateien dieser Version werden in den Papierkorb gelegt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los archivos de trucos de esta versión se mueven a la Papelera." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Os arquivos de trapaças desta versão vão para o Lixo." + } + } + } + }, + "Thumbstick threshold for library navigation; does not alter game input.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Thumbstick threshold for library navigation; does not alter game input." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Soglia dello stick per navigare nella libreria; non modifica i comandi del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Seuil du stick pour la navigation dans la bibliothèque ; ne modifie pas les commandes en jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Stick-Schwelle für die Bibliotheksnavigation; ändert keine Spieleingaben." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Umbral del stick para navegar por la biblioteca; no modifica los controles del juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Limiar do analógico para navegar na biblioteca; não altera os comandos do jogo." + } + } + } + }, + "Tile order": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Tile order" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ordine dei riquadri" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ordre des vignettes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Kachelreihenfolge" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Orden de las fichas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ordem dos blocos" + } + } + } + }, + "Toolchain gate: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Toolchain gate: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controllo della toolchain: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contrôle de la chaîne d’outils : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Toolchain-Prüfung: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Control de la cadena de herramientas: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificação do conjunto de ferramentas: %@" + } + } + } + }, + "Toolchain gate: certified.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Toolchain gate: certified." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controllo della toolchain: certificata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contrôle de la chaîne d’outils : certifiée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Toolchain-Prüfung: zertifiziert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Control de la cadena de herramientas: certificada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificação do conjunto de ferramentas: certificado." + } + } + } + }, + "Toolchain gate: overridden; this build will be recorded as untested.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Toolchain gate: overridden; this build will be recorded as untested." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Controllo della toolchain: ignorato; questa compilazione sarà registrata come non testata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Contrôle de la chaîne d’outils : contourné ; cette compilation sera enregistrée comme non testée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Toolchain-Prüfung: übergangen; dieser Build wird als ungetestet erfasst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Control de la cadena de herramientas: omitido; esta compilación se registrará como no probada." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificação do conjunto de ferramentas: ignorada; esta compilação será registrada como não testada." + } + } + } + }, + "Top bar": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Top bar" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Barra superiore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Barre supérieure" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Obere Leiste" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Barra superior" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Barra superior" + } + } + } + }, + "Touch feedback": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Touch feedback" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Feedback al tocco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Retour tactile" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Berührungsfeedback" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Respuesta táctil" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Resposta ao toque" + } + } + } + }, + "Touchscreen": { + "comment": "ControlsView.swift:38", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Touchscreen" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermo touch" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écran tactile" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Touchscreen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pantalla táctil" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tela sensível ao toque" + } + } + } + }, + "Try again": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Try again" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Riprova" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Réessayer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Erneut versuchen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reintentar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tentar novamente" + } + } + } + }, + "Try another filter or search.": { + "comment": "ConsoleView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Try another filter or search." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prova un altro filtro o un’altra ricerca." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Essayez un autre filtre ou une autre recherche." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Versuche einen anderen Filter oder Suchbegriff." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Prueba con otro filtro o búsqueda." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tente outro filtro ou outra busca." + } + } + } + }, + "Turn all off": { + "comment": "CheatsPanel.swift:294", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn all off" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattiva tutti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout désactiver" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle deaktivieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactivar todos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desativar todas" + } + } + } + }, + "Turn off": { + "comment": "ModsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn off" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattiva" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactiver" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deaktivieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactivar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desativar" + } + } + } + }, + "Turn off Cloud saves in Settings → Games to import or restore.": { + "comment": "SavesPanel.swift:204", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn off Cloud saves in Settings → Games to import or restore." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattiva Salvataggi cloud in Impostazioni → Giochi per importare o ripristinare." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivez Sauvegardes dans le cloud dans Réglages → Jeux pour importer ou restaurer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deaktiviere Cloud-Spielstände unter Einstellungen → Spiele, um Spielstände zu importieren oder wiederherzustellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactiva Partidas en la nube en Ajustes → Juegos para importar o restaurar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desative Dados salvos na nuvem em Ajustes → Jogos para importar ou restaurar." + } + } + } + }, + "Turn off Cloud saves in the game's own settings first.": { + "comment": "SavesPanel.swift:206", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn off Cloud saves in the game's own settings first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattiva prima Salvataggi cloud nelle impostazioni del gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivez d’abord les sauvegardes dans le cloud dans les réglages du jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deaktiviere zuerst Cloud-Spielstände in den eigenen Einstellungen des Spiels." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactiva primero Partidas en la nube en los ajustes del propio juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Primeiro, desative Dados salvos na nuvem nos ajustes do próprio jogo." + } + } + } + }, + "Turn off cloud sync in the launch environment first.": { + "comment": "SavesPanel.swift:207", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn off cloud sync in the launch environment first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattiva prima la sincronizzazione cloud nell’ambiente di avvio." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivez d’abord la synchronisation cloud dans l’environnement de lancement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deaktiviere zuerst die Cloud-Synchronisierung in der Startumgebung." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactiva primero la sincronización en la nube en el entorno de inicio." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Primeiro, desative a sincronização na nuvem no ambiente de execução." + } + } + } + }, + "Turn off cloud sync in this configuration file first.": { + "comment": "SavesPanel.swift:205", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn off cloud sync in this configuration file first." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Disattiva prima la sincronizzazione cloud in questo file di configurazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Désactivez d’abord la synchronisation cloud dans ce fichier de configuration." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deaktiviere zuerst die Cloud-Synchronisierung in dieser Konfigurationsdatei." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desactiva primero la sincronización en la nube en este archivo de configuración." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Primeiro, desative a sincronização na nuvem neste arquivo de configuração." + } + } + } + }, + "Turn on": { + "comment": "ModsPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn on" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attiva" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktivieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Activar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ativar" + } + } + } + }, + "Turn on iCloud Drive or choose a sync folder.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn on iCloud Drive or choose a sync folder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attiva iCloud Drive o scegli una cartella di sincronizzazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activez iCloud Drive ou choisissez un dossier de synchronisation." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktiviere iCloud Drive oder wähle einen Synchronisierungsordner." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Activa iCloud Drive o elige una carpeta de sincronización." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ative o iCloud Drive ou escolha uma pasta de sincronização." + } + } + } + }, + "Turn this game's file mods on and off, and choose which one wins.": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Turn this game's file mods on and off, and choose which one wins." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attiva o disattiva le mod dei file di questo gioco e scegli quale ha la precedenza." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Activer ou désactiver les mods de fichiers de ce jeu et choisir lequel est prioritaire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Datei-Mods dieses Spiels aktivieren und deaktivieren sowie ihre Priorität festlegen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Activa y desactiva los mods de archivos de este juego y elige cuál tiene prioridad." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ative e desative os mods de arquivos deste jogo e escolha qual terá prioridade." + } + } + } + }, + "Type the %@ yourself.": { + "comment": "ChangeMetadataView.swift:181", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Type the %@ yourself." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Inserisci manualmente: %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Renseignez vous-même le champ %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ selbst eingeben." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Introduce manualmente: %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Digite manualmente: %@." + } + } + } + }, + "USB or Bluetooth · Button positions follow the engine's layout": { + "comment": "ControlsView.swift:39", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "USB or Bluetooth · Button positions follow the engine's layout" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "USB o Bluetooth · Le posizioni dei pulsanti seguono la disposizione del motore" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "USB ou Bluetooth · La position des boutons suit la disposition du moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "USB oder Bluetooth · Die Tastenpositionen richten sich nach der Engine-Belegung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "USB o Bluetooth · Las posiciones de los botones siguen la disposición del motor" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "USB ou Bluetooth · As posições dos botões seguem a disposição da engine" + } + } + } + }, + "Unavailable": { + "comment": "DetailsTopBlock.swift:175", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Indisponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Indisponível" + } + } + } + }, + "Unavailable: %@. Existing game apps still play.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Unavailable: %@. Existing game apps still play." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non disponibile: %@. Le app dei giochi esistenti funzionano comunque." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Indisponible : %@. Les apps de jeux existantes restent jouables." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht verfügbar: %@. Vorhandene Spiele-Apps bleiben spielbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No disponible: %@. Las apps de juegos existentes siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Indisponível: %@. Apps de jogos existentes continuam funcionando." + } + } + } + }, + "Unknown": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Unknown" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sconosciuta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Inconnue" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Unbekannt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Desconocida" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Desconhecida" + } + } + } + }, + "Unsaved progress in the game will be lost.": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Unsaved progress in the game will be lost." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I progressi non salvati nel gioco andranno persi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La progression non sauvegardée dans le jeu sera perdue." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht gespeicherter Spielfortschritt geht verloren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se perderá el progreso del juego que no hayas guardado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O progresso não salvo no jogo será perdido." + } + } + } + }, + "Untested toolchain": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Untested toolchain" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Toolchain non testata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chaîne d’outils non testée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ungetestete Toolchain" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cadena de herramientas no probada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Conjunto de ferramentas não testado" + } + } + } + }, + "Untested toolchain override": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Untested toolchain override" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Ignora controllo della toolchain non testata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dérogation pour chaîne d’outils non testée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ungetestete Toolchain zulassen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Permitir cadena de herramientas no probada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Permitir conjunto de ferramentas não testado" + } + } + } + }, + "Up to one frame more display latency.": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Up to one frame more display latency." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Fino a un fotogramma in più di latenza di visualizzazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Jusqu’à une image de latence d’affichage supplémentaire." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bis zu ein Frame mehr Anzeigelatenz." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Hasta un fotograma más de latencia de visualización." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Até um quadro a mais de latência na exibição." + } + } + } + }, + "Update": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Update" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiorna" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktualisieren" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizar" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizar" + } + } + } + }, + "Update NX Library: its built-in Python includes zbic from 0.1.3.2. Installing zbic with pip does not reach it.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Update NX Library: its built-in Python includes zbic from 0.1.3.2. Installing zbic with pip does not reach it." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiorna NX Library: dalla versione 0.1.3.2 il Python incluso contiene zbic. Installare zbic con pip non lo rende disponibile nell’ambiente incluso." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mettez NX Library à jour : son Python intégré inclut zbic depuis la version 0.1.3.2. Installer zbic avec pip n’a aucun effet sur ce Python." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktualisiere NX Library: Das integrierte Python enthält zbic ab 0.1.3.2. Eine Installation von zbic mit pip erreicht es nicht." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualiza NX Library: su Python integrado incluye zbic desde la versión 0.1.3.2. Instalar zbic con pip no lo añade a ese Python." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualize o NX Library: seu Python integrado inclui o zbic desde a versão 0.1.3.2. Instalar o zbic com pip não afeta esse Python." + } + } + } + }, + "Update recommended": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Update recommended" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiornamento consigliato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à jour recommandée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Update empfohlen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualización recomendada" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualização recomendada" + } + } + } + }, + "Update unavailable": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Update unavailable" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiornamento non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à jour indisponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Update nicht verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualización no disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualização indisponível" + } + } + } + }, + "Updated %@": { + "comment": "ModDetailPanel.swift:289", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Updated %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiornamento: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à jour : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aktualisiert: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizado %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizado em %@" + } + } + } + }, + "Updated the app icon of %@.": { + "comment": "AppViewModel+AppIcon.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Updated the app icon of %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Icona dell’app di %@ aggiornata." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’icône de l’app %@ a été mise à jour." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Das App-Symbol von %@ wurde aktualisiert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha actualizado el icono de la app de %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O ícone do app de %@ foi atualizado." + } + } + } + }, + "Updates are currently unavailable.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Updates are currently unavailable." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gli aggiornamenti non sono al momento disponibili." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les mises à jour sont actuellement indisponibles." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Updates sind derzeit nicht verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las actualizaciones no están disponibles en este momento." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "As atualizações estão indisponíveis no momento." + } + } + } + }, + "Updates not checked yet": { + "comment": "SettingsAboutSection.swift:221", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Updates not checked yet" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiornamenti non ancora verificati" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mises à jour pas encore recherchées" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Noch nicht nach Updates gesucht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Aún no se han buscado actualizaciones" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizações ainda não verificadas" + } + } + } + }, + "Updating the app icon in Finder and the Dock… large games can take a minute": { + "comment": "AppIconProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Updating the app icon in Finder and the Dock… large games can take a minute" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiornamento dell’icona dell’app nel Finder e nel Dock… per i giochi grandi può servire un minuto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à jour de l’icône de l’app dans le Finder et le Dock… cela peut prendre une minute pour les jeux volumineux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "App-Symbol im Finder und im Dock wird aktualisiert … bei großen Spielen kann das eine Minute dauern" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Actualizando el icono de la app en el Finder y el Dock… los juegos grandes pueden tardar un minuto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Atualizando o ícone do app no Finder e no Dock… jogos grandes podem levar um minuto" + } + } + } + }, + "Updating to Metal %@ is strongly recommended.": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Updating to Metal %@ is strongly recommended." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "L’aggiornamento a Metal %@ è fortemente consigliato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La mise à jour vers Metal %@ est vivement recommandée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ein Update auf Metal %@ wird dringend empfohlen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se recomienda encarecidamente actualizar a Metal %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "É altamente recomendável atualizar para o Metal %@." + } + } + } + }, + "Upscaling": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Upscaling" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Upscaling" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mise à l’échelle" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Upscaling" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Reescalado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ampliação de imagem" + } + } + } + }, + "Upstream": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Upstream" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Upstream" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dépôt amont" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Upstream" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Proyecto original" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Repositório original" + } + } + } + }, + "Upstream Commits": { + "comment": "SettingsEngineSection.swift:441", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Upstream Commits" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Commit upstream" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commits du dépôt amont" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Upstream-Commits" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Commits del proyecto original" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Commits do repositório original" + } + } + } + }, + "Upstream Commits (%@ newer)": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Upstream Commits (%@ newer)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Commit upstream (%@ più recenti)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Commits du dépôt amont (%@ plus récents)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Upstream-Commits (%@ neuer)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Commits del proyecto original (%@ más recientes)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Commits do repositório original (%@ mais recentes)" + } + } + } + }, + "Use new game defaults": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use new game defaults" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Usa i valori predefiniti per i nuovi giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser les réglages par défaut des nouveaux jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Standards für neue Spiele übernehmen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usar valores predeterminados para juegos nuevos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Usar padrões de novos jogos" + } + } + } + }, + "Use newest": { + "comment": "SettingsEngineSection.swift:337", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use newest" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Usa il più recente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser le dernier moteur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neueste verwenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usar el más reciente" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Usar mais recente" + } + } + } + }, + "Use on the Mac": { + "comment": "ChangeArtworkView.swift:443", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Use on the Mac" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Usa sul Mac" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utiliser sur le Mac" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Auf dem Mac verwenden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Usar en el Mac" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Usar no Mac" + } + } + } + }, + "User save": { + "comment": "Save safety and presentation; user content stays unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "User save" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Salvataggio utente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Sauvegarde utilisateur" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Benutzerspielstand" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Partida de usuario" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Salvamento de usuário" + } + } + } + }, + "Uses more GPU time and can increase heat.": { + "comment": "Engine game setting presentation; never translate env keys or values.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Uses more GPU time and can increase heat." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Richiede più tempo GPU e può aumentare il calore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilise plus de temps GPU et peut augmenter l’échauffement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Benötigt mehr GPU-Zeit und kann die Wärmeentwicklung erhöhen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Consume más tiempo de GPU y puede aumentar el calor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Usa mais tempo de GPU e pode aumentar o aquecimento." + } + } + } + }, + "Using the game's original app icon": { + "comment": "AppIconProgress.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Using the game's original app icon" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "In uso l’icona originale dell’app del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Utilisation de l’icône d’origine de l’app du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ursprüngliches App-Symbol des Spiels wird verwendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se usa el icono original de la app del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Usando o ícone original do app do jogo" + } + } + } + }, + "Verify the pinned engine commit and toolchain when the app starts.": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Verify the pinned engine commit and toolchain when the app starts." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica il commit fissato del motore e la toolchain all’avvio dell’app." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérifier le commit épinglé du moteur et la chaîne d’outils au démarrage de l’app." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Festgelegten Engine-Commit und Toolchain beim App-Start prüfen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Verifica el commit fijado del motor y la cadena de herramientas al abrir la app." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verifica o commit fixado da engine e o conjunto de ferramentas ao iniciar o app." + } + } + } + }, + "Verifying update": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Verifying update" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Verifica dell’aggiornamento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vérification de la mise à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Update wird überprüft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Verificando actualización" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Verificando atualização" + } + } + } + }, + "Version %@": { + "comment": "SettingsSidebar.swift:12", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão %@" + } + } + } + }, + "Version %@ · %@": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione %@ · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión %@ · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão %@ · %@" + } + } + } + }, + "Version %@ · %@ · %@": { + "comment": "BuildSourcesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · %@ · %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione %@ · %@ · %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · %@ · %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · %@ · %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión %@ · %@ · %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão %@ · %@ · %@" + } + } + } + }, + "Version %@ came with this Xcode; the engine is tested with %@. New builds wait for an NX Library update tested with it. Games you built still play.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@ came with this Xcode; the engine is tested with %@. New builds wait for an NX Library update tested with it. Games you built still play." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La versione %@ è inclusa in questo Xcode; il motore è testato con %@. Per nuove compilazioni occorre attendere un aggiornamento di NX Library testato con questa versione. I giochi già compilati funzionano comunque." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "La version %@ est fournie avec ce Xcode ; le moteur est testé avec %@. Les nouvelles compilations attendent une mise à jour de NX Library testée avec elle. Les jeux déjà compilés restent jouables." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@ wurde mit diesem Xcode geliefert; die Engine wurde mit %@ getestet. Neue Builds warten auf ein damit getestetes NX Library-Update. Bereits erstellte Spiele bleiben spielbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La versión %@ viene con este Xcode; el motor se ha probado con %@. Las nuevas compilaciones deben esperar a una actualización de NX Library probada con ella. Los juegos ya compilados siguen funcionando." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "A versão %@ veio com este Xcode; a engine foi testada com %@. Novas compilações aguardam uma atualização do NX Library testada com ela. Seus jogos compilados continuam funcionando." + } + } + } + }, + "Version %@ · Build %@ · Engine %@": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · Build %@ · Engine %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione %@ · Compilazione %@ · Motore %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · Compilation %@ · Moteur %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · Build %@ · Engine %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión %@ · Compilación %@ · Motor %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão %@ · Compilação %@ · Engine %@" + } + } + } + }, + "Version %@ · Updated %@": { + "comment": "ModDetailPanel.swift:289", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · Updated %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione %@ · Aggiornamento: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · Mise à jour : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@ · Aktualisiert: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión %@ · Actualizado %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão %@ · Atualizado em %@" + } + } + } + }, + "Version %@, the one this engine is tested with.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@, the one this engine is tested with." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione %@, quella con cui è testato questo motore." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version %@, celle avec laquelle ce moteur a été testé." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@, mit der diese Engine getestet wurde." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión %@, con la que se ha probado este motor." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão %@, com a qual esta engine foi testada." + } + } + } + }, + "Version %@; the engine is tested with %@. NX Library installs %@ through Xcode.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@; the engine is tested with %@. NX Library installs %@ through Xcode." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione %@; il motore è testato con %@. NX Library installa %@ tramite Xcode." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version %@ ; le moteur est testé avec %@. NX Library installe %@ via Xcode." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@; die Engine wurde mit %@ getestet. NX Library installiert %@ über Xcode." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión %@; el motor se ha probado con %@. NX Library instala %@ a través de Xcode." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão %@; a engine foi testada com %@. O NX Library instala %@ pelo Xcode." + } + } + } + }, + "Version %@; the untested toolchain override is on, so builds may differ.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version %@; the untested toolchain override is on, so builds may differ." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione %@; il controllo della toolchain non testata è ignorato, quindi le compilazioni potrebbero differire." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version %@ ; la dérogation pour chaîne d’outils non testée est activée, les compilations peuvent donc différer." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@; ungetestete Toolchains sind zugelassen, daher können Builds abweichen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión %@; está activada la opción de cadena de herramientas no probada, por lo que las compilaciones pueden variar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão %@; a permissão para conjunto de ferramentas não testado está ativada, então as compilações podem apresentar diferenças." + } + } + } + }, + "Version, credits and licenses.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Version, credits and licenses." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Versione, crediti e licenze." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Version, crédits et licences." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version, Mitwirkende und Lizenzen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Versión, créditos y licencias." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Versão, créditos e licenças." + } + } + } + }, + "Vibration strength": { + "comment": "GameOptionsPresentation.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Vibration strength" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Intensità della vibrazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Intensité des vibrations" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Vibrationsstärke" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Intensidad de la vibración" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Força da vibração" + } + } + } + }, + "View": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "View" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Visualizza" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ansehen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ver" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ver" + } + } + } + }, + "View all on GitHub": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "View all on GitHub" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Vedi tutti su GitHub" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Tout voir sur GitHub" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Alle auf GitHub ansehen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ver todos en GitHub" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ver todos no GitHub" + } + } + } + }, + "View button switches screens": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "View button switches screens" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il pulsante View cambia schermata" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le bouton View change d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ansicht-Taste wechselt Ansichten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El botón View cambia de pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Botão View alterna as telas" + } + } + } + }, + "View flips between Spotlight and Collection; holding it opens Find a game.": { + "comment": "SettingsNavigationSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "View flips between Spotlight and Collection; holding it opens Find a game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "View alterna Spotlight e Raccolta; tenendolo premuto si apre Trova un gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "View alterne entre À la une et Collection ; un appui prolongé ouvre Trouver un jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Ansicht-Taste wechselt zwischen Spotlight und Sammlung; gedrückt halten öffnet „Spiel finden“." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "View alterna entre Destacados y Colección; al mantenerlo pulsado, abre Buscar un juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "View alterna entre Destaques e Coleção; mantê-lo pressionado abre Encontrar um jogo." + } + } + } + }, + "View the library controls.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "View the library controls." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra i comandi della libreria." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher les commandes de la bibliothèque." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bibliothekssteuerung ansehen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Consulta los controles de la biblioteca." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Veja os controles da biblioteca." + } + } + } + }, + "View update": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "View update" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mostra aggiornamento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Voir la mise à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Update ansehen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ver actualización" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ver atualização" + } + } + } + }, + "View, search and copy logs.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "View, search and copy logs." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Visualizza, cerca e copia i log." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher, rechercher et copier les journaux." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Protokolle anzeigen, durchsuchen und kopieren." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Consulta, busca y copia registros." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Veja, busque e copie logs." + } + } + } + }, + "Visible lines: %@ / %@": { + "comment": "Console filtered line count compared with total line count.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Visible lines: %@ / %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Righe visibili: %@ / %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Lignes visibles : %@ / %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Sichtbare Zeilen: %@ / %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Líneas visibles: %@ / %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Linhas visíveis: %@ / %@" + } + } + } + }, + "Visual direction": { + "comment": "SettingsAboutSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Visual direction" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Direzione visiva" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Direction artistique" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Visuelle Gestaltung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Dirección visual" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Direção visual" + } + } + } + }, + "Wait for the current build to finish before removing sources.": { + "comment": "Source removal safety copy. User paths/names remain verbatim.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wait for the current build to finish before removing sources." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aspetta che la build in corso termini prima di rimuovere le sorgenti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Attendez la fin de la compilation en cours avant de supprimer les sources." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Warte, bis der aktuelle Build abgeschlossen ist, bevor du Quelldateien entfernst." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Espera a que termine la compilación actual antes de eliminar los archivos de origen." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aguarde a compilação atual terminar antes de remover os arquivos de origem." + } + } + } + }, + "Wait for the current build, storage operation or app update to finish.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wait for the current build, storage operation or app update to finish." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attendi il termine della compilazione, dell’operazione sullo spazio di archiviazione o dell’aggiornamento dell’app in corso." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Attendez la fin de la compilation, de l’opération de stockage ou de la mise à jour de l’app en cours." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Warte, bis der aktuelle Build, Speichervorgang oder das App-Update abgeschlossen ist." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Espera a que termine la compilación, la operación de almacenamiento o la actualización de la app en curso." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aguarde o término da compilação, da operação de armazenamento ou da atualização do app em andamento." + } + } + } + }, + "Waiting for build output…": { + "comment": "BuildLiveLogsDrawer.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Waiting for build output…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "In attesa dell’output di compilazione…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "En attente de la sortie de compilation…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Warten auf Build-Ausgabe …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esperando la salida de la compilación…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aguardando a saída da compilação…" + } + } + } + }, + "Wallpaper": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wallpaper" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fond d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundbild" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondo de pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Papel de parede" + } + } + } + }, + "Wallpaper & moonlight": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wallpaper & moonlight" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondo e luce lunare" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fond d’écran et clair de lune" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundbild & Mondlicht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondo y luz de luna" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Papel de parede e luar" + } + } + } + }, + "Wallpaper brightness": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wallpaper brightness" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Luminosità dello sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Luminosité du fond d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundbildhelligkeit" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Brillo del fondo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Brilho do papel de parede" + } + } + } + }, + "Wallpaper light": { + "comment": "SettingsLookSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wallpaper light" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Luce dello sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Éclairage du fond d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundlicht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Iluminación del fondo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Iluminação do papel de parede" + } + } + } + }, + "Wallpaper sizing": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wallpaper sizing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dimensionamento dello sfondo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Dimensionnement du fond d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundbildgröße" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tamaño del fondo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tamanho do papel de parede" + } + } + } + }, + "Wallpapers": { + "comment": "ThemePickerView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wallpapers" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sfondi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fonds d’écran" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hintergrundbilder" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Fondos de pantalla" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Papéis de parede" + } + } + } + }, + "Wallpapers/%@/%@": { + "comment": "SeleneImporter.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wallpapers/%@/%@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Wallpapers/%@/%@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Wallpapers/%@/%@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wallpapers/%@/%@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Wallpapers/%@/%@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Wallpapers/%@/%@" + } + } + } + }, + "Watch for an engine update that adds this game.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Watch for an engine update that adds this game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Attendi un aggiornamento del motore che aggiunga questo gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Surveillez les mises à jour du moteur ajoutant la prise en charge de ce jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Warte auf ein Engine-Update, das dieses Spiel unterstützt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Espera una actualización del motor que añada compatibilidad con este juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Aguarde uma atualização da engine que adicione este jogo." + } + } + } + }, + "What shows while a game starts: art and logo, art only, logo on black, or minimal.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "What shows while a game starts: art and logo, art only, logo on black, or minimal." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Cosa mostrare mentre si avvia un gioco: immagine e logo, solo immagine, logo su nero o stile minimale." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "L’affichage pendant le démarrage d’un jeu : illustration et logo, illustration seule, logo sur fond noir ou minimal." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Was während des Spielstarts angezeigt wird: Artwork und Logo, nur Artwork, Logo auf Schwarz oder minimal." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Lo que se muestra mientras se inicia un juego: imagen y logotipo, solo imagen, logotipo sobre negro o mínimo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O que aparece enquanto um jogo inicia: arte e logotipo, só arte, logotipo em fundo preto ou mínimo." + } + } + } + }, + "What you played this week.": { + "comment": "SpotlightCardInfoView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "What you played this week." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "A cosa hai giocato questa settimana." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les jeux auxquels vous avez joué cette semaine." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Was du diese Woche gespielt hast." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Lo que has jugado esta semana." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O que você jogou nesta semana." + } + } + } + }, + "When you close a game, a card shows how long you played this time and in total.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "When you close a game, a card shows how long you played this time and in total." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Quando chiudi un gioco, una scheda mostra quanto hai giocato nella sessione e in totale." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand vous fermez un jeu, une fiche indique le temps joué pendant cette session et au total." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wenn du ein Spiel beendest, zeigt eine Karte deine Spielzeit dieser Sitzung und insgesamt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cuando cierras un juego, una tarjeta muestra cuánto has jugado en esta sesión y en total." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ao encerrar um jogo, um cartão mostra por quanto tempo você jogou nesta sessão e no total." + } + } + } + }, + "When you close a game, a card shows how long you played this time and in total. Sessions under 10 seconds show none.": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "When you close a game, a card shows how long you played this time and in total. Sessions under 10 seconds show none." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Quando chiudi un gioco, una scheda mostra quanto hai giocato nella sessione e in totale. Non appare per le sessioni inferiori a 10 secondi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Quand vous fermez un jeu, une fiche indique le temps joué pendant cette session et au total. Aucune fiche ne s’affiche pour les sessions de moins de 10 secondes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wenn du ein Spiel beendest, zeigt eine Karte deine Spielzeit dieser Sitzung und insgesamt. Bei Sitzungen unter 10 Sekunden erscheint keine Karte." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cuando cierras un juego, una tarjeta muestra cuánto has jugado en esta sesión y en total. No se muestra para sesiones de menos de 10 segundos." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Ao encerrar um jogo, um cartão mostra por quanto tempo você jogou nesta sessão e no total. Sessões com menos de 10 segundos não exibem cartão." + } + } + } + }, + "Where I left": { + "comment": "SettingsStartUpSection.swift:15", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Where I left" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dove avevo lasciato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Là où j’en étais" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Zuletzt verwendete Ansicht" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Donde lo dejé" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Onde parei" + } + } + } + }, + "Where should the icon appear?": { + "comment": "ChangeArtworkView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Where should the icon appear?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dove deve apparire l’icona?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Où l’icône doit-elle apparaître ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Wo soll das Symbol erscheinen?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Dónde debe aparecer el icono?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Onde o ícone deve aparecer?" + } + } + } + }, + "Which cards show on Spotlight and where it opens.": { + "comment": "SettingsSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Which cards show on Spotlight and where it opens." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Quali schede mostrare in Spotlight e dove si apre." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les cartes affichées dans À la une et son affichage initial." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Welche Karten in Spotlight erscheinen und mit welchem Filter es geöffnet wird." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las tarjetas que aparecen en Destacados y dónde se abre." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Quais cartões aparecem em Destaques e onde a tela abre." + } + } + } + }, + "Which cards sit in the Spotlight row: Collection, Surprise me, Activity, Mods and Add game.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Which cards sit in the Spotlight row: Collection, Surprise me, Activity, Mods and Add game." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le schede presenti nella riga di Spotlight: Raccolta, Sorprendimi, Attività, Mod e Aggiungi gioco." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les cartes affichées dans la rangée À la une : Collection, Surprenez-moi, Activité, Mods et Ajouter un jeu." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Welche Karten in der Spotlight-Reihe erscheinen: Sammlung, Überrasch mich, Aktivität, Mods und Spiel hinzufügen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las tarjetas de la fila de Destacados: Colección, Sorpréndeme, Actividad, Mods y Añadir juego." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Quais cartões ficam na fileira de Destaques: Coleção, Surpreenda-me, Atividade, Mods e Adicionar jogo." + } + } + } + }, + "Which game is %@?": { + "comment": "GameBananaFindPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Which game is %@?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Quale gioco è %@?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "À quel jeu correspond %@ ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Welches Spiel ist %@?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿Qué juego es %@?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Qual jogo é %@?" + } + } + } + }, + "Which games Surprise me picks from.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Which games Surprise me picks from." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I giochi tra cui sceglie Sorprendimi." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les jeux parmi lesquels Surprenez-moi fait son choix." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Aus welchen Spielen „Überrasch mich“ auswählt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Los juegos entre los que elige Sorpréndeme." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "De quais jogos o Surpreenda-me pode escolher." + } + } + } + }, + "Which letters the prompts use: Nintendo, Xbox or PlayStation. Automatic follows the connected controller.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Which letters the prompts use: Nintendo, Xbox or PlayStation. Automatic follows the connected controller." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Quali lettere usare nei suggerimenti: Nintendo, Xbox o PlayStation. Automatico segue il controller connesso." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les lettres utilisées dans les indications : Nintendo, Xbox ou PlayStation. Automatique suit la manette connectée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Welche Buchstaben die Tastenhinweise verwenden: Nintendo, Xbox oder PlayStation. „Automatisch“ richtet sich nach dem verbundenen Controller." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Las letras que usan las indicaciones: Nintendo, Xbox o PlayStation. Automático sigue al mando conectado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Quais letras as indicações usam: Nintendo, Xbox ou PlayStation. Automático segue o controle conectado." + } + } + } + }, + "Window": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Window" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Finestra" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fenêtre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fenster" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ventana" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Janela" + } + } + } + }, + "Working": { + "comment": "SavesPanel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Working" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Operazione in corso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Opération en cours" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "In Bearbeitung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "En curso" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Processando" + } + } + } + }, + "Working tree": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Working tree" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Albero di lavoro" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Copie de travail" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Arbeitsverzeichnis" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Árbol de trabajo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Árvore de trabalho" + } + } + } + }, + "Workspace: %@": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Workspace: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Area di lavoro: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Espace de travail : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Arbeitsverzeichnis: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Espacio de trabajo: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Área de trabalho: %@" + } + } + } + }, + "Worlds & stages": { + "comment": "ModCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Worlds & stages" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Mondi e livelli" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Mondes et niveaux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Welten & Level" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mundos y niveles" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Mundos e fases" + } + } + } + }, + "Write your own…": { + "comment": "ChangeMetadataView.swift:181", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Write your own…" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Scrivi tu…" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Rédiger vous-même…" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigenen Text schreiben …" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Escribir manualmente…" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Escrever seu próprio texto…" + } + } + } + }, + "Wrong game?": { + "comment": "DetailsBands.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Wrong game?" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Gioco sbagliato?" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ce n’est pas le bon jeu ?" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Falsches Spiel?" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "¿No es el juego correcto?" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Jogo errado?" + } + } + } + }, + "X": { + "comment": "ButtonPrompts.swift:37", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "X" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "X" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "X" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "X" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "X" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "X" + } + } + } + }, + "Xcode": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Xcode" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Xcode" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Xcode" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Xcode" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Xcode" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Xcode" + } + } + } + }, + "Xcode %@ is installed; the engine needs %@ or newer. Update it from the App Store.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@ is installed; the engine needs %@ or newer. Update it from the App Store." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@ è installato; il motore richiede %@ o successivo. Aggiornalo dall’App Store." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@ est installé ; le moteur nécessite %@ ou ultérieur. Mettez-le à jour depuis l’App Store." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@ ist installiert; die Engine benötigt %@ oder neuer. Aktualisiere es über den App Store." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@ está instalado; el motor necesita %@ o posterior. Actualízalo desde App Store." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O Xcode %@ está instalado; a engine precisa da versão %@ ou posterior. Atualize pela App Store." + } + } + } + }, + "Xcode %@, installed and set up.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@, installed and set up." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@, installato e configurato." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@, installé et configuré." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@, installiert und eingerichtet." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@, instalado y configurado." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Xcode %@, instalado e configurado." + } + } + } + }, + "Xcode is not ready; a porting needs it.": { + "comment": "AddGameView+Rows.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Xcode is not ready; a porting needs it." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Xcode non è pronto; è necessario per creare l’app per Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Xcode n’est pas prêt ; la création de l’app Mac le nécessite." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Xcode ist nicht bereit; es wird zum Erstellen der App benötigt." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Xcode no está listo; es necesario para compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O Xcode não está pronto; ele é necessário para compilar." + } + } + } + }, + "Xcode is not selected": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Xcode is not selected" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Xcode non è selezionato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Xcode n’est pas sélectionné" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Xcode ist nicht ausgewählt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Xcode no está seleccionado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O Xcode não está selecionado" + } + } + } + }, + "Xcode unknown": { + "comment": "SettingsStartUpSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Xcode unknown" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Xcode sconosciuto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Xcode inconnu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Xcode unbekannt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Xcode desconocido" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Xcode desconhecido" + } + } + } + }, + "Y": { + "comment": "ButtonPrompts.swift:38", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Y" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Y" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Y" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Y" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Y" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Y" + } + } + } + }, + "YYYY-MM-DD": { + "comment": "ChangeMetadataView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "YYYY-MM-DD" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "YYYY-MM-DD" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "YYYY-MM-DD" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "YYYY-MM-DD" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "YYYY-MM-DD" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "YYYY-MM-DD" + } + } + } + }, + "You have %@.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "You have %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Hai %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous disposez de %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Du hast %@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tienes %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Você tem %@." + } + } + } + }, + "You have NX Library %@. No newer release is available.": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "You have NX Library %@. No newer release is available." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Hai NX Library %@. Non sono disponibili versioni più recenti." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous disposez de NX Library %@. Aucune version plus récente n’est disponible." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Du hast NX Library %@. Es ist keine neuere Veröffentlichung verfügbar." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tienes NX Library %@. No hay ninguna versión más reciente." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Você tem o NX Library %@. Não há versão mais recente disponível." + } + } + } + }, + "You've started everything": { + "comment": "SpotlightRowView.swift:195", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "You've started everything" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Hai avviato tutti i giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous avez essayé tous vos jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Du hast alle Spiele gestartet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ya has probado todos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Você já começou todos" + } + } + } + }, + "Your Metal compiler (%@) is newer than the one nxnative is tested with (%@).": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your Metal compiler (%@) is newer than the one nxnative is tested with (%@)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il tuo compilatore Metal (%@) è più recente di quello con cui è testato nxnative (%@)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre compilateur Metal (%@) est plus récent que celui avec lequel nxnative est testé (%@)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein Metal-Compiler (%@) ist neuer als der, mit dem nxnative getestet wurde (%@)." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu compilador Metal (%@) es más reciente que aquel con el que se ha probado nxnative (%@)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Seu compilador Metal (%@) é mais recente que aquele com o qual o nxnative foi testado (%@)." + } + } + } + }, + "Your Metal compiler (%@) is older than the one nxnative needs (%@). ": { + "comment": "ToolchainGateCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your Metal compiler (%@) is older than the one nxnative needs (%@). " + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il tuo compilatore Metal (%@) è più vecchio di quello richiesto da nxnative (%@). " + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre compilateur Metal (%@) est plus ancien que celui requis par nxnative (%@). " + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein Metal-Compiler (%@) ist älter als der, den nxnative benötigt (%@). " + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu compilador Metal (%@) es anterior al que necesita nxnative (%@). " + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Seu compilador Metal (%@) é mais antigo que o necessário para o nxnative (%@). " + } + } + } + }, + "Your Metal compiler is not certified": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your Metal compiler is not certified" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il compilatore Metal non è certificato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre compilateur Metal n’est pas certifié" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein Metal-Compiler ist nicht zertifiziert" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu compilador Metal no está certificado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Seu compilador Metal não é certificado" + } + } + } + }, + "Your activity": { + "comment": "DetailsBands.swift:63", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your activity" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La tua attività" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre activité" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Aktivität" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu actividad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sua atividade" + } + } + } + }, + "Your build has been kept. Retry installs it without porting again.": { + "comment": "AddGameStepThreeView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your build has been kept. Retry installs it without porting again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La compilazione è stata conservata. Con Riprova puoi installarla senza creare di nuovo l’app per Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre compilation a été conservée. Réessayer l’installe sans recréer l’app Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein Build bleibt erhalten. Mit „Installation wiederholen“ wird er installiert, ohne die App erneut zu erstellen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se ha conservado la compilación. Al reintentar, se instalará sin volver a compilar." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sua compilação foi mantida. Tentar novamente fará a instalação sem recompilar." + } + } + } + }, + "Your changes could not be saved. Try again.": { + "comment": "AppShellView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your changes could not be saved. Try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile salvare le modifiche. Riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos modifications n’ont pas pu être enregistrées. Réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Änderungen konnten nicht gespeichert werden. Versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han podido guardar tus cambios. Inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível salvar suas alterações. Tente novamente." + } + } + } + }, + "Your console's prod.keys, which unlock your own games. They stay on this Mac.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your console's prod.keys, which unlock your own games. They stay on this Mac." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il file prod.keys della tua console, che sblocca i tuoi giochi. Resta su questo Mac." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le fichier prod.keys de votre console, qui permet de déchiffrer vos propres jeux. Les clés restent sur ce Mac." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die prod.keys deiner Konsole zum Entschlüsseln deiner eigenen Spiele. Sie bleiben auf diesem Mac." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El archivo prod.keys de tu consola, que desbloquea tus propios juegos. Se conserva en este Mac." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O prod.keys do seu console, que desbloqueia seus próprios jogos. As chaves ficam apenas neste Mac." + } + } + } + }, + "Your current save is backed up before replacement.": { + "comment": "SaveImportDetails.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your current save is backed up before replacement." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Prima della sostituzione viene creato un backup del salvataggio attuale." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Une copie de votre sauvegarde actuelle est créée avant son remplacement." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein aktueller Spielstand wird vor dem Ersetzen gesichert." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Se hace una copia de seguridad de la partida actual antes de reemplazarla." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Um backup dos dados salvos atuais será feito antes da substituição." + } + } + } + }, + "Your game options could not be saved. Try again.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your game options could not be saved. Try again." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile salvare le opzioni del gioco. Riprova." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Les options du jeu n’ont pas pu être enregistrées. Réessayez." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Spieloptionen konnten nicht gespeichert werden. Versuche es erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han podido guardar las opciones del juego. Inténtalo de nuevo." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível salvar as opções do jogo. Tente novamente." + } + } + } + }, + "Your game: %@": { + "comment": "ModDetailPanel.swift:290", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your game: %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il tuo gioco: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre jeu : %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Dein Spiel: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu juego: %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Seu jogo: %@" + } + } + } + }, + "Your games": { + "comment": "ThemePreview.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your games" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "I tuoi giochi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos jeux" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Spiele" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tus juegos" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Seus jogos" + } + } + } + }, + "Your keys are too old": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your keys are too old" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Le chiavi sono troppo vecchie" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos clés sont trop anciennes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Schlüssel sind zu alt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tus claves son demasiado antiguas" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Suas chaves são antigas demais" + } + } + } + }, + "Your keys could not be read": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your keys could not be read" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Non è stato possibile leggere le chiavi" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vos clés n’ont pas pu être lues" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Schlüssel konnten nicht gelesen werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No se han podido leer tus claves" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Não foi possível ler suas chaves" + } + } + } + }, + "Your own": { + "comment": "WallpaperCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your own" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il tuo" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le vôtre" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Eigenes" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Propio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Imagem própria" + } + } + } + }, + "Your own order of games and cards on Spotlight. Reset puts everything back in place.": { + "comment": "SettingsCatalog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your own order of games and cards on Spotlight. Reset puts everything back in place." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il tuo ordine di giochi e schede in Spotlight. Reimposta rimette tutto al suo posto." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre ordre personnalisé des jeux et des cartes dans À la une. Réinitialiser remet tout en place." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine eigene Reihenfolge von Spielen und Karten in Spotlight. „Zurücksetzen“ stellt die ursprüngliche Reihenfolge wieder her." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu propio orden de juegos y tarjetas en Destacados. Restablecer devuelve todo a su sitio." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sua própria ordem de jogos e cartões em Destaques. Redefinir coloca tudo de volta no lugar." + } + } + } + }, + "Your own order of games and cards.": { + "comment": "SettingsSpotlightSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your own order of games and cards." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il tuo ordine di giochi e schede." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre ordre personnalisé des jeux et des cartes." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine eigene Reihenfolge von Spielen und Karten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu propio orden de juegos y tarjetas." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sua própria ordem de jogos e cartões." + } + } + } + }, + "Your pick": { + "comment": "ChangeArtworkView.swift:441", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your pick" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "La tua scelta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre choix" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Auswahl" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu elección" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sua escolha" + } + } + } + }, + "Your playtime when a game closes": { + "comment": "SettingsGamesSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Your playtime when a game closes" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Il tempo di gioco alla chiusura di un gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Votre temps de jeu à la fermeture d’un jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Deine Spielzeit nach dem Beenden eines Spiels" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Tu tiempo de juego al cerrar un juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Seu tempo de jogo ao encerrar um jogo" + } + } + } + }, + "You’re up to date": { + "comment": "AppUpdateDialog.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "You’re up to date" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Hai la versione più recente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Vous êtes à jour" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Du bist auf dem neuesten Stand" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Estás al día" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Você está com a versão mais recente" + } + } + } + }, + "a drive": { + "comment": "Display copy only; internal values and paths remain unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "a drive" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "un disco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "un disque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ein Laufwerk" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "una unidad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "uma unidade" + } + } + } + }, + "art by %@": { + "comment": "ThemePickerView.swift:378", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "art by %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "immagine di %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "illustration par %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Artwork von %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "imágenes de %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "arte por %@" + } + } + } + }, + "available": { + "comment": "NXNativeEnvironment.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "available" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "disponible" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "verfügbar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "disponible" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "disponível" + } + } + } + }, + "by %@": { + "comment": "ModStorePage.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "by %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "di %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "par %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "von %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "de %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "por %@" + } + } + } + }, + "checked by the build command": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "checked by the build command" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "verificato dal comando di compilazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vérifié par la commande de compilation" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "wird vom Build-Befehl geprüft" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "se comprueba en el comando de compilación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "verificado pelo comando de compilação" + } + } + } + }, + "current %@, expected %@": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "current %@, expected %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "attuale %@, previsto %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "actuel %@, attendu %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "aktuell %@, erwartet %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "actual %@, esperado %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "atual %@, esperado %@" + } + } + } + }, + "done": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "done" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "completato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "terminé" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "fertig" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "hecho" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "concluído" + } + } + } + }, + "end": { + "comment": "ButtonPrompts.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "end" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "end" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "fin" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ende" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "fin" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "end" + } + } + } + }, + "esc": { + "comment": "ButtonPrompts.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "esc" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "esc" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "esc" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "esc" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "esc" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "esc" + } + } + } + }, + "fullGameImageFileName": { + "comment": "LaunchBoxArt.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "fullGameImageFileName" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "fullGameImageFileName" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "fullGameImageFileName" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "fullGameImageFileName" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "fullGameImageFileName" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "fullGameImageFileName" + } + } + } + }, + "git could not run": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "git could not run" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "impossibile eseguire git" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "git n’a pas pu s’exécuter" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "git konnte nicht ausgeführt werden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "no se ha podido ejecutar git" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "não foi possível executar git" + } + } + } + }, + "header_key does not open %@.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "header_key does not open %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "header_key non apre %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "header_key ne permet pas de déchiffrer %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "header_key kann %@ nicht öffnen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "header_key no abre %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "header_key não abre %@." + } + } + } + }, + "header_key present · %@ application key generations": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "header_key present · %@ application key generations" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "header_key presente · %@ generazioni di chiavi applicazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "header_key présente · %@ générations de clés d’application" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "header_key vorhanden · %@ Anwendungsschlüsselgenerationen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "header_key presente · %@ generaciones de claves de aplicación" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "header_key presente · %@ gerações de chaves de aplicativo" + } + } + } + }, + "home": { + "comment": "ButtonPrompts.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "home" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "home" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "début" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Pos1" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "inicio" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "home" + } + } + } + }, + "iCloud Drive": { + "comment": "CloudSaves.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive" + } + } + } + }, + "iCloud Drive is not on for this Mac. Turn it on in System Settings › Apple Account › iCloud, or choose a folder.": { + "comment": "CloudSavesPreferences.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive is not on for this Mac. Turn it on in System Settings › Apple Account › iCloud, or choose a folder." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive non è attivo su questo Mac. Attivalo in Impostazioni di Sistema › Apple Account › iCloud oppure scegli una cartella." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive n’est pas activé sur ce Mac. Activez-le dans Réglages Système › Compte Apple › iCloud, ou choisissez un dossier." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive ist auf diesem Mac nicht aktiviert. Aktiviere es unter Systemeinstellungen › Apple Account › iCloud oder wähle einen Ordner." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "iCloud Drive no está activado en este Mac. Actívalo en Ajustes del Sistema › Cuenta de Apple › iCloud o elige una carpeta." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O iCloud Drive não está ativado neste Mac. Ative-o em Ajustes do Sistema › Conta Apple › iCloud ou escolha uma pasta." + } + } + } + }, + "iPhone Duo screen (1.43:1)": { + "comment": "Engine setting option display label.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "iPhone Duo screen (1.43:1)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Schermo iPhone Duo (1.43:1)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Écran iPhone Duo (1.43:1)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "iPhone Duo-Bildschirm (1.43:1)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Pantalla de iPhone Duo (1.43:1)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Tela do iPhone Duo (1.43:1)" + } + } + } + }, + "imageFileName": { + "comment": "LaunchBoxArt.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "imageFileName" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "imageFileName" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "imageFileName" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "imageFileName" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "imageFileName" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "imageFileName" + } + } + } + }, + "imageTypeName": { + "comment": "LaunchBoxArt.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "imageTypeName" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "imageTypeName" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "imageTypeName" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "imageTypeName" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "imageTypeName" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "imageTypeName" + } + } + } + }, + "in": { + "comment": "SeleneArtBackground.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "in" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "presente" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "chargée" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "eingeblendet" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "en" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "carregada" + } + } + } + }, + "installed %@ · required %@": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "installed %@ · required %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "installato %@ · richiesto %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "installé %@ · requis %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "installiert %@ · erforderlich %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "instalado %@ · necesario %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "instalado %@ · necessário %@" + } + } + } + }, + "installed %@, tested %@": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "installed %@, tested %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "installato %@, testato %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "installé %@, testé %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "installiert %@, getestet %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "instalado %@, probado %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "instalado %@, testado %@" + } + } + } + }, + "local.nxlibrary.build.io": { + "comment": "BuildService.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.build.io" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.build.io" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.build.io" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.build.io" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.build.io" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.build.io" + } + } + } + }, + "local.nxlibrary.saves": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.saves" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.saves" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.saves" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.saves" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.saves" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.saves" + } + } + } + }, + "local.nxlibrary.sounds": { + "comment": "SoundEffectService.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.sounds" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.sounds" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.sounds" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.sounds" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.sounds" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "local.nxlibrary.sounds" + } + } + } + }, + "macOS could not launch %@.": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "macOS could not launch %@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "macOS non ha potuto avviare %@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "macOS n’a pas pu lancer %@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "macOS konnte %@ nicht starten." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "macOS no ha podido abrir %@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O macOS não conseguiu abrir %@." + } + } + } + }, + "main has moved on: %@%@.": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "main has moved on: %@%@." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "main è avanzato: %@%@." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "main a progressé : %@%@." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "main hat sich weiterentwickelt: %@%@." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "main ha avanzado: %@%@." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "main avançou: %@%@." + } + } + } + }, + "missing": { + "comment": "NXNativeEnvironment.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "missing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "non disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "manquant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "fehlt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "falta" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ausente" + } + } + } + }, + "missing %@": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "missing %@" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "manca %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "%@ manquant" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ fehlt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "falta %@" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "falta %@" + } + } + } + }, + "none": { + "comment": "SeleneArtBackground.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "none" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "nessuna" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "aucune" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "keine" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "ninguno" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ausente" + } + } + } + }, + "not found": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "not found" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "non trovato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "introuvable" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "nicht gefunden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "no encontrado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "não encontrado" + } + } + } + }, + "nxlibrary.setup-check": { + "comment": "SetupCheckModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "nxlibrary.setup-check" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "nxlibrary.setup-check" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "nxlibrary.setup-check" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "nxlibrary.setup-check" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "nxlibrary.setup-check" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "nxlibrary.setup-check" + } + } + } + }, + "nxnative CLI": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "nxnative CLI" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "CLI di nxnative" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "CLI nxnative" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "nxnative-CLI" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "CLI de nxnative" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "CLI do nxnative" + } + } + } + }, + "nxnative engine": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "nxnative engine" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Motore nxnative" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Moteur nxnative" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "nxnative-Engine" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Motor nxnative" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Engine nxnative" + } + } + } + }, + "nxnative has %lld newer commits. Engine updates reach you with NX Library updates, once tested.": { + "comment": "Quantity-sensitive UI message; preserve %lld count and other placeholders.", + "localizations": { + "en": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "nxnative has %lld newer commit. Engine updates reach you with NX Library updates, once tested." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "nxnative has %lld newer commits. Engine updates reach you with NX Library updates, once tested." + } + } + } + } + }, + "it": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "nxnative ha %lld commit più recente. Gli aggiornamenti del motore arrivano con gli aggiornamenti di NX Library, dopo essere stati testati." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "nxnative ha %lld commit più recenti. Gli aggiornamenti del motore arrivano con gli aggiornamenti di NX Library, dopo essere stati testati." + } + } + } + } + }, + "fr": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "nxnative compte %lld commit plus récent. Les mises à jour du moteur vous parviennent avec celles de NX Library, une fois testées." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "nxnative compte %lld commits plus récents. Les mises à jour du moteur vous parviennent avec celles de NX Library, une fois testées." + } + } + } + } + }, + "de": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "nxnative hat %lld neueren Commit. Engine-Updates erhältst du mit NX Library-Updates, sobald sie getestet wurden." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "nxnative hat %lld neuere Commits. Engine-Updates erhältst du mit NX Library-Updates, sobald sie getestet wurden." + } + } + } + } + }, + "es": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "nxnative tiene %lld commit más reciente. Las actualizaciones del motor te llegan con las de NX Library, una vez probadas." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "nxnative tiene %lld commits más recientes. Las actualizaciones del motor te llegan con las de NX Library, una vez probadas." + } + } + } + } + }, + "pt-BR": { + "variations": { + "plural": { + "one": { + "stringUnit": { + "state": "translated", + "value": "O nxnative tem %lld commit mais recente. As atualizações da engine chegam com as atualizações do NX Library, após serem testadas." + } + }, + "other": { + "stringUnit": { + "state": "translated", + "value": "O nxnative tem %lld commits mais recentes. As atualizações da engine chegam com as atualizações do NX Library, após serem testadas." + } + } + } + } + } + } + }, + "nxnative, used as it is upstream, at the commit this NX Library is tested with.": { + "comment": "SetupCheck.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "nxnative, used as it is upstream, at the commit this NX Library is tested with." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "nxnative, usato senza modifiche rispetto a upstream, al commit con cui è testata questa NX Library." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "nxnative, utilisé tel quel depuis le dépôt amont, au commit avec lequel cette version de NX Library a été testée." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "nxnative, unverändert aus dem Upstream, mit dem Commit, mit dem diese NX Library-Version getestet wurde." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "nxnative, sin modificar respecto al proyecto original, en el commit con el que se ha probado este NX Library." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "nxnative, usado sem alterações do repositório original, no commit com o qual este NX Library foi testado." + } + } + } + }, + "picture": { + "comment": "Display copy only; internal values and paths remain unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "picture" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "immagine" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "image" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Bild" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "imagen" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "imagem" + } + } + } + }, + "prod.keys": { + "comment": "SettingsEngineSection.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "prod.keys" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "prod.keys" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "prod.keys" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "prod.keys" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "prod.keys" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "prod.keys" + } + } + } + }, + "prod.keys imported: header key present, %@ application key generations.": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "prod.keys imported: header key present, %@ application key generations." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "prod.keys importato: chiave header presente, %@ generazioni di chiavi applicazione." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "prod.keys importé : clé d’en-tête présente, %@ générations de clés d’application." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "prod.keys importiert: Header-Schlüssel vorhanden, %@ Anwendungsschlüsselgenerationen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "prod.keys importado: clave de cabecera presente, %@ generaciones de claves de aplicación." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "prod.keys importado: chave de cabeçalho presente, %@ gerações de chaves de aplicativo." + } + } + } + }, + "prod.keys is missing, unreadable or has no header_key.": { + "comment": "PortingFailureReason.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "prod.keys is missing, unreadable or has no header_key." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "prod.keys non è disponibile, non è leggibile o non contiene header_key." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "prod.keys est absent, illisible ou ne contient pas de header_key." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "prod.keys fehlt, ist nicht lesbar oder enthält keinen header_key." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "prod.keys falta, no se puede leer o no contiene header_key." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "prod.keys está ausente, ilegível ou sem header_key." + } + } + } + }, + "prod.keys is missing. Import it in Settings.": { + "comment": "NXLibraryError.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "prod.keys is missing. Import it in Settings." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "prod.keys non è disponibile. Importalo in Impostazioni." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "prod.keys est introuvable. Importez-le dans les Réglages." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "prod.keys fehlt. Importiere die Datei in den Einstellungen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Falta prod.keys. Impórtalo en Ajustes." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "O arquivo prod.keys não foi encontrado. Importe-o nos Ajustes." + } + } + } + }, + "regionName": { + "comment": "LaunchBoxArt.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "regionName" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "regionName" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "regionName" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "regionName" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "regionName" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "regionName" + } + } + } + }, + "required options present": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "required options present" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "opzioni richieste presenti" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "options requises présentes" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "erforderliche Optionen vorhanden" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "opciones necesarias presentes" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "opções obrigatórias presentes" + } + } + } + }, + "switched to": { + "comment": "AppViewModel+NewestEngine.swift:69", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "switched to" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "passato a" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "basculé vers" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "gewechselt zu" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "cambiado a" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "trocada para" + } + } + } + }, + "tab": { + "comment": "ButtonPrompts.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "tab" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "tab" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "tab" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Tab" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "tab" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "tab" + } + } + } + }, + "terminal": { + "comment": "BuildLiveLogsDrawer.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "terminal" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "terminale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "terminal" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "terminal" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "terminal" + } + } + } + }, + "the bundled Python is missing; reinstall NX Library": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "the bundled Python is missing; reinstall NX Library" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "il Python incluso non è disponibile; reinstalla NX Library" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "le Python intégré est introuvable ; réinstallez NX Library" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "das mitgelieferte Python fehlt; installiere NX Library erneut" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "falta el Python incluido; reinstala NX Library" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "o Python incluído não foi encontrado; reinstale o NX Library" + } + } + } + }, + "the drive": { + "comment": "Display copy only; internal values and paths remain unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "the drive" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "il disco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "le disque" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "das Laufwerk" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "la unidad" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "a unidade" + } + } + } + }, + "the environment check is still running": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "the environment check is still running" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "il controllo dell’ambiente è ancora in corso" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "la vérification de l’environnement est toujours en cours" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "die Umgebungsprüfung läuft noch" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "la comprobación del entorno sigue en curso" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "a verificação do ambiente ainda está em andamento" + } + } + } + }, + "the game": { + "comment": "Fallback display name when no game name is available.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "the game" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "il gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "le jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "das Spiel" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "el juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "o jogo" + } + } + } + }, + "the game’s files": { + "comment": "Display copy only; internal values and paths remain unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "the game’s files" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "i file del gioco" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "les fichiers du jeu" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "die Dateien des Spiels" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "los archivos del juego" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "os arquivos do jogo" + } + } + } + }, + "the nxnative venv Python is missing": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "the nxnative venv Python is missing" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "il Python del venv di nxnative non è disponibile" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "le Python de l’environnement venv de nxnative est introuvable" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "das Python der nxnative-venv fehlt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "falta el Python del entorno virtual de nxnative" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "o Python do venv do nxnative não foi encontrado" + } + } + } + }, + "this session": { + "comment": "SessionCard.swift:91", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "this session" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "questa sessione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "cette session" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "diese Sitzung" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "esta sesión" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "nesta sessão" + } + } + } + }, + "total": { + "comment": "SessionCard.swift:95", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "total" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "totale" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "au total" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "insgesamt" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "total" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "total" + } + } + } + }, + "truncated": { + "comment": "CheatFile.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "truncated" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "troncato" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "tronqué" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "abgeschnitten" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "truncado" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "truncado" + } + } + } + }, + "turn them back on here or in the game's Cheats panel (⌘⇧C).": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "turn them back on here or in the game's Cheats panel (⌘⇧C)." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "riattivali qui o nel pannello Trucchi del gioco (⌘⇧C)." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "réactivez-les ici ou dans le panneau Codes de triche du jeu (⌘⇧C)." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "aktiviere sie hier oder im Cheats-Bereich des Spiels (⌘⇧C) erneut." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "vuelve a activarlos aquí o en el panel de trucos del juego (⌘⇧C)." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "ative-as novamente aqui ou no painel Trapaças do jogo (⌘⇧C)." + } + } + } + }, + "under a minute": { + "comment": "Compact playtime units, metadata heading or accessible duration.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "under a minute" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "meno di un minuto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "moins d’une minute" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "unter einer Minute" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "menos de un minuto" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "menos de um minuto" + } + } + } + }, + "unknown error": { + "comment": "NewestEngine.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "unknown error" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "errore sconosciuto" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "erreur inconnue" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "unbekannter Fehler" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "error desconocido" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "erro desconhecido" + } + } + } + }, + "updated from %@ to": { + "comment": "AppViewModel+NewestEngine.swift:69", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "updated from %@ to" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "aggiornato da %@ a" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "mis à jour de %@ vers" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "von %@ aktualisiert auf" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "actualizado de %@ a" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "atualizada de %@ para" + } + } + } + }, + "version %@ (build %@)": { + "comment": "CheatCopy.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "version %@ (build %@)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "versione %@ (build %@)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "version %@ (build %@)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version %@ (Build %@)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "versión %@ (compilación %@)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "versão %@ (build %@)" + } + } + } + }, + "version unavailable (requires 3.12+)": { + "comment": "CompatibilityChecker.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "version unavailable (requires 3.12+)" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "versione non disponibile (richiede 3.12+)" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "version indisponible (nécessite 3.12+)" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Version nicht verfügbar (3.12+ erforderlich)" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "versión no disponible (requiere 3.12+)" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "versão indisponível (requer 3.12+)" + } + } + } + }, + "video": { + "comment": "Display copy only; internal values and paths remain unchanged.", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "video" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "video" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "vidéo" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Video" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "vídeo" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "vídeo" + } + } + } + }, + "wait for storage cleanup to finish": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "wait for storage cleanup to finish" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "attendi il termine della pulizia dello spazio di archiviazione" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "attendez la fin du nettoyage du stockage" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "warte, bis die Speicherbereinigung abgeschlossen ist" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "espera a que termine la limpieza del almacenamiento" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "aguarde o término da limpeza do armazenamento" + } + } + } + }, + "wait for the app icon update to finish": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "wait for the app icon update to finish" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "attendi il termine dell’aggiornamento dell’icona dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "attendez la fin de la mise à jour de l’icône de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "warte, bis das App-Symbol aktualisiert wurde" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "espera a que termine la actualización del icono de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "aguarde o término da atualização do ícone do app" + } + } + } + }, + "wait for the app update to finish": { + "comment": "AppViewModel.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "wait for the app update to finish" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "attendi il termine dell’aggiornamento dell’app" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "attendez la fin de la mise à jour de l’app" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "warte, bis das App-Update abgeschlossen ist" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "espera a que termine la actualización de la app" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "aguarde o término da atualização do app" + } + } + } + }, + "⌘K": { + "comment": "ButtonPrompts.swift:44", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "⌘K" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "⌘K" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "⌘K" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "⌘K" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "⌘K" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "⌘K" + } + } + } + }, + "⌘Return to save · Empty for Automatic": { + "comment": "ChangeMetadataView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "⌘Return to save · Empty for Automatic" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "⌘Invio per salvare · Lascia vuoto per Automatico" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "⌘Retour pour enregistrer · Laisser vide pour Automatique" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "⌘Return zum Speichern · Leer lassen für „Automatisch“" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "⌘Retorno para guardar · Vacío para Automático" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "⌘Return para salvar · Deixe em branco para Automático" + } + } + } + }, + "◂ ▸ Move": { + "comment": "SpotlightView.swift", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "◂ ▸ Move" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "◂ ▸ Sposta" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "◂ ▸ Déplacer" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "◂ ▸ Verschieben" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "◂ ▸ Mover" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "◂ ▸ Mover" + } + } + } + } + }, + "version": "1.0" +} diff --git a/Sources/NXLibraryCore/Services/AppIconWriter.swift b/Sources/NXLibraryCore/Services/AppIconWriter.swift index 98203e0..4b4ec1d 100644 --- a/Sources/NXLibraryCore/Services/AppIconWriter.swift +++ b/Sources/NXLibraryCore/Services/AppIconWriter.swift @@ -20,10 +20,10 @@ public struct AppIconWriter { public var errorDescription: String? { switch self { - case .notAnApp: return "The game's app was not found." - case .notAnImage: return "The icon is not an image NX Library can read." - case .iconFailed(let detail): return "The icon could not be made: \(detail)" - case .signingFailed(let detail): return "The app could not be signed again, so it was left as it was: \(detail)" + case .notAnApp: return L10n.text("The game's app was not found.") + case .notAnImage: return L10n.text("The icon is not an image NX Library can read.") + case .iconFailed(let detail): return L10n.format("The icon could not be made: %@", [String(describing: (detail))]) + case .signingFailed(let detail): return L10n.format("The app could not be signed again, so it was left as it was: %@", [String(describing: (detail))]) } } } diff --git a/Sources/NXLibraryCore/Services/AppLocalization.swift b/Sources/NXLibraryCore/Services/AppLocalization.swift new file mode 100644 index 0000000..52f6dbc --- /dev/null +++ b/Sources/NXLibraryCore/Services/AppLocalization.swift @@ -0,0 +1,72 @@ +import Foundation +import Combine + +/// The selected locale is captured by background work, while views observe changes on the main thread. +public final class AppLocalization: ObservableObject { + public static let shared = AppLocalization() + @Published public private(set) var context = AppLocale(language: .english, region: "US") + + public func configure(_ settings: AppSettings) { + let next = AppLocale(language: settings.appLanguage, region: settings.appRegion) + guard next != context else { return } + L10n.setContext(next) + context = next + } +} + +public enum L10n { + private static let lock = NSLock() + private static var selected = AppLocale(language: .english, region: "US") + public static var context: AppLocale { + lock.lock(); defer { lock.unlock() } + return selected + } + public static func setContext(_ context: AppLocale) { + lock.lock(); defer { lock.unlock() } + selected = context + } + + /// Installed apps find their resources before considering SwiftPM's development location. + public static func resources(in app: URL? = nil) -> Bundle? { + let root = app ?? Bundle.main.bundleURL + for folder in [root.appendingPathComponent("Contents/Resources"), root] { + if let bundle = Bundle(url: folder.appendingPathComponent("NXLibrary_NXLibraryCore.bundle")) { return bundle } + } + return app == nil ? Bundle.module : nil + } + + private static func languageBundle(_ language: AppLanguage, in resources: Bundle) -> Bundle? { + guard let path = resources.path(forResource: language.rawValue, ofType: "lproj") else { return nil } + return Bundle(path: path) + } + + /// At most one retained bundle per supported language; never cache interpolated user data or mutable copy. + private static let defaultLanguageBundles: [AppLanguage: Bundle] = { + guard let resources = resources() else { return [:] } + var bundles: [AppLanguage: Bundle] = [:] + for language in AppLanguage.allCases where language != .automatic { + bundles[language] = languageBundle(language, in: resources) + } + return bundles + }() + + public static func text(_ key: String, context: AppLocale? = nil, bundle: Bundle? = nil) -> String { + let context = context ?? Self.context + // Explicit test/import bundles must never reuse another bundle's resources. + let localized: Bundle? + if let bundle { localized = languageBundle(context.language, in: bundle) } + else { localized = defaultLanguageBundles[context.language] } + guard let localized else { return key } + return localized.localizedString(forKey: key, value: key, table: "Localizable") + } + + public static func format(_ key: String, _ arguments: [CVarArg], context: AppLocale? = nil) -> String { + let context = context ?? Self.context + return String(format: text(key, context: context), locale: context.formatLocale, arguments: arguments) + } + + public static func count(_ key: String, _ count: Int, arguments: [CVarArg] = [], context: AppLocale? = nil) -> String { + let context = context ?? Self.context + return String(format: text(key, context: context), locale: context.formatLocale, arguments: [count] + arguments) + } +} diff --git a/Sources/NXLibraryCore/Services/ArtworkPicker.swift b/Sources/NXLibraryCore/Services/ArtworkPicker.swift index c42496e..89800bb 100644 --- a/Sources/NXLibraryCore/Services/ArtworkPicker.swift +++ b/Sources/NXLibraryCore/Services/ArtworkPicker.swift @@ -8,10 +8,10 @@ public enum ArtworkSlot: String, CaseIterable, Codable, Sendable { public var title: String { switch self { - case .cover: return "Cover" - case .background: return "Background" - case .icon: return "Icon" - case .logo: return "Logo" + case .cover: return L10n.text("Cover") + case .background: return L10n.text("Background") + case .icon: return L10n.text("Icon") + case .logo: return L10n.text("Logo") } } @@ -181,12 +181,12 @@ public enum ArtworkCandidates { for (index, url) in (titleDB?.screenshots ?? []).enumerated() { add("Title DB screenshot \(index + 1)", url) } - add("Nintendo store", storeHeader) - add("Nintendo store", storeImage) + add(L10n.text("Nintendo store"), storeHeader) + add(L10n.text("Nintendo store"), storeImage) case .icon: add("Title DB icon", titleDB?.icon) case .logo: - add("Nintendo store", storeLogo) + add(L10n.text("Nintendo store"), storeLogo) } // Everything found online (`OnlineArtwork`) after the Nintendo sources, in its order. for candidate in online where candidate.slot == slot && !list.contains(where: { $0.url == candidate.url }) { @@ -237,7 +237,7 @@ public final class ArtworkPicks: ObservableObject { public enum Failure: LocalizedError { case notAnImage - public var errorDescription: String? { "That file is not an image NX Library can read." } + public var errorDescription: String? { L10n.text("That file is not an image NX Library can read.") } } public let directory: URL diff --git a/Sources/NXLibraryCore/Services/BuildOutputInstaller.swift b/Sources/NXLibraryCore/Services/BuildOutputInstaller.swift index 65c617b..901f05c 100644 --- a/Sources/NXLibraryCore/Services/BuildOutputInstaller.swift +++ b/Sources/NXLibraryCore/Services/BuildOutputInstaller.swift @@ -7,8 +7,8 @@ public enum BuildInstallError: Error, LocalizedError { case replaceFailed(String) public var errorDescription: String? { switch self { - case .invalidApp(let p): return "The built app is invalid: \(p)" - case .replaceFailed(let m): return "Could not replace the app: \(m)" + case .invalidApp(let p): return L10n.format("The built app is invalid: %@", [String(describing: (p))]) + case .replaceFailed(let m): return L10n.format("Could not replace the app: %@", [String(describing: (m))]) } } } diff --git a/Sources/NXLibraryCore/Services/BuildService.swift b/Sources/NXLibraryCore/Services/BuildService.swift index 1c27028..74c71c2 100644 --- a/Sources/NXLibraryCore/Services/BuildService.swift +++ b/Sources/NXLibraryCore/Services/BuildService.swift @@ -29,7 +29,7 @@ public struct BuildResult: Equatable { public final class BuildService { private let fm: FileManager - private let ioQueue = DispatchQueue(label: "local.nxlibrary.build.io") + private let ioQueue = DispatchQueue(label: L10n.text("local.nxlibrary.build.io")) private let stateLock = NSLock() private var process: Process? private var logHandle: FileHandle? @@ -39,8 +39,16 @@ public final class BuildService { private var eventHandler: ((BuildEvent) -> Void)? private var completion: ((BuildResult) -> Void)? private var sleepAssertion: IOPMAssertionID? + /// How long a stop waits before it kills what is left. Since nxnative 0b124486 every tool the build script + /// runs leads a session of its own, which a signal to the build's process group does not reach: on SIGTERM + /// the script stops that session itself, killing what is left of it after 5 s (dm/tools/proc_group.py GRACE), + /// then exits with 143. Killing the script before then would leave its tools running with no parent. + private let stopGrace: TimeInterval - public init(fileManager: FileManager = .default) { self.fm = fileManager } + public init(fileManager: FileManager = .default, stopGrace: TimeInterval = 8) { + self.fm = fileManager + self.stopGrace = stopGrace + } deinit { // A backstop for callers that drop the service without cancelling. @@ -48,7 +56,7 @@ public final class BuildService { let running = process stateLock.unlock() if let running, running.isRunning { - killpg(running.processIdentifier, SIGKILL) + Self.kill(tree: running.processIdentifier) } } @@ -159,20 +167,42 @@ public final class BuildService { stateLock.unlock() guard let process, process.isRunning else { return } killpg(process.processIdentifier, SIGTERM) - DispatchQueue.global().asyncAfter(deadline: .now() + 5) { - if process.isRunning { killpg(process.processIdentifier, SIGKILL) } + DispatchQueue.global().asyncAfter(deadline: .now() + stopGrace) { + if process.isRunning { Self.kill(tree: process.processIdentifier) } } } - /// Synchronous backstop for app termination. The normal cancel path gives - /// the toolchain a grace period; shutting down cannot wait for it. + /// Synchronous stop for app termination: the same SIGTERM as a cancel, so the engine stops its tools, waiting at + /// most `stopGrace` (a build that stops normally ends in well under a second), then everything still running. public func terminateNow() { stateLock.lock() cancelled = true let process = self.process stateLock.unlock() guard let process, process.isRunning else { return } - killpg(process.processIdentifier, SIGKILL) + killpg(process.processIdentifier, SIGTERM) + let deadline = Date().addingTimeInterval(stopGrace) + while process.isRunning, Date() < deadline { usleep(50_000) } + if process.isRunning { Self.kill(tree: process.processIdentifier) } + } + + /// SIGKILL to the build's process group and to every process descended from its leader, listed before any + /// is killed: a tool in a session of its own is still the script's child while the script runs. + static func kill(tree leader: pid_t) { + var found: [pid_t] = [] + var pending = [leader] + while let parent = pending.popLast() { + let count = proc_listchildpids(parent, nil, 0) + guard count > 0 else { continue } + var children = [pid_t](repeating: 0, count: Int(count) + 32) + let listed = proc_listchildpids(parent, &children, Int32(children.count * MemoryLayout<pid_t>.size)) + for child in children.prefix(max(0, Int(listed))) where child > 0 && !found.contains(child) { + found.append(child) + pending.append(child) + } + } + killpg(leader, SIGKILL) + for pid in found { Darwin.kill(pid, SIGKILL) } } private func consume(_ data: Data, isError: Bool) { diff --git a/Sources/NXLibraryCore/Services/CloudSavesPreferences.swift b/Sources/NXLibraryCore/Services/CloudSavesPreferences.swift index 0686d80..cf7620a 100644 --- a/Sources/NXLibraryCore/Services/CloudSavesPreferences.swift +++ b/Sources/NXLibraryCore/Services/CloudSavesPreferences.swift @@ -30,9 +30,9 @@ public struct CloudSavesPreferences { public var errorDescription: String? { switch self { case .iCloudDriveOff: - return "iCloud Drive is not on for this Mac. Turn it on in System Settings › Apple Account › iCloud, or choose a folder." + return L10n.text("iCloud Drive is not on for this Mac. Turn it on in System Settings › Apple Account › iCloud, or choose a folder.") case .folderUnavailable(let path): - return "Could not use \(path) for cloud saves." + return L10n.format("Could not use %@ for cloud saves.", [String(describing: (path))]) } } } diff --git a/Sources/NXLibraryCore/Services/CompatibilityChecker.swift b/Sources/NXLibraryCore/Services/CompatibilityChecker.swift index 58f0428..029e390 100644 --- a/Sources/NXLibraryCore/Services/CompatibilityChecker.swift +++ b/Sources/NXLibraryCore/Services/CompatibilityChecker.swift @@ -80,12 +80,12 @@ public final class CompatibilityChecker { let head = run("/usr/bin/git", ["rev-parse", "HEAD"]).stdout .trimmingCharacters(in: .whitespacesAndNewlines) - checks.append(.init(id: "commit", title: "Engine commit", + checks.append(.init(id: "commit", title: L10n.text("Engine commit"), status: head == expectedEngineCommit ? .pass : .block, - detail: "current \(head), expected \(expectedEngineCommit)")) + detail: L10n.format("current %@, expected %@", [String(describing: (head)), String(describing: (expectedEngineCommit))]))) let dirty = Self.relevantChanges(run("/usr/bin/git", ["status", "--porcelain"]).stdout) - checks.append(.init(id: "tree", title: "Working tree", + checks.append(.init(id: "tree", title: L10n.text("Working tree"), status: dirty.isEmpty ? .pass : .block, detail: dirty.joined(separator: "\n"))) @@ -94,9 +94,9 @@ public final class CompatibilityChecker { let required = requiredMetalVersion() ?? "unknown" let standing = Self.metalStanding(installed: metal, required: required) let installedNumber = Self.metalNumber(metal) ?? (metal.isEmpty ? "not found" : metal) - checks.append(.init(id: "metal", title: "Metal compiler", + checks.append(.init(id: "metal", title: L10n.text("Metal compiler"), status: standing == .same ? .pass : .block, - detail: "installed \(installedNumber), tested \(Self.metalNumber(required) ?? required)")) + detail: L10n.format("installed %@, tested %@", [String(describing: (installedNumber)), String(describing: (Self.metalNumber(required) ?? required))]))) let pythonVersion = run(pythonURL.path, [ "-c", "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}')" @@ -104,19 +104,19 @@ public final class CompatibilityChecker { let versionParts = pythonVersion.split(separator: ".").compactMap { Int($0) } let pythonOK = versionParts.count >= 2 && (versionParts[0] > 3 || (versionParts[0] == 3 && versionParts[1] >= 12)) - checks.append(.init(id: "python", title: "Python venv", + checks.append(.init(id: "python", title: L10n.text("Python venv"), status: pythonOK ? .pass : .block, - detail: pythonVersion.isEmpty ? "version unavailable (requires 3.12+)" - : "\(pythonVersion) (requires 3.12+)")) + detail: pythonVersion.isEmpty ? L10n.text("version unavailable (requires 3.12+)") + : L10n.format("%@ (requires 3.12+)", [String(describing: (pythonVersion))]))) let help = run(pythonURL.path, [ repoURL.appendingPathComponent("scripts/build_ipa_from_nsp.py").path, "-h" ]).stdout let requiredFlags = ["--platform", "--progress", "-o", "-k", "--app-name", "--bundle-id"] let missing = requiredFlags.filter { !help.contains($0) } - checks.append(.init(id: "cli", title: "nxnative CLI", + checks.append(.init(id: "cli", title: L10n.text("nxnative CLI"), status: missing.isEmpty ? .pass : .block, - detail: missing.isEmpty ? "required options present" : "missing \(missing.joined(separator: ", "))")) + detail: missing.isEmpty ? L10n.text("required options present") : L10n.format("missing %@", [String(describing: (missing.joined(separator: ", ")))]))) return CompatibilityReport(ok: checks.allSatisfy { $0.status != .block }, checks: checks, metal: standing) } diff --git a/Sources/NXLibraryCore/Services/DiagnosticsReport.swift b/Sources/NXLibraryCore/Services/DiagnosticsReport.swift index bd966f3..86aaad1 100644 --- a/Sources/NXLibraryCore/Services/DiagnosticsReport.swift +++ b/Sources/NXLibraryCore/Services/DiagnosticsReport.swift @@ -20,7 +20,6 @@ public enum DiagnosticsReport { public var metalFound: String? = nil public var metalCertified: String? = nil public var untestedOverride = false - public var dotnet: String? = nil public var python: String? = nil public var setupRows: [(name: String, ready: Bool)] = [] public var keysImported = false @@ -57,7 +56,6 @@ public enum DiagnosticsReport { "- Xcode: \(value(i.xcodePath)) \(value(i.xcodeVersion))", "- Metal compiler: \(value(i.metalFound)) (certified: \(value(i.metalCertified)))", "- Untested-toolchain override: \(i.untestedOverride ? "on" : "off")", - "- .NET SDK: \(value(i.dotnet))", "- Python: \(value(i.python))", ""] lines += ["## Setup"] + i.setupRows.map { "- \($0.name): \($0.ready ? "ready" : "missing")" } + [""] lines += ["## Keys", diff --git a/Sources/NXLibraryCore/Services/DotnetRelease.swift b/Sources/NXLibraryCore/Services/DotnetRelease.swift deleted file mode 100644 index 100ce3a..0000000 --- a/Sources/NXLibraryCore/Services/DotnetRelease.swift +++ /dev/null @@ -1,68 +0,0 @@ -import Foundation -import CryptoKit - -public struct DotnetPackage: Equatable { - public let version: String - public let url: URL - public let sha512: String -} - -/// Microsoft's own installer for the .NET 10 SDK, found through its release -/// metadata and checked against the hash published there before it is opened. -public enum DotnetRelease { - public static let metadataURL = URL(string: "https://builds.dotnet.microsoft.com/dotnet/release-metadata/10.0/releases.json")! - - public static func trustedDownloadURL(_ url: URL) -> Bool { - url.scheme?.lowercased() == "https" && url.user == nil && url.password == nil - && (url.port == nil || url.port == 443) - && ["builds.dotnet.microsoft.com", "download.visualstudio.microsoft.com"].contains(url.host?.lowercased() ?? "") - } - - public enum Failure: LocalizedError { - case noPackage - public var errorDescription: String? { "Microsoft's release list has no .NET 10 SDK installer for this Mac." } - } - - private struct Metadata: Decodable { - let latestSDK: String? - let releases: [Release]? - enum CodingKeys: String, CodingKey { case latestSDK = "latest-sdk", releases } - } - private struct Release: Decodable { let sdk: SDK?; let sdks: [SDK]? } - private struct SDK: Decodable { let version: String; let files: [File] } - private struct File: Decodable { let rid: String?; let url: String; let hash: String } - - public static func latestSDKPackage(from json: Data) throws -> DotnetPackage { - let metadata = try JSONDecoder().decode(Metadata.self, from: json) - guard let latest = metadata.latestSDK else { throw Failure.noPackage } - let sdks = (metadata.releases ?? []).flatMap { [$0.sdk].compactMap { $0 } + ($0.sdks ?? []) } - guard let sdk = sdks.first(where: { $0.version == latest }), - let file = sdk.files.first(where: { $0.rid == "osx-arm64" && $0.url.hasSuffix(".pkg") }), - let url = URL(string: file.url), trustedDownloadURL(url), - file.hash.utf8.count == 128, - file.hash.utf8.allSatisfy({ (48...57).contains($0) || (65...70).contains($0) || (97...102).contains($0) }) - else { throw Failure.noPackage } - return DotnetPackage(version: sdk.version, url: url, sha512: file.hash) - } - - public static func verify(file: URL, sha512: String) throws -> Bool { - let handle = try FileHandle(forReadingFrom: file) - defer { try? handle.close() } - var hasher = SHA512() - while let chunk = try handle.read(upToCount: 1 << 20), !chunk.isEmpty { - hasher.update(data: chunk) - } - let hex = hasher.finalize().map { String(format: "%02x", $0) }.joined() - return hex == sha512.lowercased() - } -} - -/// The installer and its metadata keep the same HTTPS trust boundary on redirects. -public final class DotnetDownloadPolicy: NSObject, URLSessionTaskDelegate { - public func urlSession(_ session: URLSession, task: URLSessionTask, - willPerformHTTPRedirection response: HTTPURLResponse, - newRequest request: URLRequest, - completionHandler: @escaping (URLRequest?) -> Void) { - completionHandler(request.url.map(DotnetRelease.trustedDownloadURL) == true ? request : nil) - } -} diff --git a/Sources/NXLibraryCore/Services/EnginePackageReader.swift b/Sources/NXLibraryCore/Services/EnginePackageReader.swift index 8e6b4e1..e40fe75 100644 --- a/Sources/NXLibraryCore/Services/EnginePackageReader.swift +++ b/Sources/NXLibraryCore/Services/EnginePackageReader.swift @@ -31,12 +31,12 @@ public enum EngineSourceError: Error, Equatable, LocalizedError { case unreadablePackage(String) public var errorDescription: String? { switch self { - case .invalidPackage: return "The package contents could not be read. Check the files and imported keys." + case .invalidPackage: return L10n.text("The package contents could not be read. Check the files and imported keys.") case .unreadablePackages(let detail): return "The packages could not be read: \(detail)" case .unreadablePackage(let detail): return "This package could not be read: \(detail)" case .invalidDump(let detail): return "The extracted folder is incomplete: \(detail)." case .invalidContainer(let detail): return "The NUP file could not be read: \(detail)." - case .containerNotAlone: return "Choose one NUP file on its own: it holds the whole game." + case .containerNotAlone: return L10n.text("Choose one NUP file on its own: it holds the whole game.") case .missingProfile(let title): return "The selected engine has no registered profile for \(title). With this engine, extracted folders and NUP files need a registered profile." } } @@ -99,7 +99,8 @@ try: with contextlib.redirect_stdout(io.StringIO()): spec.loader.exec_module(engine) environment = dict(os.environ) - engine.configure_dotnet(environment) + if hasattr(engine, 'configure_dotnet'): # engines before nxnative 641636e2 (.NET tools) + engine.configure_dotnet(environment) if hasattr(engine, 'package_reader'): with engine.package_reader(repo, keys, environment) as reader: found = reader.contents([p for p, _ in packages]) diff --git a/Sources/NXLibraryCore/Services/GameAppDestination.swift b/Sources/NXLibraryCore/Services/GameAppDestination.swift index e2a93f0..eb75c09 100644 --- a/Sources/NXLibraryCore/Services/GameAppDestination.swift +++ b/Sources/NXLibraryCore/Services/GameAppDestination.swift @@ -44,10 +44,10 @@ public enum GameAppDestination { guard folder.isFileURL, folder.standardizedFileURL.path != "/", fm.fileExists(atPath: folder.path, isDirectory: &isDirectory), isDirectory.boolValue, fm.isReadableFile(atPath: folder.path), fm.isExecutableFile(atPath: folder.path) else { - return "The Game apps folder is unavailable. Reconnect it or choose another in Settings → Library." + return L10n.text("The Game apps folder is unavailable. Reconnect it or choose another in Settings → Library.") } guard fm.isWritableFile(atPath: folder.path) else { - return "The app folder is read-only: \(folder.path). Allow writing here before building." + return L10n.format("The app folder is read-only: %@. Allow writing here before building.", [String(describing: (folder.path))]) } return nil } diff --git a/Sources/NXLibraryCore/Services/GameBanana/GameBananaBandModel.swift b/Sources/NXLibraryCore/Services/GameBanana/GameBananaBandModel.swift index 030114d..5311a7b 100644 --- a/Sources/NXLibraryCore/Services/GameBanana/GameBananaBandModel.swift +++ b/Sources/NXLibraryCore/Services/GameBanana/GameBananaBandModel.swift @@ -248,12 +248,12 @@ public final class GameBananaBandModel: ObservableObject { /// The band's status line, under its title. public var statusLine: String? { switch phase { - case .looking: return "Looking for mods that work…" - case .ask: return "Is this the right game?" - case .noPage: return "No GameBanana page found" - case .noneWork: return "No mods for this game work with NX Library yet" - case .offline: return "No connection. Mods you installed still work." - case .failed: return "GameBanana didn't answer. Try again in a moment." + case .looking: return L10n.text("Looking for mods that work…") + case .ask: return L10n.text("Is this the right game?") + case .noPage: return L10n.text("No GameBanana page found") + case .noneWork: return L10n.text("No mods for this game work with NX Library yet") + case .offline: return L10n.text("No connection. Mods you installed still work.") + case .failed: return L10n.text("GameBanana didn't answer. Try again in a moment.") case .idle, .matching, .ready: return nil } } diff --git a/Sources/NXLibraryCore/Services/GameBanana/ModCatalog.swift b/Sources/NXLibraryCore/Services/GameBanana/ModCatalog.swift index 92dabeb..db7602a 100644 --- a/Sources/NXLibraryCore/Services/GameBanana/ModCatalog.swift +++ b/Sources/NXLibraryCore/Services/GameBanana/ModCatalog.swift @@ -6,13 +6,13 @@ public enum ModCategory: String, CaseIterable, Codable, Sendable { public var title: String { switch self { - case .characters: return "Characters & skins" - case .worlds: return "Worlds & stages" - case .graphics: return "Graphics & effects" - case .sound: return "Music & sound" - case .interface: return "Interface & text" - case .gameplay: return "Gameplay" - case .more: return "More mods" + case .characters: return L10n.text("Characters & skins") + case .worlds: return L10n.text("Worlds & stages") + case .graphics: return L10n.text("Graphics & effects") + case .sound: return L10n.text("Music & sound") + case .interface: return L10n.text("Interface & text") + case .gameplay: return L10n.text("Gameplay") + case .more: return L10n.text("More mods") } } @@ -57,10 +57,10 @@ public enum ModStoreSort: String, CaseIterable, Codable, Sendable { public var title: String { switch self { - case .popular: return "Popular" - case .liked: return "Most liked" - case .newest: return "Newest" - case .updated: return "Recently updated" + case .popular: return L10n.text("Popular") + case .liked: return L10n.text("Most liked") + case .newest: return L10n.text("Newest") + case .updated: return L10n.text("Recently updated") } } @@ -193,13 +193,13 @@ public enum ModStoreLayout { } public static func sections(games: [ModCatalogGame], sort: ModStoreSort) -> [ModStoreSection] { - var sections = [ModStoreSection(kind: .sort, title: "Sort", entries: [])] + var sections = [ModStoreSection(kind: .sort, title: L10n.text("Sort"), entries: [])] var seen = Set<Int>() // A mod listed under two games (shared by both) appears once, under the first. let all = sorted(games.flatMap(\.entries).filter { seen.insert($0.id).inserted }, by: sort) guard !all.isEmpty else { return sections } - sections.append(ModStoreSection(kind: .featured, title: "Featured", entries: featured(all))) + sections.append(ModStoreSection(kind: .featured, title: L10n.text("Featured"), entries: featured(all))) sections.append(ModStoreSection(kind: .popular, title: sort.rowTitle, entries: Array(all.prefix(rowLimit)))) for category in ModCategory.allCases { let entries = all.filter { $0.category == category } diff --git a/Sources/NXLibraryCore/Services/GameBanana/ModInstaller.swift b/Sources/NXLibraryCore/Services/GameBanana/ModInstaller.swift index 04bca2e..b099693 100644 --- a/Sources/NXLibraryCore/Services/GameBanana/ModInstaller.swift +++ b/Sources/NXLibraryCore/Services/GameBanana/ModInstaller.swift @@ -7,15 +7,15 @@ public enum ModInstallError: Error, Equatable { public var message: String { switch self { - case .notAllowed: return "Quit the game to install mods." - case .download: return "The download didn't finish." - case .damaged: return "The download was damaged. Try again." - case .unreadable: return "This archive can't be opened." - case .unsafe: return "This archive has files outside its folder, so it wasn't installed." - case .noRomFS: return "This file has no romfs folder, so it can't work." - case .noSpace: return "There isn't enough space to install this mod." + case .notAllowed: return L10n.text("Quit the game to install mods.") + case .download: return L10n.text("The download didn't finish.") + case .damaged: return L10n.text("The download was damaged. Try again.") + case .unreadable: return L10n.text("This archive can't be opened.") + case .unsafe: return L10n.text("This archive has files outside its folder, so it wasn't installed.") + case .noRomFS: return L10n.text("This file has no romfs folder, so it can't work.") + case .noSpace: return L10n.text("There isn't enough space to install this mod.") // The reason stays out of the sentence: the player gets plain words, not a system error. - case .writeFailed: return "The mod couldn't be copied into the game's folder. Try again." + case .writeFailed: return L10n.text("The mod couldn't be copied into the game's folder. Try again.") } } } @@ -34,7 +34,7 @@ public enum ModInstallResult: Equatable { /// The sentence to show, if any. public var message: String? { - if case .installedOff = self { return "Installed, but too many mods are on. Turn one off first." } + if case .installedOff = self { return L10n.text("Installed, but too many mods are on. Turn one off first.") } return nil } } diff --git a/Sources/NXLibraryCore/Services/GameTDB.swift b/Sources/NXLibraryCore/Services/GameTDB.swift index 060dfcb..02a0216 100644 --- a/Sources/NXLibraryCore/Services/GameTDB.swift +++ b/Sources/NXLibraryCore/Services/GameTDB.swift @@ -39,7 +39,7 @@ public enum GameTDB { public enum Failure: LocalizedError { case unreadable - public var errorDescription: String? { "GameTDB's database could not be read." } + public var errorDescription: String? { L10n.text("GameTDB's database could not be read.") } } public static func parse(_ data: Data) throws -> [GameTDBEntry] { diff --git a/Sources/NXLibraryCore/Services/InstallProgress.swift b/Sources/NXLibraryCore/Services/InstallProgress.swift index e39608a..563f21c 100644 --- a/Sources/NXLibraryCore/Services/InstallProgress.swift +++ b/Sources/NXLibraryCore/Services/InstallProgress.swift @@ -45,8 +45,8 @@ public struct InstallProgress: Equatable { public static func bytes(done: Int64, total: Int64) -> InstallProgress { let mb = { (value: Int64) in Int((Double(value) / 1_048_576).rounded()) } - guard total > 0 else { return InstallProgress(fraction: nil, label: "\(mb(done)) MB") } - return InstallProgress(fraction: min(1, Double(done) / Double(total)), label: "\(mb(done)) of \(mb(total)) MB") + guard total > 0 else { return InstallProgress(fraction: nil, label: L10n.format("%@ MB", [String(describing: (mb(done)))])) } + return InstallProgress(fraction: min(1, Double(done) / Double(total)), label: L10n.format("%@ of %@ MB", [String(describing: (mb(done))), String(describing: (mb(total)))])) } } diff --git a/Sources/NXLibraryCore/Services/KeyStore.swift b/Sources/NXLibraryCore/Services/KeyStore.swift index 826a580..7261f27 100644 --- a/Sources/NXLibraryCore/Services/KeyStore.swift +++ b/Sources/NXLibraryCore/Services/KeyStore.swift @@ -17,9 +17,9 @@ public enum KeyStoreError: Error, LocalizedError { case invalidLine(Int) public var errorDescription: String? { switch self { - case .unreadable: return "The key file could not be read." - case .missingHeaderKey: return "The key file has no header_key." - case .invalidLine(let line): return "Invalid key line number \(line)." + case .unreadable: return L10n.text("The key file could not be read.") + case .missingHeaderKey: return L10n.text("The key file has no header_key.") + case .invalidLine(let line): return L10n.format("Invalid key line number %@.", [String(describing: (line))]) } } } diff --git a/Sources/NXLibraryCore/Services/LaunchBoxArt.swift b/Sources/NXLibraryCore/Services/LaunchBoxArt.swift index ef0fd7a..994bc7e 100644 --- a/Sources/NXLibraryCore/Services/LaunchBoxArt.swift +++ b/Sources/NXLibraryCore/Services/LaunchBoxArt.swift @@ -109,10 +109,10 @@ public enum LaunchBoxArt { (value(object, key) as? NSNumber).map(\.intValue).flatMap { $0 > 0 ? $0 : nil } } return array.compactMap { entry in - guard let object = entry as? [String: Any], let kind = text(object, "imageTypeName"), - let preview = text(object, "imageFileName"), let full = text(object, "fullGameImageFileName"), + guard let object = entry as? [String: Any], let kind = text(object, L10n.text("imageTypeName")), + let preview = text(object, L10n.text("imageFileName")), let full = text(object, L10n.text("fullGameImageFileName")), safeName(preview), safeName(full) else { return nil } - return PayloadImage(kind: kind, region: text(object, "regionName"), preview: preview, full: full, + return PayloadImage(kind: kind, region: text(object, L10n.text("regionName")), preview: preview, full: full, width: number(object, "fullGameImageWidth"), height: number(object, "fullGameImageHeight")) } } diff --git a/Sources/NXLibraryCore/Services/MetadataPicker.swift b/Sources/NXLibraryCore/Services/MetadataPicker.swift index b18324b..0f14263 100644 --- a/Sources/NXLibraryCore/Services/MetadataPicker.swift +++ b/Sources/NXLibraryCore/Services/MetadataPicker.swift @@ -7,11 +7,11 @@ public enum MetadataField: String, CaseIterable, Codable, Sendable { public var title: String { switch self { - case .description: return "Description" - case .genres: return "Genres" - case .developer: return "Developer" - case .publisher: return "Publisher" - case .releaseDate: return "Release date" + case .description: return L10n.text("Description") + case .genres: return L10n.text("Genres") + case .developer: return L10n.text("Developer") + case .publisher: return L10n.text("Publisher") + case .releaseDate: return L10n.text("Release date") } } } @@ -115,7 +115,9 @@ public final class MetadataPicks: ObservableObject { var result = metadata for (key, value) in picks { switch MetadataField(rawValue: key) { - case .description: result.description = value + case .description: + result.description = value + result.descriptionLanguage = "custom" case .genres: result.genres = value.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } case .developer: result.developers = [value] case .publisher: result.publishers = [value] diff --git a/Sources/NXLibraryCore/Services/ModStore.swift b/Sources/NXLibraryCore/Services/ModStore.swift index baf0c10..b6f7057 100644 --- a/Sources/NXLibraryCore/Services/ModStore.swift +++ b/Sources/NXLibraryCore/Services/ModStore.swift @@ -30,7 +30,7 @@ public struct ModConflict: Equatable, Sendable { self.lower = lower } - public var message: String { "\(upper) and \(lower) change the same files: the upper one wins." } + public var message: String { L10n.format("%@ and %@ change the same files: the upper one wins.", [String(describing: (upper)), String(describing: (lower))]) } } public enum ModStoreError: Error, Equatable { diff --git a/Sources/NXLibraryCore/Services/NXNativeEnvironment.swift b/Sources/NXLibraryCore/Services/NXNativeEnvironment.swift index 29eabe6..2d2de25 100644 --- a/Sources/NXLibraryCore/Services/NXNativeEnvironment.swift +++ b/Sources/NXLibraryCore/Services/NXNativeEnvironment.swift @@ -98,10 +98,7 @@ public final class NXNativeEnvironment { let metal = MetalVersion.parse(run("/usr/bin/xcrun", ["metal", "--version"]).stdout) if metal == nil { issues.append("Metal compiler not found") } - // The engine's script runs `dotnet build` itself, so it needs the SDK, not only the runtime. - let sdks = run("/usr/bin/env", ["dotnet", "--list-sdks"]).stdout - let dotnetOK = sdks.split(separator: "\n").contains { $0.hasPrefix("10.") } - if !dotnetOK { issues.append("the .NET 10 SDK is missing") } + // Since nxnative 641636e2 the shader translator and nx_extract are committed native executables: no .NET. // A Mac build needs no xcodegen, and SPIRV-Cross is the engine's own in-repo build. var tools: [ToolStatus] = [] @@ -110,7 +107,7 @@ public final class NXNativeEnvironment { let result = run("/usr/bin/env", [tool, "--version"]) let ok = result.status == 0 tools.append(ToolStatus(id: tool, name: tool, ok: ok, - detail: ok ? "available" : "missing")) + detail: ok ? L10n.text("available") : L10n.text("missing"))) if !ok { issues.append("\(tool) is missing") } } diff --git a/Sources/NXLibraryCore/Services/NXNativeGameSettings.swift b/Sources/NXLibraryCore/Services/NXNativeGameSettings.swift index 4aef6fa..6d57c84 100644 --- a/Sources/NXLibraryCore/Services/NXNativeGameSettings.swift +++ b/Sources/NXLibraryCore/Services/NXNativeGameSettings.swift @@ -29,8 +29,8 @@ public enum GameSettingsError: LocalizedError, Equatable { public var errorDescription: String? { switch self { - case .notOffered(let env): return "This build has no \(env) setting." - case .refused(let env, let value): return "This build does not take \(env)=\(value)." + case .notOffered(let env): return L10n.format("This build has no %@ setting.", [String(describing: (env))]) + case .refused(let env, let value): return L10n.format("This build does not take %@=%@.", [String(describing: (env)), String(describing: (value))]) } } } diff --git a/Sources/NXLibraryCore/Services/NewestEngine.swift b/Sources/NXLibraryCore/Services/NewestEngine.swift index a581941..3397513 100644 --- a/Sources/NXLibraryCore/Services/NewestEngine.swift +++ b/Sources/NXLibraryCore/Services/NewestEngine.swift @@ -42,38 +42,42 @@ public enum UpstreamCommitMark: Equatable, Sendable { /// The words every screen uses for the newest engine, so Settings and Setup say the same thing. public enum NewestEngineCopy { - public static let note = "Newest nxnative, not yet tested by NX Library. It usually works, but something may not. " - + "Its new features reach NX Library in a future version." + public static var note: String { + L10n.text("Newest nxnative, not yet tested by NX Library. It usually works, but something may not. ") + + L10n.text("Its new features reach NX Library in a future version.") + } public static func ahead(_ count: Int) -> String { - "\(count) commit\(count == 1 ? "" : "s") ahead" + L10n.count("%lld commits ahead", count, arguments: []) } /// Settings → Engine, Newest engine row, while the tested engine is in use. public static func offer(commit: String, aheadBy: Int) -> String { - "Build with nxnative main (\(commit.prefix(7)), \(ahead(aheadBy))) before NX Library has tested it." + L10n.format("Build with nxnative main (%@, %@) before NX Library has tested it.", [String(describing: (commit.prefix(7))), String(describing: (ahead(aheadBy)))]) } public static func needsNewerApp(metal: String) -> String { - "The newest nxnative needs Metal \(metal), so it needs a newer NX Library." + L10n.format("The newest nxnative needs Metal %@, so it needs a newer NX Library.", [String(describing: (metal))]) } /// Setup's engine card while the newest engine is in use (the note is its description). public static func setupDetail(choice: NewestEngineChoice, tested: String) -> String { - "At \(choice.shortCommit), \(ahead(choice.aheadBy)) of the tested \(tested.prefix(7)), chosen in Settings → Engine." + L10n.format("At %@, %@ of the tested %@, chosen in Settings → Engine.", [String(describing: (choice.shortCommit)), String(describing: (ahead(choice.aheadBy))), String(describing: (tested.prefix(7)))]) } /// Settings → Engine, Newest engine row, while main is past the commit in use. `aheadBy` counts from the pin, /// like the choice's own count; a rewritten main (not further ahead) gets no count. public static func update(choice: NewestEngineChoice, main: String, aheadBy: Int) -> String { let newer = aheadBy - choice.aheadBy - let count = newer > 0 ? ", \(newer) commit\(newer == 1 ? "" : "s") newer" : "" - return "main has moved on: \(main.prefix(7))\(count)." + let count = newer > 0 ? L10n.count(", %lld commits newer", newer, arguments: []) : "" + return L10n.format("main has moved on: %@%@.", [String(describing: (main.prefix(7))), String(describing: (count))]) } - public static let info = "Builds games with nxnative main instead of the engine this NX Library is tested with. " - + "Builds are recorded as untested and the untested-toolchain override stays on. " - + "Back to tested returns to the tested engine; games you built keep playing." + public static var info: String { + L10n.text("Builds games with nxnative main instead of the engine this NX Library is tested with. ") + + L10n.text("Builds are recorded as untested and the untested-toolchain override stays on. ") + + L10n.text("Back to tested returns to the tested engine; games you built keep playing.") + } } public enum NewestEngineError: Error, Equatable, LocalizedError { @@ -85,13 +89,13 @@ public enum NewestEngineError: Error, Equatable, LocalizedError { public var errorDescription: String? { switch self { case .localChanges: - return "The engine folder has changed files. NX Library never touches them: put them aside, then try again." + return L10n.text("The engine folder has changed files. NX Library never touches them: put them aside, then try again.") case .downloadFailed: - return "The newest nxnative could not be downloaded. Check the connection, then try again." + return L10n.text("The newest nxnative could not be downloaded. Check the connection, then try again.") case .needsNewerApp(let metal): return NewestEngineCopy.needsNewerApp(metal: metal) case .switchFailed(let detail): - return "The engine could not be switched: \(detail)" + return L10n.format("The engine could not be switched: %@", [String(describing: (detail))]) } } } @@ -183,7 +187,7 @@ public struct NewestEngineSwitcher { private func git(_ args: [String]) -> CommandResult { (try? runner.run(URL(fileURLWithPath: "/usr/bin/git"), ["-C", repo.path] + args, environment: environment, currentDirectory: nil)) - ?? CommandResult(status: -1, stdout: "", stderr: "git could not run") + ?? CommandResult(status: -1, stdout: "", stderr: L10n.text("git could not run")) } private func requireClean() throws { @@ -235,6 +239,6 @@ public struct NewestEngineSwitcher { } static func firstLine(_ text: String) -> String { - text.split(separator: "\n").first.map { String($0).trimmingCharacters(in: .whitespaces) } ?? "unknown error" + text.split(separator: "\n").first.map { String($0).trimmingCharacters(in: .whitespaces) } ?? L10n.text("unknown error") } } diff --git a/Sources/NXLibraryCore/Services/NintendoStoreMetadata.swift b/Sources/NXLibraryCore/Services/NintendoStoreMetadata.swift index a4d976a..9315284 100644 --- a/Sources/NXLibraryCore/Services/NintendoStoreMetadata.swift +++ b/Sources/NXLibraryCore/Services/NintendoStoreMetadata.swift @@ -17,6 +17,10 @@ public struct NintendoStoreMetadata: Codable, Equatable { public var screenshotURLs: [URL] = [] /// Where `screenshotURLs` came from when not the store or Title DB ("LaunchBox"); shown, never saved. public var screenshotSource: String? + public var descriptionLanguage = "en" + public var localeKey: String? = nil + /// The last successful response for the requested description language, including an empty translation. + public var descriptionCheckedAt: Date? public init(titleId: String, name: String, description: String, developers: [String], publishers: [String], genres: [String], releaseDate: String?, storeURL: URL, @@ -40,11 +44,15 @@ public struct NintendoStoreMetadata: Codable, Equatable { private enum CodingKeys: String, CodingKey { case titleId, name, description, developers, publishers, genres, releaseDate + case descriptionLanguage, localeKey, descriptionCheckedAt case storeURL, imageURL, headerImageURL, logoURL, nsuid, fetchedAt, screenshotURLs } public init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) + descriptionLanguage = try c.decodeIfPresent(String.self, forKey: .descriptionLanguage) ?? "en" + localeKey = try c.decodeIfPresent(String.self, forKey: .localeKey) + descriptionCheckedAt = try c.decodeIfPresent(Date.self, forKey: .descriptionCheckedAt) titleId = try c.decode(String.self, forKey: .titleId) name = try c.decode(String.self, forKey: .name) description = try c.decode(String.self, forKey: .description) @@ -76,10 +84,10 @@ public enum NintendoStoreError: Error, Equatable, LocalizedError { public var errorDescription: String? { switch self { - case .notFound: return "No Nintendo Store entry was found for this game." - case .invalidURL: return "The Nintendo Store URL is invalid." - case .http(let status): return "Nintendo Store returned status \(status)." - case .decoding: return "Nintendo Store returned a response this app could not read." + case .notFound: return L10n.text("No Nintendo Store entry was found for this game.") + case .invalidURL: return L10n.text("The Nintendo Store URL is invalid.") + case .http(let status): return L10n.format("Nintendo Store returned status %@.", [String(describing: (status))]) + case .decoding: return L10n.text("Nintendo Store returned a response this app could not read.") } } } @@ -120,6 +128,7 @@ public final class NintendoStoreMetadataService: @unchecked Sendable { private let fileManager: FileManager private let refreshLock = NSLock() private var screenshotRefreshTried: Set<String> = [] + private var descriptionRetryAfter: [String: Date] = [:] public init( client: NintendoStoreHTTPClient = URLSessionNintendoStoreClient(), @@ -132,9 +141,26 @@ public final class NintendoStoreMetadataService: @unchecked Sendable { self.fileManager = fileManager } - public func cached(for titleId: String) -> NintendoStoreMetadata? { - guard let data = try? Data(contentsOf: cacheURL(for: titleId)) else { return nil } - return try? JSONDecoder().decode(NintendoStoreMetadata.self, from: data) + public func cached(for titleId: String, locale: AppLocale = AppLocale(language: .english, region: "US")) -> NintendoStoreMetadata? { + func read(_ url: URL) -> NintendoStoreMetadata? { + guard let data = try? Data(contentsOf: url), + let metadata = try? JSONDecoder().decode(NintendoStoreMetadata.self, from: data), + metadata.titleId == titleId else { return nil } + return metadata + } + if let exact = read(cacheURL(for: titleId, locale: locale)) { return exact } + // The provider localizes descriptions by language, not by formatting region. + var alternatives: [NintendoStoreMetadata] = [] + if locale.language == .english, + let legacy = read(cacheURL(for: titleId, locale: AppLocale(language: .english, region: "US"))) { + alternatives.append(legacy) + } + let root = cacheDirectory.appendingPathComponent("localized") + for folder in (try? fileManager.contentsOfDirectory(at: root, includingPropertiesForKeys: nil)) ?? [] + where folder.lastPathComponent.hasPrefix(locale.language.rawValue + "_") { + if let entry = read(folder.appendingPathComponent(Self.safeFileName(titleId) + ".json")) { alternatives.append(entry) } + } + return alternatives.max { $0.fetchedAt < $1.fetchedAt } } /// Whether `metadata` can be used without asking the network again: younger @@ -142,12 +168,20 @@ public final class NintendoStoreMetadataService: @unchecked Sendable { /// A titledb-style title (16 characters, "0100" prefix) with no screenshots /// is re-fetched once per app session (per service instance); offline, that /// one attempt fails and the cached entry is used for the rest of the session. - public func isFresh(_ metadata: NintendoStoreMetadata, now: Date = Date()) -> Bool { + public func isFresh(_ metadata: NintendoStoreMetadata, now: Date = Date(), locale: AppLocale = AppLocale(language: .english, region: "US")) -> Bool { + guard (metadata.localeKey ?? "en_US").hasPrefix(locale.language.rawValue + "_") else { return false } guard now.timeIntervalSince(metadata.fetchedAt) < Self.ttl else { return false } + if locale.metadataLanguage != "en", metadata.descriptionLanguage != locale.metadataLanguage { + refreshLock.lock() + let retryAfter = descriptionRetryAfter[metadata.titleId + "/" + locale.metadataLanguage] + refreshLock.unlock() + guard metadata.descriptionCheckedAt.map({ now.timeIntervalSince($0) < 24 * 3600 }) == true + || retryAfter.map({ now < $0 }) == true else { return false } + } guard metadata.screenshotURLs.isEmpty, Self.looksLikeTitleDBID(metadata.titleId) else { return true } refreshLock.lock() defer { refreshLock.unlock() } - return screenshotRefreshTried.contains(metadata.titleId) + return screenshotRefreshTried.contains(metadata.titleId + "/" + locale.metadataLanguage) } /// Synchronous so the lock is never held across a suspension point. @@ -158,30 +192,82 @@ public final class NintendoStoreMetadataService: @unchecked Sendable { } private static func looksLikeTitleDBID(_ id: String) -> Bool { - id.count == 16 && id.hasPrefix("0100") + id.count == 16 && (id.hasPrefix("0100") || id.hasPrefix("0400")) && id.allSatisfy(\.isHexDigit) } - public func metadata(for game: PortedGame) async throws -> NintendoStoreMetadata { + public func metadata(for game: PortedGame, locale: AppLocale = AppLocale(language: .english, region: "US")) async throws -> NintendoStoreMetadata { let key = game.titleId ?? game.uniqueKey - if let cached = cached(for: key) { - if isFresh(cached) { return cached } + let previous = cached(for: key, locale: locale) + if let cached = cached(for: key, locale: locale) { + if isFresh(cached, locale: locale) { return cached } if cached.screenshotURLs.isEmpty, Self.looksLikeTitleDBID(cached.titleId) { - markScreenshotRefreshTried(cached.titleId) + markScreenshotRefreshTried(cached.titleId + "/" + locale.metadataLanguage) } } + // A language switch reuses fresh artwork and credits; only the description needs a request. + if locale.metadataLanguage != "en", var base = previous ?? cached(for: key), + Date().timeIntervalSince(base.fetchedAt) < Self.ttl { + let item = await fetchTitleDBItem(titleId: key, language: locale.metadataLanguage) + applyDescription(item, to: &base, locale: locale) + if let item { Self.fill(&base, from: item) } + markScreenshotRefreshTried(key + "/" + locale.metadataLanguage) + base.localeKey = locale.cacheKey + try? cache(base, for: key, locale: locale) + return base + } + do { - let metadata = try await fetch(game: game, cacheKey: key) - try? cache(metadata, for: key) + var metadata: NintendoStoreMetadata + do { metadata = try await fetch(game: game, cacheKey: key) } + catch { + guard locale.metadataLanguage != "en", + let localized = await fetchTitleDB(titleId: key, language: locale.metadataLanguage), + !localized.description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw error } + metadata = localized + metadata.descriptionLanguage = locale.metadataLanguage + metadata.descriptionCheckedAt = Date() + } + if locale.metadataLanguage != "en", metadata.descriptionLanguage != locale.metadataLanguage { + let item = await fetchTitleDBItem(titleId: key, language: locale.metadataLanguage) + applyDescription(item, to: &metadata, locale: locale) + } + if metadata.descriptionLanguage != locale.metadataLanguage, + let previous, previous.descriptionLanguage == locale.metadataLanguage, + !previous.description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + metadata.description = previous.description + metadata.descriptionLanguage = previous.descriptionLanguage + metadata.fetchedAt = min(metadata.fetchedAt, previous.fetchedAt) + } + metadata.localeKey = locale.cacheKey + try? cache(metadata, for: key, locale: locale) return metadata } catch { // A stale cache is still better than an empty panel when Nintendo is // unreachable; the UI shows the fetched date. - if let cached = cached(for: key) { return cached } + if let cached = cached(for: key, locale: locale) { return cached } + if locale.cacheKey != "en_US", let english = cached(for: key) { return english } throw error } } + private func applyDescription(_ item: TitleDBItem?, to metadata: inout NintendoStoreMetadata, locale: AppLocale) { + if let item { + metadata.descriptionCheckedAt = Date() + if let text = item.description?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty, + text != metadata.description.trimmingCharacters(in: .whitespacesAndNewlines) + || metadata.descriptionLanguage == locale.metadataLanguage { + metadata.description = text + metadata.descriptionLanguage = locale.metadataLanguage + } + } else { + // A transient network failure is retried soon; it is not persisted as a missing translation. + refreshLock.lock() + descriptionRetryAfter[metadata.titleId + "/" + locale.metadataLanguage] = Date().addingTimeInterval(60) + refreshLock.unlock() + } + } + private func fetch(game: PortedGame, cacheKey: String) async throws -> NintendoStoreMetadata { var titleDBUsed = false var meta = try await fetchStore(game: game, cacheKey: cacheKey, titleDBUsed: &titleDBUsed) @@ -314,9 +400,9 @@ public final class NintendoStoreMetadataService: @unchecked Sendable { } } - private func fetchTitleDBItem(titleId: String) async -> TitleDBItem? { - guard titleId.count == 16, titleId.hasPrefix("0100"), - let url = URL(string: "https://api.nlib.cc/nx/\(titleId)?fields=name,publisher,description,banner,icon,screens,releaseDate,developer,category") else { + private func fetchTitleDBItem(titleId: String, language: String = "en") async -> TitleDBItem? { + guard Self.looksLikeTitleDBID(titleId), + let url = URL(string: "https://api.nlib.cc/nx/\(titleId)?fields=name,publisher,description,banner,icon,screens,releaseDate,developer,category&lang=\(language)") else { return nil } var request = URLRequest(url: url) @@ -330,8 +416,8 @@ public final class NintendoStoreMetadataService: @unchecked Sendable { } } - private func fetchTitleDB(titleId: String) async -> NintendoStoreMetadata? { - guard let item = await fetchTitleDBItem(titleId: titleId), + private func fetchTitleDB(titleId: String, language: String = "en") async -> NintendoStoreMetadata? { + guard let item = await fetchTitleDBItem(titleId: titleId, language: language), let name = item.name, !name.isEmpty else { return nil } return NintendoStoreMetadata( titleId: titleId, @@ -553,14 +639,17 @@ public final class NintendoStoreMetadataService: @unchecked Sendable { try? fileManager.removeItem(at: cacheDirectory) } - private func cacheURL(for titleId: String) -> URL { - cacheDirectory.appendingPathComponent(Self.safeFileName(titleId) + ".json") + private func cacheURL(for titleId: String, locale: AppLocale) -> URL { + let folder = locale.cacheKey == "en_US" ? cacheDirectory + : cacheDirectory.appendingPathComponent("localized/\(locale.cacheKey)") + return folder.appendingPathComponent(Self.safeFileName(titleId) + ".json") } - private func cache(_ metadata: NintendoStoreMetadata, for titleId: String) throws { - try fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true) + private func cache(_ metadata: NintendoStoreMetadata, for titleId: String, locale: AppLocale) throws { + let url = cacheURL(for: titleId, locale: locale) + try fileManager.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true) let data = try JSONEncoder().encode(metadata) - try data.write(to: cacheURL(for: titleId), options: .atomic) + try data.write(to: url, options: .atomic) } private static func safeFileName(_ value: String) -> String { diff --git a/Sources/NXLibraryCore/Services/SaveTransaction.swift b/Sources/NXLibraryCore/Services/SaveTransaction.swift index ac4db10..6bf69da 100644 --- a/Sources/NXLibraryCore/Services/SaveTransaction.swift +++ b/Sources/NXLibraryCore/Services/SaveTransaction.swift @@ -57,6 +57,7 @@ public final class SaveTransaction { private func replace(location: SaveLocation, identity: SaveIdentity, reason: SaveSnapshotReason, desired: (SaveSlot) -> URL, revalidate: () throws -> Void) throws -> SaveSnapshot { guard !Self.needsRecovery(location: location) else { throw SaveFailure.recoveryRequired } + guard !SaveFiles.exists(location.cloudPullJournal) else { throw SaveFailure.cloudPullPending } try check(location) let previous = try SaveFiles.inventory(location) let id = UUID(); let stage = staging(id, location) diff --git a/Sources/NXLibraryCore/Services/SavedChoicesStore.swift b/Sources/NXLibraryCore/Services/SavedChoicesStore.swift index f864d8d..32e379c 100644 --- a/Sources/NXLibraryCore/Services/SavedChoicesStore.swift +++ b/Sources/NXLibraryCore/Services/SavedChoicesStore.swift @@ -13,7 +13,7 @@ public enum SavedChoicesError: LocalizedError, Equatable { public var errorDescription: String? { switch self { - case .notSaved(let domain): return "Could not save the choice in \(domain)." + case .notSaved(let domain): return L10n.format("Could not save the choice in %@.", [String(describing: (domain))]) } } } diff --git a/Sources/NXLibraryCore/Services/SeleneImporter.swift b/Sources/NXLibraryCore/Services/SeleneImporter.swift index ff8216c..496b85f 100644 --- a/Sources/NXLibraryCore/Services/SeleneImporter.swift +++ b/Sources/NXLibraryCore/Services/SeleneImporter.swift @@ -48,7 +48,7 @@ public enum SeleneImportError: Error, Equatable, LocalizedError { case notASeleneFolder public var errorDescription: String? { - "That folder is not a Selene checkout. Pick the folder that contains theme/theme.yaml." + L10n.text("That folder is not a Selene checkout. Pick the folder that contains theme/theme.yaml.") } } @@ -91,7 +91,7 @@ public struct SeleneImporter { let audio = theme.appendingPathComponent("Audio") for name in Self.soundFiles { - let relative = "Sounds/\(name).wav" + let relative = L10n.format("Sounds/%@.wav", [String(describing: (name))]) let source = audio.appendingPathComponent("\(name).wav") if fileManager.fileExists(atPath: source.path) { try copy(source, to: relative) @@ -109,7 +109,7 @@ public struct SeleneImporter { options: [.skipsHiddenFiles])) ?? [] for file in files.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) { guard (try? file.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true else { continue } - let relative = "Wallpapers/\(folder.lastPathComponent)/\(file.lastPathComponent)" + let relative = L10n.format("Wallpapers/%@/%@", [String(describing: (folder.lastPathComponent)), String(describing: (file.lastPathComponent))]) try copy(file, to: relative) report.copied.append(relative) } diff --git a/Sources/NXLibraryCore/Services/SetupProbe.swift b/Sources/NXLibraryCore/Services/SetupProbe.swift index 93aafc0..45e92a3 100644 --- a/Sources/NXLibraryCore/Services/SetupProbe.swift +++ b/Sources/NXLibraryCore/Services/SetupProbe.swift @@ -49,8 +49,6 @@ public struct SetupProbe { xcodeStatus: xcode.status, metalVersion: MetalVersion.parse(run("/usr/bin/xcrun", ["metal", "--version"]).stdout), certifiedMetal: certifiedMetal, allowUntested: allowUntested, - dotnetSDKs: run("/usr/bin/env", ["dotnet", "--list-sdks"]).stdout - .split(separator: "\n").map(String.init).filter { !$0.isEmpty }, engine: engine, engineFolder: engineFolder, pinnedCommit: pinnedCommit, runtimeProblem: runtimeProblem(), keys: keys, keysError: keysError, diff --git a/Sources/NXLibraryCore/Services/SourceCatalog.swift b/Sources/NXLibraryCore/Services/SourceCatalog.swift index 10aa073..a962f7d 100644 --- a/Sources/NXLibraryCore/Services/SourceCatalog.swift +++ b/Sources/NXLibraryCore/Services/SourceCatalog.swift @@ -58,9 +58,9 @@ public enum SourceCatalogError: Error, LocalizedError { public var errorDescription: String? { switch self { case .unreadable(let url): - return "No readable title ID in \(url.lastPathComponent)." + return L10n.format("No readable title ID in %@.", [String(describing: (url.lastPathComponent))]) case .tooLarge(let url): - return "\(url.lastPathComponent) has an unsupported internal table size." + return L10n.format("%@ has an unsupported internal table size.", [String(describing: (url.lastPathComponent))]) } } } diff --git a/Sources/NXLibraryCore/Services/SteamGridDBArt.swift b/Sources/NXLibraryCore/Services/SteamGridDBArt.swift index 86da0ad..b351181 100644 --- a/Sources/NXLibraryCore/Services/SteamGridDBArt.swift +++ b/Sources/NXLibraryCore/Services/SteamGridDBArt.swift @@ -48,10 +48,10 @@ public enum SteamGridDBError: LocalizedError, Equatable { public var errorDescription: String? { switch self { - case .invalidKey: return "That is not a SteamGridDB API key. Copy it from Preferences → API in your SteamGridDB profile." - case .rejected: return "SteamGridDB did not accept that key. Check it in Preferences → API in your SteamGridDB profile." - case .unreachable: return "SteamGridDB could not be reached. Check the connection and try again." - case .notSaved: return "The key could not be saved on this Mac." + case .invalidKey: return L10n.text("That is not a SteamGridDB API key. Copy it from Preferences → API in your SteamGridDB profile.") + case .rejected: return L10n.text("SteamGridDB did not accept that key. Check it in Preferences → API in your SteamGridDB profile.") + case .unreachable: return L10n.text("SteamGridDB could not be reached. Check the connection and try again.") + case .notSaved: return L10n.text("The key could not be saved on this Mac.") } } } diff --git a/Sources/NXLibraryCore/Services/StorageManager.swift b/Sources/NXLibraryCore/Services/StorageManager.swift index b17444b..608f086 100644 --- a/Sources/NXLibraryCore/Services/StorageManager.swift +++ b/Sources/NXLibraryCore/Services/StorageManager.swift @@ -1,4 +1,16 @@ import Foundation +import Darwin + +public enum StorageCleanupError: LocalizedError { + case unsafeCache, unavailable + + public var errorDescription: String? { + switch self { + case .unsafeCache: return L10n.text("The cache folder could not be safely identified. Nothing was deleted.") + case .unavailable: return L10n.text("The folder is unavailable. Connect its drive and check folder access, then recalculate storage.") + } + } +} public struct StorageUsage: Equatable { public var libraryBytes: Int64 @@ -37,60 +49,148 @@ public final class StorageManager { } public func usage(games: [PortedGame]) -> StorageUsage { - var library: Int64 = 0 - var cache: Int64 = 0 - var logs: Int64 = 0 - for game in games { - library += size(of: game.url) - if let path = game.buildCachePath { - cache += size(of: URL(fileURLWithPath: path)) - } - if let titleId = game.titleId { - logs += size(of: logsRoot.appendingPathComponent("\(titleId).log")) - logs += size(of: buildLogURL(for: titleId)) + snapshot(games: games).total + } + + /// Logical file sizes, including hidden files, without traversing links. A path shared by library entries + /// is measured and counted once in the totals, but remains visible on each entry that uses it. + public func snapshot(games: [PortedGame], applicationsFolder: URL? = nil) -> StorageSnapshot { + var sizes: [String: StorageSize] = [:] + var apps = Set<String>(), caches = Set<String>(), logs = Set<String>() + func key(_ url: URL) -> String { url.resolvingSymlinksInPath().standardizedFileURL.path } + func measure(_ url: URL, in paths: inout Set<String>) -> StorageSize { + let path = key(url) + paths.insert(path) + if let value = sizes[path] { return value } + let value = size(of: URL(fileURLWithPath: path)) + sizes[path] = value + return value + } + let rows = games.map { game -> GameStorageUsage in + let app = game.url.pathExtension.lowercased() == "app" ? measure(game.url, in: &apps) : .missing + var cache: StorageSize = .missing + var shared: [String] = [] + if game.buildCachePath != nil { + if let url = try? cacheURL(for: game, protecting: games) { + cache = measure(url, in: &caches) + shared = games.filter { + $0.id != game.id && $0.buildCachePath.map { key(URL(fileURLWithPath: $0)) == key(url) } == true + }.map(\.name) + } else { cache = .unavailable } } + let logSizes = logURLs(for: game).map { measure($0, in: &logs) } + let logSize: StorageSize = logSizes.contains(.unavailable) ? .unavailable + : .bytes(logSizes.reduce(0) { $0 + ($1.knownBytes ?? 0) }) + return GameStorageUsage(id: game.id, name: game.name, app: app, cache: cache, logs: logSize, + canDeleteCache: (cache.knownBytes ?? 0) > 0, sharedCacheWith: shared) + }.sorted { + let left = $0.cache.knownBytes ?? 0, right = $1.cache.knownBytes ?? 0 + if left != right { return left > right } + let order = $0.name.localizedCaseInsensitiveCompare($1.name) + return order == .orderedSame ? $0.id.uuidString < $1.id.uuidString : order == .orderedAscending } - return StorageUsage( - libraryBytes: library, cacheBytes: cache, logsBytes: logs, - freeBytes: freeSpace(at: applicationsRoot) - ) + func total(_ paths: Set<String>) -> Int64 { paths.reduce(0) { $0 + (sizes[$1]?.knownBytes ?? 0) } } + let free = freeSpace(at: applicationsFolder ?? applicationsRoot) + return StorageSnapshot(games: rows, + total: StorageUsage(libraryBytes: total(apps), cacheBytes: total(caches), + logsBytes: total(logs), freeBytes: free ?? 0), freeBytes: free) } - public func deleteCache(for game: PortedGame) throws { - guard let path = game.buildCachePath else { return } - let url = URL(fileURLWithPath: path) - guard url.lastPathComponent.hasSuffix("_build") else { return } - if fm.fileExists(atPath: url.path) { try fm.removeItem(at: url) } + @discardableResult + public func deleteCache(for game: PortedGame, protecting games: [PortedGame] = []) throws -> Bool { + guard game.buildCachePath != nil else { return false } + let url = try cacheURL(for: game, protecting: games) + switch size(of: url) { + case .missing: return false + case .unavailable: throw StorageCleanupError.unavailable + case .bytes: break + } + try fm.removeItem(at: url) + return true } - public func deleteLogs(for game: PortedGame) throws { - guard let titleId = game.titleId else { return } - for url in [ - logsRoot.appendingPathComponent("\(titleId).log"), - buildLogURL(for: titleId), - ] where fm.fileExists(atPath: url.path) { + private func cacheURL(for game: PortedGame, protecting games: [PortedGame]) throws -> URL { + guard let title = game.titleId, title.count == 16, title.allSatisfy(\.isHexDigit), + let path = game.buildCachePath, path.hasPrefix("/"), + let workspace = game.workspacePath, workspace.hasPrefix("/") else { + throw StorageCleanupError.unsafeCache + } + let url = URL(fileURLWithPath: path).standardizedFileURL + guard url.lastPathComponent.lowercased() == "\(title.lowercased())_build", + url.deletingLastPathComponent().resolvingSymlinksInPath().standardizedFileURL.path + == URL(fileURLWithPath: workspace).resolvingSymlinksInPath().standardizedFileURL.path, + (try? fm.destinationOfSymbolicLink(atPath: url.path)) == nil else { + throw StorageCleanupError.unsafeCache + } + var isDirectory: ObjCBool = false + if fm.fileExists(atPath: url.path, isDirectory: &isDirectory), !isDirectory.boolValue { + throw StorageCleanupError.unsafeCache + } + let cache = url.resolvingSymlinksInPath().path + for entry in games + [game] { + let protected = [entry.url] + entry.sources.map(\.url) + + [entry.pendingInstall?.tempApp, entry.pendingInstall?.finalApp].compactMap { $0 } + guard !protected.contains(where: { + let path = $0.resolvingSymlinksInPath().path + return path == cache || path.hasPrefix(cache + "/") + }) else { throw StorageCleanupError.unsafeCache } + } + return url + } + + @discardableResult + public func deleteLogs(for game: PortedGame) throws -> Bool { + var removed = false + for url in logURLs(for: game) where fm.fileExists(atPath: url.path) { try fm.removeItem(at: url) + removed = true } + return removed } + private func logURLs(for game: PortedGame) -> [URL] { + guard let title = game.titleId, title.count == 16, title.allSatisfy(\.isHexDigit) else { return [] } + return [logsRoot.appendingPathComponent("\(title).log"), buildLogURL(for: title)] + } private func buildLogURL(for titleId: String) -> URL { supportRoot.appendingPathComponent("Logs").appendingPathComponent("\(titleId).log") } - private func size(of url: URL) -> Int64 { - var isDirectory: ObjCBool = false - guard fm.fileExists(atPath: url.path, isDirectory: &isDirectory) else { return 0 } - if !isDirectory.boolValue { - return Int64((try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0) + private func size(of url: URL) -> StorageSize { + let parts = url.standardizedFileURL.pathComponents + if parts.count > 2, parts[1] == "Volumes", + !fm.fileExists(atPath: "/Volumes/\(parts[2])") { return .unavailable } + var node = stat() + guard lstat(url.path, &node) == 0 else { + return errno == ENOENT ? .missing : .unavailable + } + if node.st_mode & S_IFMT == S_IFREG { return .bytes(Int64(node.st_size)) } + guard node.st_mode & S_IFMT == S_IFDIR else { return .unavailable } + var total: Int64 = 0 + var failed = false + url.withUnsafeFileSystemRepresentation { path in + guard let path, let root = strdup(path) else { failed = true; return } + defer { free(root) } + var roots: [UnsafeMutablePointer<CChar>?] = [root, nil] + guard let tree = fts_open(&roots, FTS_PHYSICAL | FTS_NOCHDIR, nil) else { failed = true; return } + defer { fts_close(tree) } + while let entry = fts_read(tree) { + switch Int32(entry.pointee.fts_info) { + case FTS_F: + if let stat = entry.pointee.fts_statp { total += Int64(stat.pointee.st_size) } + case FTS_DNR, FTS_ERR, FTS_NS: failed = true + default: break + } + } } - return DirectorySize.bytes(at: url) + return failed ? .unavailable : .bytes(total) } - private func freeSpace(at url: URL) -> Int64 { - let target = fm.fileExists(atPath: url.path) ? url : fm.homeDirectoryForCurrentUser - let values = try? target.resourceValues( + private func freeSpace(at url: URL) -> Int64? { + guard fm.fileExists(atPath: url.path) else { return nil } + let values = try? url.resourceValues( forKeys: [.volumeAvailableCapacityForImportantUsageKey]) - return values?.volumeAvailableCapacityForImportantUsage ?? 0 + return values?.volumeAvailableCapacityForImportantUsage } } diff --git a/Sources/NXLibraryCore/Services/UpstreamChecker.swift b/Sources/NXLibraryCore/Services/UpstreamChecker.swift index 417060b..f169eb2 100644 --- a/Sources/NXLibraryCore/Services/UpstreamChecker.swift +++ b/Sources/NXLibraryCore/Services/UpstreamChecker.swift @@ -59,18 +59,18 @@ public enum UpstreamCheckerError: Error, Equatable, LocalizedError { public var errorDescription: String? { switch self { case .invalidBase: - return "The engine commit is not a valid hash." + return L10n.text("The engine commit is not a valid hash.") case .invalidURL: - return "Could not build the GitHub API URL." + return L10n.text("Could not build the GitHub API URL.") case .http(let status, let message): - return "GitHub returned status \(status)\(message.map { ": \($0)" } ?? ".")" + return L10n.format("GitHub returned status %@%@", [String(describing: (status)), String(describing: (message.map { ": \($0)" } ?? "."))]) case .rateLimited(let reset): if let reset { - return "GitHub API rate limit reached. Try again after \(reset.formatted(date: .abbreviated, time: .shortened))." + return L10n.format("GitHub API rate limit reached. Try again after %@.", [String(describing: (reset.formatted(date: .abbreviated, time: .shortened)))]) } - return "GitHub API rate limit reached. Try again later." + return L10n.text("GitHub API rate limit reached. Try again later.") case .decoding: - return "GitHub returned a response this app could not read." + return L10n.text("GitHub returned a response this app could not read.") } } } diff --git a/Tests/NXLibraryCoreTests/AppLocaleTests.swift b/Tests/NXLibraryCoreTests/AppLocaleTests.swift new file mode 100644 index 0000000..4426999 --- /dev/null +++ b/Tests/NXLibraryCoreTests/AppLocaleTests.swift @@ -0,0 +1,157 @@ +import XCTest +@testable import NXLibraryCore + +final class AppLocaleTests: XCTestCase { + func testSettingsTitlesUseTheirExplicitContextWithoutChangingSectionIdentity() { + let original = L10n.context + defer { L10n.setContext(original) } + L10n.setContext(AppLocale(language: .english, region: "US")) + let identities = SettingsSection.allCases.map(\.id) + let italian = AppLocale(language: .italian, region: "IT") + let french = AppLocale(language: .french, region: "FR") + XCTAssertEqual(SettingsSection.games.title(in: italian), "Giochi") + XCTAssertEqual(SettingsSection.games.title(in: french), "Jeux") + XCTAssertEqual(SettingsSection.games.title, "Games") + XCTAssertEqual(SettingsSection.allCases.map(\.id), identities) + XCTAssertEqual(SettingsSection.startUp.id, "startUp") + } + + func testLanguageBundleReuseKeepsInjectedResourcesIsolated() throws { + let root = FileManager.default.temporaryDirectory.appendingPathComponent("NXLocalization-\(UUID()).bundle") + defer { try? FileManager.default.removeItem(at: root) } + let folder = root.appendingPathComponent("it.lproj") + try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) + let strings = ["Games": "INJECTED_TEST_VALUE"] + try PropertyListSerialization.data(fromPropertyList: strings, format: .xml, options: 0) + .write(to: folder.appendingPathComponent("Localizable.strings")) + let bundle = try XCTUnwrap(Bundle(url: root)) + let italian = AppLocale(language: .italian, region: "IT") + XCTAssertEqual(L10n.text("Games", context: italian), "Giochi") + XCTAssertEqual(L10n.text("Games", context: italian, bundle: bundle), "INJECTED_TEST_VALUE") + XCTAssertEqual(L10n.text("Games", context: italian), "Giochi") + XCTAssertEqual(L10n.text("Games", context: AppLocale(language: .english, region: "IT")), "Games") + } + + func testCommonGenresRefreshInEveryLanguageAndUnknownGenresStayVerbatim() { + let previous = L10n.context + defer { L10n.setContext(previous) } + let expected: [(AppLanguage, String, String)] = [ + (.english, "Action", "Adventure"), (.italian, "Azione", "Avventura"), + (.french, "Action", "Aventure"), (.german, "Action", "Abenteuer"), + (.spanish, "Acción", "Aventura"), (.portugueseBrazil, "Ação", "Aventura") + ] + for (language, action, adventure) in expected { + L10n.setContext(AppLocale(language: language, region: "CH")) + XCTAssertEqual(GameGenre.list(["Action", "Adventure", "Personal label"]), [action, adventure, "Personal label"]) + } + } + + func testAutomaticUsesFirstSupportedPreferenceAndIndependentRegion() { + let context = AppLocale(language: .automatic, region: "CH", preferredLanguages: ["nl-NL", "it-CH"]) + XCTAssertEqual(context.language, .italian) + XCTAssertEqual(context.region, "CH") + XCTAssertEqual(context.formatLocale.identifier, "it_CH") + XCTAssertEqual(context.metadataLanguage, "it") + } + + func testExplicitLanguageAndPortugueseMapping() { + let german = AppLocale(language: .german, region: "BR", preferredLanguages: ["it"]) + XCTAssertEqual(german.language, .german) + XCTAssertEqual(german.formatLocale.identifier, "de_BR") + let portuguese = AppLocale(language: .automatic, region: "PT", preferredLanguages: ["pt-PT"]) + XCTAssertEqual(portuguese.language, .portugueseBrazil) + XCTAssertEqual(portuguese.metadataLanguage, "pt") + XCTAssertEqual(AppLocale(language: .automatic, region: "US", preferredLanguages: ["ja", "nl"]).language, .english) + } + + func testSettingsMigrationAndInvalidPreferencesDoNotChangeGameLanguage() throws { + let old = try JSONDecoder().decode(AppSettings.self, from: Data("{}".utf8)) + XCTAssertEqual(old.appLanguage, .automatic) + XCTAssertNil(old.appRegion) + var settings = old + settings.appLanguage = .french + settings.appRegion = "CA" + let restored = try JSONDecoder().decode(AppSettings.self, from: JSONEncoder().encode(settings)) + XCTAssertEqual(restored.appLanguage, .french) + XCTAssertEqual(restored.appRegion, "CA") + XCTAssertEqual(restored.gameDefaults, old.gameDefaults) + let invalid = try JSONDecoder().decode(AppSettings.self, from: Data(#"{"appLanguage":"future","appRegion":"INVALID"}"#.utf8)) + XCTAssertEqual(invalid.appLanguage, .automatic) + XCTAssertNil(invalid.appRegion) + } + + func testSelectedLocaleFormatsGameSizeAndReleaseDate() { + let original = L10n.context + defer { L10n.setContext(original) } + let game = PortedGame(name: "Game", url: URL(fileURLWithPath: "/tmp/Game.app"), platform: .macApp, + sizeBytes: 1_500_000_000, modificationDate: Date()) + for (region, decimal) in [("US", "1.5"), ("DE", "1,5")] { + L10n.setContext(AppLocale(language: .english, region: region)) + let result = GameDetailPresentation(game: game, metadata: nil, totalPlaytime: 0, lastPlayed: nil) + XCTAssertTrue(result.size.contains(decimal), result.size) + } + } + + func testSaveSafetyCopyUsesTheSelectedLanguage() { + let original = L10n.context + defer { L10n.setContext(original) } + L10n.setContext(AppLocale(language: .italian, region: "US")) + XCTAssertEqual(SavePresentation.slotName(.account), "Salvataggio utente") + XCTAssertTrue(SavePresentation.closedNotice.contains("Finder")) + XCTAssertFalse(SavePresentation.closedNotice.hasPrefix("Keep the game")) + } + + func testCopyRefreshesAfterTheFirstLanguageWasAlreadyUsed() { + let original = L10n.context + defer { L10n.setContext(original) } + L10n.setContext(AppLocale(language: .english, region: "US")) + let english = NewestEngineCopy.note + let englishCheats = CheatCopy.about + L10n.setContext(AppLocale(language: .italian, region: "CH")) + XCTAssertNotEqual(NewestEngineCopy.note, english) + XCTAssertNotEqual(CheatCopy.about, englishCheats) + XCTAssertTrue(SettingsCatalog.search("lingua").contains { $0.id == "appLanguage" }) + } + + func testRegionSearchUsesLocalizedNamesAndStableCountryCodes() { + let context = AppLocale(language: .spanish, region: "US") + let choices = AppRegionOption.matching("Suiza", context: context) + XCTAssertEqual(choices.map(\.id), ["CH"]) + XCTAssertEqual(AppRegionOption.matching("CH", context: context).first { $0.id == "CH" }?.id, "CH") + XCTAssertTrue(AppRegionOption.matching("zzzznotacountry", context: context).isEmpty) + } + + func testTechnicalClassificationAndUserTitlesStayIndependentOfUILanguage() { + let original = L10n.context + defer { L10n.setContext(original) } + for language in AppLanguage.allCases where language != .automatic && language != .english { + L10n.setContext(AppLocale(language: language, region: "CH")) + let warning = BuildWarningCopy.text(stage: "Pipeline archive", summary: "RAW_INTERNAL_SENTINEL") + XCTAssertFalse(warning.contains("RAW_INTERNAL_SENTINEL"), language.rawValue) + let missingTicket = PortingFailureReason(kind: .keys, detail: "missing-common-ticket") + XCTAssertEqual(missingTicket.action, .none) + let game = PortedGame(name: "Play", url: URL(fileURLWithPath: "/tmp/Play.app"), platform: .macApp, + sizeBytes: 0, modificationDate: Date()) + XCTAssertEqual(LibraryPresentation(game: game, metadata: nil, totalPlaytime: 0, lastPlayed: nil).title, "Play") + } + } + + func testQuantitiesUseCatalogPluralForms() { + let context = AppLocale(language: .english, region: "US") + XCTAssertEqual(L10n.count("%lld games found.", 1, context: context), "1 game found.") + XCTAssertEqual(L10n.count("%lld games found.", 0, context: context), "0 games found.") + XCTAssertEqual(L10n.count("Detected %lld games in %@.", 2, arguments: ["My games"], context: context), "Detected 2 games in My games.") + } + + func testTranslationsAndFallbackPreserveUserData() { + let expected: [(AppLanguage, String)] = [(.italian, "Gioca"), (.french, "Jouer"), (.german, "Spielen"), (.spanish, "Jugar"), (.portugueseBrazil, "Jogar")] + for (language, value) in expected { + let context = AppLocale(language: language, region: "CH") + XCTAssertEqual(L10n.text("Play", context: context), value) + XCTAssertEqual(L10n.text("Unknown future message", context: context), "Unknown future message") + let rendered = L10n.format("Delete cache for %@?", ["Settings / Luigi’s Mansion"], context: context) + XCTAssertTrue(rendered.contains("Settings / Luigi’s Mansion")) + XCTAssertFalse(rendered.contains("%@")) + } + } +} diff --git a/Tests/NXLibraryCoreTests/BuildServiceTests.swift b/Tests/NXLibraryCoreTests/BuildServiceTests.swift index f216bad..ec76fb8 100644 --- a/Tests/NXLibraryCoreTests/BuildServiceTests.swift +++ b/Tests/NXLibraryCoreTests/BuildServiceTests.swift @@ -307,6 +307,86 @@ final class BuildServiceTests: XCTestCase { XCTAssertEqual(kill(pid, 0), -1) XCTAssertEqual(errno, ESRCH) } + + // MARK: - Tools in sessions of their own (nxnative 0b124486) + + /// A build script that runs `sleep 60` in a session of its own, as nxnative's proc_group does, and writes its pid. + /// `stops`: on SIGTERM it stops the tool's group and exits 143, as the engine does; otherwise it ignores SIGTERM. + private func sessionToolBuild(stops: Bool) throws -> (BuildCommand, URL, URL) { + let python = URL(fileURLWithPath: "/usr/bin/python3") + guard FileManager.default.isExecutableFile(atPath: python.path) else { throw XCTSkip("system Python unavailable") } + let root = FileManager.default.temporaryDirectory.appendingPathComponent("NXSession-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) + let pidFile = root.appendingPathComponent("tool.pid") + let script = """ + import os, signal, subprocess, sys, time + tool = subprocess.Popen(['/bin/sleep', '60'], start_new_session=True) + def stop(*_): + os.killpg(tool.pid, signal.SIGTERM); tool.wait(); sys.exit(143) + signal.signal(signal.SIGTERM, stop if \(stops ? "True" : "False") else signal.SIG_IGN) + open(sys.argv[1] + '.tmp', 'w').write(str(tool.pid)); os.rename(sys.argv[1] + '.tmp', sys.argv[1]) + time.sleep(60) + """ + let command = BuildCommand(executable: python, arguments: ["-I", "-c", script, pidFile.path], + environment: [:], workingDirectory: root) + return (command, root, pidFile) + } + + private func start(_ service: BuildService, _ command: BuildCommand, root: URL, pidFile: URL, + done: XCTestExpectation) throws -> pid_t { + service.start(command: command, logURL: root.appendingPathComponent("build.log"), + tempApp: root.appendingPathComponent("Temp.app"), finalApp: root.appendingPathComponent("Final.app"), + eventHandler: { _ in }, completion: { result in + XCTAssertTrue(result.cancelled) + done.fulfill() + }) + let deadline = Date().addingTimeInterval(10) + while !FileManager.default.fileExists(atPath: pidFile.path), Date() < deadline { Thread.sleep(forTimeInterval: 0.05) } + return try XCTUnwrap(Int32(String(contentsOf: pidFile).trimmingCharacters(in: .whitespacesAndNewlines))) + } + + private func assertGone(_ pid: pid_t, file: StaticString = #filePath, line: UInt = #line) { + let deadline = Date().addingTimeInterval(3) + while kill(pid, 0) == 0, Date() < deadline { Thread.sleep(forTimeInterval: 0.05) } + XCTAssertEqual(kill(pid, 0), -1, "the tool is still running", file: file, line: line) + } + + /// Quitting NX Library mid-build sends the same SIGTERM as a cancel, so the engine stops its tools itself, at once. + func testQuitLetsTheEngineStopItsTools() throws { + let (command, root, pidFile) = try sessionToolBuild(stops: true) + defer { try? FileManager.default.removeItem(at: root) } + let service = BuildService(stopGrace: 8) + let done = expectation(description: "stopped") + let tool = try start(service, command, root: root, pidFile: pidFile, done: done) + let began = Date() + service.terminateNow() + XCTAssertLessThan(Date().timeIntervalSince(began), 5) + wait(for: [done], timeout: 10) + assertGone(tool) + } + + /// A script that does not stop in time is killed with its tools, including one in a session of its own. + func testQuitKillsToolsTheScriptLeft() throws { + let (command, root, pidFile) = try sessionToolBuild(stops: false) + defer { try? FileManager.default.removeItem(at: root) } + let service = BuildService(stopGrace: 1) + let done = expectation(description: "killed") + let tool = try start(service, command, root: root, pidFile: pidFile, done: done) + service.terminateNow() + wait(for: [done], timeout: 10) + assertGone(tool) + } + + func testCancelKillsToolsTheScriptLeft() throws { + let (command, root, pidFile) = try sessionToolBuild(stops: false) + defer { try? FileManager.default.removeItem(at: root) } + let service = BuildService(stopGrace: 1) + let done = expectation(description: "cancelled") + let tool = try start(service, command, root: root, pidFile: pidFile, done: done) + service.cancel() + wait(for: [done], timeout: 10) + assertGone(tool) + } } private final class DenyInstallDirectory: FileManager, @unchecked Sendable { diff --git a/Tests/NXLibraryCoreTests/DiagnosticsReportTests.swift b/Tests/NXLibraryCoreTests/DiagnosticsReportTests.swift index 9427c6c..36d0bad 100644 --- a/Tests/NXLibraryCoreTests/DiagnosticsReportTests.swift +++ b/Tests/NXLibraryCoreTests/DiagnosticsReportTests.swift @@ -12,7 +12,7 @@ final class DiagnosticsReportTests: XCTestCase { i.engineHead = "ee369f0 fix: something" i.home = "/Users/someone" i.xcodePath = "/Applications/Xcode.app/Contents/Developer" - i.setupRows = [("Xcode", true), (".NET SDK", false)] + i.setupRows = [("Xcode", true), ("zstd", false)] i.keysImported = true i.hasHeaderKey = true i.keyGenerations = 19 @@ -78,7 +78,8 @@ final class DiagnosticsReportTests: XCTestCase { XCTAssertTrue(report.contains(heading), heading) } XCTAssertTrue(report.contains("noProfile")) - XCTAssertTrue(report.contains(".NET SDK: missing")) + XCTAssertTrue(report.contains("zstd: missing")) + XCTAssertFalse(report.contains(".NET")) } func testNoGameSectionWithoutGame() { diff --git a/Tests/NXLibraryCoreTests/DotnetReleaseTests.swift b/Tests/NXLibraryCoreTests/DotnetReleaseTests.swift deleted file mode 100644 index bb44138..0000000 --- a/Tests/NXLibraryCoreTests/DotnetReleaseTests.swift +++ /dev/null @@ -1,84 +0,0 @@ -import XCTest -import CryptoKit -@testable import NXLibraryCore - -final class DotnetReleaseTests: XCTestCase { - // Trimmed from releases.json for 10.0: two releases, the latest SDK's files in - // every shape the picker must skip. - private let json = """ - {"channel-version": "10.0", "latest-sdk": "10.0.403", "releases": [ - {"release-version": "10.0.14", - "sdk": {"version": "10.0.403", "files": [ - {"name": "dotnet-sdk-osx-arm64.tar.gz", "rid": "osx-arm64", - "url": "https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.403/dotnet-sdk-10.0.403-osx-arm64.tar.gz", "hash": "aa"}, - {"name": "dotnet-sdk-osx-x64.pkg", "rid": "osx-x64", - "url": "https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.403/dotnet-sdk-10.0.403-osx-x64.pkg", "hash": "bb"}, - {"name": "dotnet-sdk-osx-arm64.pkg", "rid": "osx-arm64", - "url": "https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.403/dotnet-sdk-10.0.403-osx-arm64.pkg", "hash": "\(String(repeating: "71", count: 64))"}]}}, - {"release-version": "10.0.13", - "sdk": {"version": "10.0.402", "files": [ - {"name": "dotnet-sdk-osx-arm64.pkg", "rid": "osx-arm64", - "url": "https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.402/dotnet-sdk-10.0.402-osx-arm64.pkg", "hash": "cc"}]}} - ]} - """ - - func testPicksLatestArm64Pkg() throws { - let package = try DotnetRelease.latestSDKPackage(from: Data(json.utf8)) - XCTAssertEqual(package.version, "10.0.403") - XCTAssertEqual(package.url.absoluteString, - "https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.403/dotnet-sdk-10.0.403-osx-arm64.pkg") - XCTAssertEqual(package.sha512, String(repeating: "71", count: 64)) - } - - func testMissingPackageThrows() { - let noArm = json.replacingOccurrences(of: "dotnet-sdk-10.0.403-osx-arm64.pkg", with: "dotnet-sdk-10.0.403-osx-arm64.zip") - XCTAssertThrowsError(try DotnetRelease.latestSDKPackage(from: Data(noArm.utf8))) - XCTAssertThrowsError(try DotnetRelease.latestSDKPackage(from: Data("{}".utf8))) - } - - func testRejectsInstallerURLsOutsideTheOfficialHTTPSHosts() { - let official = "https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.403/dotnet-sdk-10.0.403-osx-arm64.pkg" - for url in ["http://builds.dotnet.microsoft.com/a.pkg", "file:///tmp/a.pkg", - "https://example.invalid/a.pkg", "https://builds.dotnet.microsoft.com.example.invalid/a.pkg", - "https://user:password@builds.dotnet.microsoft.com/a.pkg"] { - let data = Data(json.replacingOccurrences(of: official, with: url).utf8) - XCTAssertThrowsError(try DotnetRelease.latestSDKPackage(from: data), url) - } - } - - func testRejectsMissingOrMalformedPublishedHash() { - for invalid in ["", "71CB3E", String(repeating: "z", count: 128)] { - let data = Data(json.replacingOccurrences(of: String(repeating: "71", count: 64), with: invalid).utf8) - XCTAssertThrowsError(try DotnetRelease.latestSDKPackage(from: data)) - } - } - - func testInstallerRedirectCannotLeaveOfficialHTTPSHosts() throws { - let policy = DotnetDownloadPolicy() - let original = URL(string: "https://builds.dotnet.microsoft.com/a.pkg")! - let task = URLSession.shared.dataTask(with: original) - defer { task.cancel() } - let response = try XCTUnwrap(HTTPURLResponse(url: original, statusCode: 302, httpVersion: nil, headerFields: nil)) - for target in ["https://download.visualstudio.microsoft.com/a.pkg", "https://example.invalid/a.pkg", - "http://builds.dotnet.microsoft.com/a.pkg"] { - let request = URLRequest(url: URL(string: target)!) - var followed: URLRequest? - policy.urlSession(.shared, task: task, willPerformHTTPRedirection: response, newRequest: request) { followed = $0 } - XCTAssertEqual(followed != nil, target.hasPrefix("https://download.visualstudio.microsoft.com/")) - } - } - - func testVerify() throws { - let file = FileManager.default.temporaryDirectory.appendingPathComponent("dotnet-\(UUID().uuidString).pkg") - defer { try? FileManager.default.removeItem(at: file) } - let content = Data((0..<3_000_000).map { UInt8($0 % 251) }) - try content.write(to: file) - let hex = SHA512.hash(data: content).map { String(format: "%02x", $0) }.joined() - XCTAssertTrue(try DotnetRelease.verify(file: file, sha512: hex.uppercased())) - - var changed = content - changed[1_500_000] ^= 1 - try changed.write(to: file) - XCTAssertFalse(try DotnetRelease.verify(file: file, sha512: hex)) - } -} diff --git a/Tests/NXLibraryCoreTests/EngineUpgradeSourcesTests.swift b/Tests/NXLibraryCoreTests/EngineUpgradeSourcesTests.swift index a7eb247..ee53d70 100644 --- a/Tests/NXLibraryCoreTests/EngineUpgradeSourcesTests.swift +++ b/Tests/NXLibraryCoreTests/EngineUpgradeSourcesTests.swift @@ -381,6 +381,22 @@ def package_reader(repo, keys, environment): } } + /// nxnative 641636e2 commits nx_extract as a native executable and removes configure_dotnet: the probe still reads. + func testMetadataBridgeRunsWithoutDotnetConfiguration() throws { + let (reader, root) = try readerFixture(#""" +import contextlib +@contextlib.contextmanager +def package_reader(repo, keys, environment): + class Reader: + @staticmethod + def contents(paths): return {p: [('Application', 0x0100000000010000, 0)] for p in paths} + yield Reader +"""#) + let game = root.appendingPathComponent("game.nsp") + XCTAssertEqual(try reader.contents(of: [(game, false)])[game], + .contents([PackageContent(kind: .base, titleId: 0x0100000000010000, version: 0)])) + } + /// Engines before nxnative 5c4dc18f read one package at a time and only merged or unidentified ones. func testMetadataBridgeRunsWithTheOlderReaderContract() throws { let (reader, root) = try readerFixture(#""" diff --git a/Tests/NXLibraryCoreTests/EnvironmentTests.swift b/Tests/NXLibraryCoreTests/EnvironmentTests.swift index c527091..93b7980 100644 --- a/Tests/NXLibraryCoreTests/EnvironmentTests.swift +++ b/Tests/NXLibraryCoreTests/EnvironmentTests.swift @@ -15,7 +15,6 @@ final class EnvironmentTests: XCTestCase { } private final class FakeRunner: CommandRunning { - var sdks = "10.0.401 [/usr/local/share/dotnet/sdk]\n" func run(_ executable: URL, _ arguments: [String], environment: [String: String]?, currentDirectory: URL?) throws -> CommandResult { @@ -25,9 +24,6 @@ final class EnvironmentTests: XCTestCase { if arguments == ["metal", "--version"] { return .init(status: 0, stdout: "Apple metal version 32023.917\n", stderr: "") } - if arguments == ["dotnet", "--list-sdks"] { - return .init(status: 0, stdout: sdks, stderr: "") - } return .init(status: 0, stdout: "", stderr: "") } } @@ -82,7 +78,7 @@ final class EnvironmentTests: XCTestCase { runner: runner).inspect() XCTAssertTrue(runner.calls.contains("env zstd --version")) XCTAssertTrue(runner.calls.contains("env glslangValidator --version")) - XCTAssertTrue(runner.calls.contains("env dotnet --list-sdks")) + XCTAssertFalse(runner.calls.contains { $0.contains("dotnet") }) XCTAssertFalse(runner.calls.contains { $0.contains("xcodegen") || $0.contains("spirv-cross") }) XCTAssertTrue(runner.calls.contains("env make --version")) XCTAssertEqual(status.tools.map(\.id), ["zstd", "glslangValidator", "make"]) @@ -101,15 +97,12 @@ final class EnvironmentTests: XCTestCase { XCTAssertTrue(runner.environments.allSatisfy { $0?["PATH"]?.hasPrefix("/App/Runtime/bin:") == true }) } - func testDotnetNeedsSDK10() throws { + /// nxnative 641636e2: builds need no .NET, so a Mac without it can build. + func testBuildsNeedNoDotnet() throws { let repo = try makeRepo() defer { try? FileManager.default.removeItem(at: repo) } - let runner = FakeRunner() let toolchain = EngineToolchain.resolve(runtime: nil, repo: repo) - XCTAssertFalse(NXNativeEnvironment(repoURL: repo, toolchain: toolchain, runner: runner) - .inspect().issues.contains("the .NET 10 SDK is missing")) - runner.sdks = "9.0.100 [/usr/local/share/dotnet/sdk]\n" - XCTAssertTrue(NXNativeEnvironment(repoURL: repo, toolchain: toolchain, runner: runner) - .inspect().issues.contains("the .NET 10 SDK is missing")) + let issues = NXNativeEnvironment(repoURL: repo, toolchain: toolchain, runner: FakeRunner()).inspect().issues + XCTAssertFalse(issues.contains { $0.contains(".NET") }) } } diff --git a/Tests/NXLibraryCoreTests/FirstRunSetupTests.swift b/Tests/NXLibraryCoreTests/FirstRunSetupTests.swift index 0163411..39ee1ba 100644 --- a/Tests/NXLibraryCoreTests/FirstRunSetupTests.swift +++ b/Tests/NXLibraryCoreTests/FirstRunSetupTests.swift @@ -99,8 +99,8 @@ final class FirstRunSetupTests: XCTestCase { SetupItem(id: $0, title: $0.rawValue, state: .ready, chip: "", about: "", detail: "") } let requirements = SetupCheck.requirements(all) - XCTAssertEqual(requirements.map(\.id), [.xcode, .metal, .dotnet, .engine, .runtime, .keys]) - XCTAssertEqual(SetupGrid.rowCounts(requirements.count, maxPerRow: 3), [3, 3]) + XCTAssertEqual(requirements.map(\.id), [.xcode, .metal, .engine, .runtime, .keys]) + XCTAssertEqual(SetupGrid.rowCounts(requirements.count, maxPerRow: 3), [3, 2]) } func testUnavailableGameFolderDoesNotReopenTheTechnicalSetup() { diff --git a/Tests/NXLibraryCoreTests/GameDetailPresentationTests.swift b/Tests/NXLibraryCoreTests/GameDetailPresentationTests.swift index de3bdb6..37131af 100644 --- a/Tests/NXLibraryCoreTests/GameDetailPresentationTests.swift +++ b/Tests/NXLibraryCoreTests/GameDetailPresentationTests.swift @@ -2,6 +2,19 @@ import XCTest @testable import NXLibraryCore final class GameDetailPresentationTests: XCTestCase { + func testKnownGenresTranslateInBothPresentationsWithoutChangingMetadata() { + let old = L10n.context + defer { L10n.setContext(old) } + L10n.setContext(AppLocale(language: .italian, region: "IT")) + let metadata = meta(genres: ["Adventure", "Action", "Role-Playing", "My custom genre"]) + let detail = GameDetailPresentation(game: game("Game"), metadata: metadata, totalPlaytime: 0, lastPlayed: nil) + let library = LibraryPresentation(game: game("Game"), metadata: metadata, totalPlaytime: 0, lastPlayed: nil) + XCTAssertEqual(detail.genres, ["Avventura", "Azione", "Giochi di ruolo", "My custom genre"]) + XCTAssertEqual(library.genres, ["Avventura", "Azione", "Giochi di ruolo"]) + XCTAssertEqual(metadata.genres.first, "Adventure") + XCTAssertEqual(detail.descriptionLanguageNote, metadata.description.isEmpty ? nil : "Descrizione in inglese.") + } + func testRebuildIsUnavailableWhenRecordedSourceFilesAreMissing() { var missing = PortedGame(name: "Synthetic", url: URL(fileURLWithPath: "/Applications/Synthetic.app"), platform: .macApp, sizeBytes: 0, modificationDate: .distantPast) diff --git a/Tests/NXLibraryCoreTests/NewestEngineTests.swift b/Tests/NXLibraryCoreTests/NewestEngineTests.swift index a22d334..c5387c9 100644 --- a/Tests/NXLibraryCoreTests/NewestEngineTests.swift +++ b/Tests/NXLibraryCoreTests/NewestEngineTests.swift @@ -255,7 +255,6 @@ final class NewestEngineTests: XCTestCase { private func facts(head: String, choice: NewestEngineChoice?) -> SetupFacts { var facts = SetupFacts(xcodeStatus: 0, metalVersion: "32023.922", certifiedMetal: "32023.922", allowUntested: true, - dotnetSDKs: ["10.0.401 [/usr/local/share/dotnet/sdk]"], engine: .checkout(head: head, dirty: false), engineFolder: repo, pinnedCommit: pin, runtimeProblem: nil, keys: nil, keysError: nil, libraryReadable: true, engineBehind: false) diff --git a/Tests/NXLibraryCoreTests/NintendoStoreMetadataTests.swift b/Tests/NXLibraryCoreTests/NintendoStoreMetadataTests.swift index 9f88cad..5a8d461 100644 --- a/Tests/NXLibraryCoreTests/NintendoStoreMetadataTests.swift +++ b/Tests/NXLibraryCoreTests/NintendoStoreMetadataTests.swift @@ -30,6 +30,138 @@ final class NintendoStoreMetadataTests: XCTestCase { return url } + func testWarmLanguageSwitchUsesOneLocalizedRequestInsteadOfEnglishSearches() async throws { + let client = FakeClient() + client.responses["ncom_game_en_us/query"] = Data(#"{"hits":[{"title":"Game","nsuid":"1","platform":"Nintendo Switch","description":"English description.","url":"/games/detail/game-switch/"}]}"#.utf8) + let service = NintendoStoreMetadataService(client: client, cacheDirectory: cacheDirectory()) + let game = game(name: "Game", titleId: "0100000000010000") + _ = try await service.metadata(for: game) + client.requested = [] + client.responses["lang=it"] = Data(#"{"description":"Descrizione italiana."}"#.utf8) + let result = try await service.metadata(for: game, locale: AppLocale(language: .italian, region: "IT")) + XCTAssertEqual(result.description, "Descrizione italiana.") + XCTAssertEqual(client.requested.count, 1) + XCTAssertTrue(client.requested.first?.contains("lang=it") == true) + client.requested = [] + let otherRegion = try await service.metadata(for: game, locale: AppLocale(language: .italian, region: "CH")) + XCTAssertEqual(otherRegion.description, result.description) + XCTAssertTrue(client.requested.isEmpty, "Formatting region must not trigger another metadata request") + } + + func testSwitch2RequestsTheSelectedDescriptionLanguage() async throws { + let client = FakeClient() + client.responses["ncom_game_en_us/query"] = Data(#"{"hits":[{"title":"Game","nsuid":"1","platform":"Nintendo Switch 2","description":"English description.","url":"/games/detail/game-switch-2/"}]}"#.utf8) + client.responses["lang=it"] = Data(#"{"description":"Descrizione italiana per Switch 2."}"#.utf8) + let service = NintendoStoreMetadataService(client: client, cacheDirectory: cacheDirectory()) + let result = try await service.metadata(for: game(name: "Game", titleId: "0400000000010000"), locale: AppLocale(language: .italian, region: "IT")) + XCTAssertEqual(result.descriptionLanguage, "it") + XCTAssertEqual(result.description, "Descrizione italiana per Switch 2.") + } + + func testOldEnglishFallbackIsRetriedButAnUnavailableTranslationDoesNotLoop() async throws { + let client = FakeClient() + let cache = cacheDirectory() + let game = game(name: "Game", titleId: "0400000000010000") + var metadata = NintendoStoreMetadata(titleId: game.titleId!, name: "Game", description: "English description.", developers: [], publishers: [], genres: ["Action"], releaseDate: nil, storeURL: URL(string: "https://example.com")!, imageURL: nil, headerImageURL: nil, nsuid: nil, fetchedAt: Date()) + metadata.localeKey = "it_IT" + let file = cache.appendingPathComponent("localized/it_IT/0400000000010000.json") + try FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true) + try JSONEncoder().encode(metadata).write(to: file) + client.responses["lang=it"] = Data(#"{"description":""}"#.utf8) + let service = NintendoStoreMetadataService(client: client, cacheDirectory: cache) + let locale = AppLocale(language: .italian, region: "IT") + let result = try await service.metadata(for: game, locale: locale) + XCTAssertEqual(result.descriptionLanguage, "en") + XCTAssertEqual(result.description, metadata.description) + XCTAssertEqual(client.requested.count, 1) + _ = try await service.metadata(for: game, locale: locale) + XCTAssertEqual(client.requested.count, 1) + } + + func testLocalizedDescriptionWinsOverEnglishStoreAndCachesByLocale() async throws { + let client = FakeClient() + client.responses["ncom_game_en_us/query"] = Data(#"{"hits":[{"title":"Game","nsuid":"1","platform":"Nintendo Switch","description":"An English description.","url":"/games/detail/game-switch/"}]}"#.utf8) + client.responses["lang=it"] = Data(#"{"id":"0100000000010000","description":"Una descrizione italiana."}"#.utf8) + client.responses["lang=de"] = Data(#"{"id":"0100000000010000","description":"Eine deutsche Beschreibung."}"#.utf8) + let cache = cacheDirectory() + let service = NintendoStoreMetadataService(client: client, cacheDirectory: cache) + let game = game(name: "Game", titleId: "0100000000010000") + let it = AppLocale(language: .italian, region: "CH") + let de = AppLocale(language: .german, region: "CH") + let italian = try await service.metadata(for: game, locale: it) + XCTAssertEqual(italian.description, "Una descrizione italiana.") + XCTAssertEqual(italian.descriptionLanguage, "it") + XCTAssertEqual(italian.name, "Game") + let german = try await service.metadata(for: game, locale: de) + XCTAssertEqual(german.description, "Eine deutsche Beschreibung.") + XCTAssertEqual(service.cached(for: game.titleId!, locale: it)?.description, "Una descrizione italiana.") + let offline = NintendoStoreMetadataService(client: FakeClient(), cacheDirectory: cache) + let restored = try await offline.metadata(for: game, locale: it) + XCTAssertEqual(restored.description, italian.description) + XCTAssertFalse(client.requested.contains { $0.contains("region=") }) + } + + func testMissingTranslationKeepsEnglishAndPortugueseUsesSupportedLanguageCode() async throws { + let client = FakeClient() + client.responses["ncom_game_en_us/query"] = Data(#"{"hits":[{"title":"Game","nsuid":"1","platform":"Nintendo Switch","description":"English is still useful.","url":"/games/detail/game-switch/"}]}"#.utf8) + client.responses["lang=pt"] = Data(#"{"description":""}"#.utf8) + let service = NintendoStoreMetadataService(client: client, cacheDirectory: cacheDirectory()) + let metadata = try await service.metadata(for: game(name: "Game", titleId: "0100000000010000"), locale: AppLocale(language: .portugueseBrazil, region: "BR")) + XCTAssertEqual(metadata.description, "English is still useful.") + XCTAssertEqual(metadata.descriptionLanguage, "en") + XCTAssertTrue(client.requested.contains { $0.contains("lang=pt") }) + XCTAssertFalse(client.requested.contains { $0.contains("lang=pt-BR") }) + } + + func testOfflineEnglishCacheCanServeAnotherRegionWithoutClaimingTranslation() async throws { + let client = FakeClient() + client.responses["ncom_game_en_us/query"] = Data(#"{"hits":[{"title":"Game","nsuid":"1","platform":"Nintendo Switch","description":"English cached text.","url":"/games/detail/game-switch/"}]}"#.utf8) + let cache = cacheDirectory() + let game = game(name: "Game", titleId: "0100000000010000") + _ = try await NintendoStoreMetadataService(client: client, cacheDirectory: cache).metadata(for: game) + let offline = NintendoStoreMetadataService(client: FakeClient(), cacheDirectory: cache) + for language in [AppLanguage.english, .italian] { + let result = try await offline.metadata(for: game, locale: AppLocale(language: language, region: "CH")) + XCTAssertEqual(result.description, "English cached text.") + XCTAssertEqual(result.descriptionLanguage, "en") + } + } + + func testExpiredTranslationSurvivesPartialProviderFailure() async throws { + let client = FakeClient() + client.responses["ncom_game_en_us/query"] = Data(#"{"hits":[{"title":"Game","nsuid":"1","platform":"Nintendo Switch","description":"English description.","url":"/games/detail/game-switch/"}]}"#.utf8) + client.responses["lang=it"] = Data(#"{"name":"Game","description":"Descrizione italiana conservata."}"#.utf8) + let cache = cacheDirectory() + let service = NintendoStoreMetadataService(client: client, cacheDirectory: cache) + let game = game(name: "Game", titleId: "0100000000010000") + let locale = AppLocale(language: .italian, region: "CH") + var first = try await service.metadata(for: game, locale: locale) + first.fetchedAt = Date().addingTimeInterval(-9 * 24 * 3600) + let file = cache.appendingPathComponent("localized/it_CH/0100000000010000.json") + try JSONEncoder().encode(first).write(to: file) + client.responses.removeValue(forKey: "lang=it") + let refreshed = try await service.metadata(for: game, locale: locale) + XCTAssertEqual(refreshed.description, "Descrizione italiana conservata.") + XCTAssertEqual(refreshed.descriptionLanguage, "it") + XCTAssertEqual(service.cached(for: game.titleId!, locale: locale)?.description, first.description) + } + + func testEmptyLocalizedResponseDoesNotHideEnglishCache() async throws { + let english = FakeClient() + english.responses["ncom_game_en_us/query"] = Data(#"{"hits":[{"title":"Game","nsuid":"1","platform":"Nintendo Switch","description":"Cached English description.","url":"/games/detail/game-switch/"}]}"#.utf8) + let cache = cacheDirectory() + let game = game(name: "Game", titleId: "0100000000010000") + _ = try await NintendoStoreMetadataService(client: english, cacheDirectory: cache).metadata(for: game) + let partial = FakeClient() + partial.responses["lang=it"] = Data(#"{"name":"Game","description":""}"#.utf8) + let service = NintendoStoreMetadataService(client: partial, cacheDirectory: cache) + let locale = AppLocale(language: .italian, region: "CH") + let result = try await service.metadata(for: game, locale: locale) + XCTAssertEqual(result.description, "Cached English description.") + XCTAssertEqual(result.descriptionLanguage, "en") + XCTAssertFalse(service.cached(for: game.titleId!, locale: locale)?.description.isEmpty ?? false) + } + func testUsesTheLegacyCatalogWithoutFetchingTheProductPage() async throws { let client = FakeClient() client.responses["ncom_game_en_us/query"] = Data(""" diff --git a/Tests/NXLibraryCoreTests/PortingFailureReasonTests.swift b/Tests/NXLibraryCoreTests/PortingFailureReasonTests.swift index af5e9c3..7ba7965 100644 --- a/Tests/NXLibraryCoreTests/PortingFailureReasonTests.swift +++ b/Tests/NXLibraryCoreTests/PortingFailureReasonTests.swift @@ -5,7 +5,7 @@ final class PortingFailureReasonTests: XCTestCase { /// One real line per table row, as the pinned engine prints it ("[RED] " + its BuildError text). static let engineLines: [(id: String, kind: PortingFailureReason.Kind, line: String)] = [ ("rosetta.intel", .rosetta, "[RED] this Python (/usr/local/bin/python3) is an Intel one running under Rosetta, and the tools it starts would run as Intel code too: run the script with an arm64 Python"), - ("translator.run", .translator, "[RED] the shader translator does not run (exit 1): it needs the .NET 10 runtime for this Mac's architecture"), + ("translator.run", .translator, "[RED] the shader translator does not run (exit code 1): /x/dmsh_xlate is the committed native executable; check out the repository again if it is missing or damaged"), ("translator.start", .translator, "[RED] the shader translator does not start ([Errno 2] No such file): /x/translator"), ("zbic.missing", .zbic, "[RED] main: ZBIC (zstd) segments need the zbic package (python3 -m pip install zbic)"), ("clt.selected", .cltSelected, "[RED] the selected developer folder is the Command Line Tools (/Library/Developer/CommandLineTools), which have no iOS SDK"), diff --git a/Tests/NXLibraryCoreTests/SaveTransactionTests.swift b/Tests/NXLibraryCoreTests/SaveTransactionTests.swift index 66b5d14..9a7abb6 100644 --- a/Tests/NXLibraryCoreTests/SaveTransactionTests.swift +++ b/Tests/NXLibraryCoreTests/SaveTransactionTests.swift @@ -24,6 +24,27 @@ final class SaveTransactionTests: XCTestCase { XCTAssertEqual(try String(contentsOf: location.directory(.account).appendingPathComponent("obsolete")), "old") XCTAssertNoThrow(try tx.recover(location: location)) } + /// nxnative e6048dea: a cut-short cloud pull leaves `save.pull`; the game's next start puts this Mac's save back, + /// so an import or restore before then is refused and changes nothing. + func testCloudPullJournalRefusesImportAndRestore() throws { + let h = try SaveTestHome(); defer { h.remove() } + let location = try h.location(); let store = SaveSnapshotStore(root: h.backups) + try h.write("mine", location.directory(.account).appendingPathComponent("data")) + let backup = try store.create(location: location, identity: h.identity, reason: .manual) + let source = h.base.appendingPathComponent("export") + try h.write("new", source.appendingPathComponent("data")) + let preview = try store.stageImport(source: source, slot: .account, location: location) + try h.write("journal", location.cloudPullJournal) + let tx = SaveTransaction(store: store, isRunning: { false }) + XCTAssertThrowsError(try tx.importSave(preview, location: location, identity: h.identity)) { + XCTAssertEqual($0 as? SaveFailure, .cloudPullPending) + } + XCTAssertThrowsError(try tx.restore(backup, location: location, identity: h.identity)) { + XCTAssertEqual($0 as? SaveFailure, .cloudPullPending) + } + XCTAssertEqual(try String(contentsOf: location.directory(.account).appendingPathComponent("data")), "mine") + XCTAssertEqual(try String(contentsOf: location.cloudPullJournal), "journal") + } func testRestoreRemovesSlotAbsentInSnapshot() throws { let h = try SaveTestHome(); defer { h.remove() } let location = try h.location(); let store = SaveSnapshotStore(root: h.backups) diff --git a/Tests/NXLibraryCoreTests/SetupCheckTests.swift b/Tests/NXLibraryCoreTests/SetupCheckTests.swift index 707c9ea..ba123b4 100644 --- a/Tests/NXLibraryCoreTests/SetupCheckTests.swift +++ b/Tests/NXLibraryCoreTests/SetupCheckTests.swift @@ -17,7 +17,6 @@ final class SetupCheckTests: XCTestCase { private func facts() -> SetupFacts { SetupFacts(xcodeStatus: 0, metalVersion: "32023.921", certifiedMetal: "32023.921", allowUntested: false, - dotnetSDKs: ["10.0.401 [/usr/local/share/dotnet/sdk]"], engine: .checkout(head: pin, dirty: false), engineFolder: folder, pinnedCommit: pin, runtimeProblem: nil, keys: KeySummary(hasHeaderKey: true, applicationKeyGenerations: [0, 1, 2]), keysError: nil, @@ -76,20 +75,10 @@ final class SetupCheckTests: XCTestCase { XCTAssertEqual(item(.metal).state, .ready) } - func testDotnet() { - XCTAssertEqual(item(.dotnet).state, .ready) - for sdks in [["9.0.1 [/usr/local/share/dotnet/sdk]"], []] { - let card = item(.dotnet) { $0.dotnetSDKs = sdks } - XCTAssertEqual(card.state, .missing) - XCTAssertEqual(card.chip, "NOT INSTALLED") - XCTAssertEqual(card.actionTitle, "Install") - } - XCTAssertNil(item(.dotnet).actionTitle) - XCTAssertTrue(item(.dotnet).detail.contains("10.0.401")) - let update = item(.dotnet) { $0.dotnetLatest = "10.0.404" } - XCTAssertEqual(update.state, .ready) - XCTAssertEqual(update.actionTitle, "Update to 10.0.404") - XCTAssertNil(item(.dotnet) { $0.dotnetLatest = "10.0.401" }.actionTitle) + /// nxnative 641636e2 commits its tools as native executables: Setup has no .NET card. + func testNoDotnetCard() { + XCTAssertNil(SetupItemID(rawValue: "dotnet")) + XCTAssertFalse(SetupCheck.items(from: facts()).contains { $0.title.contains(".NET") }) } func testEngine() { @@ -205,7 +194,7 @@ final class SetupCheckTests: XCTestCase { XCTAssertEqual(files.state, readable == nil ? .ready : .warning) XCTAssertTrue(cards.filter(\.isRequired).allSatisfy { $0.state == .ready }) XCTAssertFalse(SetupCheck.shouldReopen(previous: [files.id: .ready], current: cards)) - XCTAssertEqual(SetupCheck.automatic(cards, includeUpdates: true), []) + XCTAssertEqual(SetupCheck.automatic(cards), []) } } @@ -220,9 +209,9 @@ final class SetupCheckTests: XCTestCase { XCTAssertEqual(items.first { $0.id == .xcode }!.state, .ready) XCTAssertNil(engine.progress) - f.dotnetSDKs = [] - let downloading = SetupCheck.applying([.dotnet: .working("DOWNLOADING…", progress: 0.25, detail: "50 of 200 MB")], - to: SetupCheck.items(from: f)).first { $0.id == .dotnet }! + f.metalVersion = nil + let downloading = SetupCheck.applying([.metal: .working("DOWNLOADING…", progress: 0.25, detail: "50 of 200 MB")], + to: SetupCheck.items(from: f)).first { $0.id == .metal }! XCTAssertEqual(downloading.chip, "DOWNLOADING… 25%") XCTAssertEqual(downloading.progress, 0.25) XCTAssertEqual(downloading.detail, "50 of 200 MB") @@ -230,29 +219,24 @@ final class SetupCheckTests: XCTestCase { func testAutomaticActions() { var f = facts() - XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f), includeUpdates: true), []) - - f.dotnetLatest = "10.0.404" - XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f), includeUpdates: true), [.dotnet]) - XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f), includeUpdates: false), []) + XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f)), []) f.metalVersion = "32023.917" f.engine = .missing f.keys = nil - XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f), includeUpdates: false), [.metal, .engine]) + XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f)), [.metal, .engine]) // Metal comes through Xcode: nothing to try until Xcode is ready. f.xcodeVersion = "26.4" - XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f), includeUpdates: false), [.engine]) + XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f)), [.engine]) f.xcodeVersion = nil f.xcodeStatus = nil - XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f), includeUpdates: false), [.engine]) + XCTAssertEqual(SetupCheck.automatic(SetupCheck.items(from: f)), [.engine]) f.xcodeStatus = 0 f.certifiedMetal = "32024.5" f.engine = .checkout(head: "ee369f0", dirty: true) - f.dotnetSDKs = [] - let items = SetupCheck.applying([.dotnet: .working("DOWNLOADING…")], to: SetupCheck.items(from: f)) - XCTAssertEqual(SetupCheck.automatic(items, includeUpdates: true), []) + let items = SetupCheck.applying([.metal: .working("DOWNLOADING…")], to: SetupCheck.items(from: f)) + XCTAssertEqual(SetupCheck.automatic(items), []) } func testShouldReopenOnlyOnRegression() { @@ -395,7 +379,6 @@ final class SetupCheckTests: XCTestCase { let runner = FakeRunner() runner.responses["xcodebuild -version"] = CommandResult(status: 69, stdout: "", stderr: "license") runner.responses["metal --version"] = CommandResult(status: 0, stdout: "Apple metal version 32023.921 (metalfe-32023.921.6)\n", stderr: "") - runner.responses["dotnet --list-sdks"] = CommandResult(status: 0, stdout: "10.0.401 [/usr/local/share/dotnet/sdk]\n", stderr: "") runner.responses["rev-parse HEAD"] = CommandResult(status: 0, stdout: pin + "\n", stderr: "") runner.responses["status --porcelain"] = CommandResult(status: 0, stdout: " M dm/gfx/nvn/dmsh_chain.h\n?? x/_build/a\n", stderr: "") @@ -410,7 +393,6 @@ final class SetupCheckTests: XCTestCase { XCTAssertEqual(facts.xcodeStatus, 69) XCTAssertNil(facts.xcodeVersion) XCTAssertEqual(facts.metalVersion, "32023.921") - XCTAssertEqual(facts.dotnetSDKs, ["10.0.401 [/usr/local/share/dotnet/sdk]"]) XCTAssertNil(facts.keys) XCTAssertNil(facts.libraryReadable) diff --git a/Tests/NXLibraryCoreTests/StorageManagerTests.swift b/Tests/NXLibraryCoreTests/StorageManagerTests.swift index 8fde18e..145ab19 100644 --- a/Tests/NXLibraryCoreTests/StorageManagerTests.swift +++ b/Tests/NXLibraryCoreTests/StorageManagerTests.swift @@ -2,11 +2,185 @@ import XCTest @testable import NXLibraryCore final class StorageManagerTests: XCTestCase { + private let title = "0100000000010000" + + private func fixture() throws -> (URL, PortedGame, StorageManager) { + let root = FileManager.default.temporaryDirectory.appendingPathComponent("NXStorage-\(UUID())") + addTeardownBlock { try? FileManager.default.removeItem(at: root) } + let workspace = root.appendingPathComponent("workspaces/\(title)") + let cache = workspace.appendingPathComponent("\(title)_build") + try FileManager.default.createDirectory(at: cache, withIntermediateDirectories: true) + var game = PortedGame(name: "Game", url: root.appendingPathComponent("Game.app"), + platform: .macApp, sizeBytes: 0, modificationDate: Date(), titleId: title) + game.workspacePath = workspace.path + game.buildCachePath = cache.path + let manager = StorageManager(supportRoot: root, applicationsRoot: root, logsRoot: root.appendingPathComponent("launch")) + return (root, game, manager) + } + + func testCacheDeletionRejectsUnrelatedFolderEvenWhenItsNameEndsInBuild() throws { + let (root, original, manager) = try fixture() + let unrelated = root.appendingPathComponent("personal_build") + try FileManager.default.createDirectory(at: unrelated, withIntermediateDirectories: true) + let file = unrelated.appendingPathComponent("keep") + try Data([1]).write(to: file) + var game = original + game.buildCachePath = unrelated.path + XCTAssertThrowsError(try manager.deleteCache(for: game)) + XCTAssertTrue(FileManager.default.fileExists(atPath: file.path)) + } + + func testCacheDeletionRejectsSymbolicLinkToAnotherFolder() throws { + let (root, game, manager) = try fixture() + let cache = URL(fileURLWithPath: try XCTUnwrap(game.buildCachePath)) + try FileManager.default.removeItem(at: cache) + let external = root.appendingPathComponent("external") + try FileManager.default.createDirectory(at: external, withIntermediateDirectories: true) + try FileManager.default.createSymbolicLink(at: cache, withDestinationURL: external) + XCTAssertThrowsError(try manager.deleteCache(for: game)) + XCTAssertNotNil(try? FileManager.default.destinationOfSymbolicLink(atPath: cache.path)) + } + + func testCacheDeletionRefusesToRemoveTheInstalledApp() throws { + let (_, original, manager) = try fixture() + var game = original + game.url = URL(fileURLWithPath: try XCTUnwrap(game.buildCachePath)).appendingPathComponent("Game.app") + try FileManager.default.createDirectory(at: game.url, withIntermediateDirectories: true) + XCTAssertThrowsError(try manager.deleteCache(for: game)) + XCTAssertTrue(FileManager.default.fileExists(atPath: game.url.path)) + } + + func testUsageCountsSharedFilesOnceAndIncludesHiddenCacheFiles() throws { + let (_, game, manager) = try fixture() + let cache = URL(fileURLWithPath: try XCTUnwrap(game.buildCachePath)) + try Data(count: 20).write(to: cache.appendingPathComponent("visible")) + try Data(count: 30).write(to: cache.appendingPathComponent(".hidden")) + var duplicate = game + duplicate.id = UUID() + let usage = manager.usage(games: [game, duplicate]) + XCTAssertEqual(usage.cacheBytes, 50) + } + + func testCacheDeletionPreservesSourcesAndInternalSymbolicLinkTargets() throws { + let (root, original, manager) = try fixture() + var game = original + let cache = URL(fileURLWithPath: try XCTUnwrap(game.buildCachePath)) + let source = cache.appendingPathComponent("original.nsp") + try Data(count: 7).write(to: source) + game.sources = [GameFileItem(url: source, kind: .base, sizeBytes: 7)] + XCTAssertThrowsError(try manager.deleteCache(for: game)) + XCTAssertTrue(FileManager.default.fileExists(atPath: source.path)) + game.sources = [] + let external = root.appendingPathComponent("external.nsp") + try Data(count: 9).write(to: external) + try FileManager.default.createSymbolicLink(at: cache.appendingPathComponent("link.nsp"), withDestinationURL: external) + try manager.deleteCache(for: game) + XCTAssertEqual(try Data(contentsOf: external).count, 9) + } + + func testMissingVolumeIsNotReportedAsSuccessfulCleanup() throws { + let (_, original, manager) = try fixture() + var game = original + game.workspacePath = "/Volumes/NXStorageMissing-\(UUID())/workspace" + game.buildCachePath = game.workspacePath! + "/\(title)_build" + XCTAssertThrowsError(try manager.deleteCache(for: game)) { error in + guard case StorageCleanupError.unavailable = error else { return XCTFail("Expected a disconnected drive, got \(error)") } + } + } + + func testRecordedCacheMustBeADirectory() throws { + let (_, game, manager) = try fixture() + let cache = URL(fileURLWithPath: try XCTUnwrap(game.buildCachePath)) + try FileManager.default.removeItem(at: cache) + try Data(count: 3).write(to: cache) + XCTAssertThrowsError(try manager.deleteCache(for: game)) + XCTAssertEqual(try Data(contentsOf: cache).count, 3) + XCTAssertFalse(manager.snapshot(games: [game]).games[0].canDeleteCache) + } + + func testPerGameBreakdownAndSelectiveDeletionKeepOtherGamesAndLogs() throws { + let (root, first, manager) = try fixture() + let firstCache = URL(fileURLWithPath: try XCTUnwrap(first.buildCachePath)) + try Data(count: 30).write(to: firstCache.appendingPathComponent("cache")) + try FileManager.default.createDirectory(at: first.url, withIntermediateDirectories: true) + try Data(count: 40).write(to: first.url.appendingPathComponent("game")) + let log = root.appendingPathComponent("Logs/\(title).log") + try FileManager.default.createDirectory(at: log.deletingLastPathComponent(), withIntermediateDirectories: true) + try Data(count: 7).write(to: log) + var second = first + second.id = UUID() + second.name = "Second" + second.titleId = "0100000000020000" + let secondCache = firstCache.deletingLastPathComponent().appendingPathComponent("0100000000020000_build") + second.buildCachePath = secondCache.path + try FileManager.default.createDirectory(at: secondCache, withIntermediateDirectories: true) + try Data(count: 60).write(to: secondCache.appendingPathComponent("cache")) + let snapshot = manager.snapshot(games: [first, second]) + XCTAssertEqual(snapshot.games.map(\.id), [second.id, first.id]) + XCTAssertEqual(snapshot.games[1].app, .bytes(40)) + XCTAssertEqual(snapshot.games[1].cache, .bytes(30)) + XCTAssertEqual(snapshot.games[1].logs, .bytes(7)) + XCTAssertEqual(snapshot.total.libraryBytes, 40) + XCTAssertEqual(snapshot.total.cacheBytes, 90) + XCTAssertTrue(try manager.deleteCache(for: first, protecting: [first, second])) + XCTAssertTrue(FileManager.default.fileExists(atPath: secondCache.path)) + XCTAssertEqual(try Data(contentsOf: log).count, 7) + let after = manager.snapshot(games: [first, second]) + XCTAssertEqual(after.total.cacheBytes, 60) + XCTAssertEqual(after.games.first { $0.id == first.id }?.cache, .missing) + XCTAssertFalse(after.games.first { $0.id == first.id }!.canDeleteCache) + } + + func testSharedCacheIsExplainedAndAnotherGamesSourcesAreProtected() throws { + let (_, first, manager) = try fixture() + let cache = URL(fileURLWithPath: try XCTUnwrap(first.buildCachePath)) + try Data(count: 10).write(to: cache.appendingPathComponent("data")) + var second = first + second.id = UUID() + second.name = "Other build" + let snapshot = manager.snapshot(games: [first, second]) + XCTAssertEqual(snapshot.total.cacheBytes, 10) + XCTAssertEqual(snapshot.games.first { $0.id == first.id }?.sharedCacheWith, ["Other build"]) + second.sources = [GameFileItem(url: cache.appendingPathComponent("data"), kind: .base, sizeBytes: 10)] + XCTAssertThrowsError(try manager.deleteCache(for: first, protecting: [second])) + XCTAssertFalse(manager.snapshot(games: [first, second]).games.first { $0.id == first.id }!.canDeleteCache) + XCTAssertTrue(FileManager.default.fileExists(atPath: cache.appendingPathComponent("data").path)) + } + + func testMissingAndDisconnectedFoldersHaveDifferentStates() throws { + let (_, original, manager) = try fixture() + var game = original + game.url = URL(fileURLWithPath: "/Volumes/NXStorageMissing-\(UUID())/Game.app") + game.workspacePath = nil + game.buildCachePath = nil + let snapshot = manager.snapshot(games: [game], applicationsFolder: game.url.deletingLastPathComponent()) + XCTAssertEqual(snapshot.games[0].app, .unavailable) + XCTAssertEqual(snapshot.games[0].cache, .missing) + XCTAssertTrue(snapshot.hasUnavailableFiles) + XCTAssertNil(snapshot.freeBytes) + let local = manager.snapshot(games: [original]) + XCTAssertEqual(local.games[0].app, .missing) + XCTAssertEqual(local.games[0].cache, .bytes(0)) + XCTAssertFalse(local.hasUnavailableFiles) + } + + func testUnreadableCacheIsNotPresentedAsEmptyOrDeletable() throws { + let (_, game, manager) = try fixture() + let cache = URL(fileURLWithPath: try XCTUnwrap(game.buildCachePath)) + try Data(count: 10).write(to: cache.appendingPathComponent("data")) + try FileManager.default.setAttributes([.posixPermissions: 0], ofItemAtPath: cache.path) + defer { try? FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: cache.path) } + let snapshot = manager.snapshot(games: [game]) + XCTAssertEqual(snapshot.games[0].cache, .unavailable) + XCTAssertFalse(snapshot.games[0].canDeleteCache) + XCTAssertThrowsError(try manager.deleteCache(for: game)) + } + func testUsageAndCacheDeletion() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("NXStorage-\(UUID().uuidString)") let app = root.appendingPathComponent("Applications/Game.app") - let cache = root.appendingPathComponent("Workspaces/0100/0100_build") + let cache = root.appendingPathComponent("Workspaces/0100/0100000000010000_build") let logs = root.appendingPathComponent("Logs") let supportLogs = root.appendingPathComponent("Support/Logs") try FileManager.default.createDirectory(at: app, withIntermediateDirectories: true) @@ -21,6 +195,7 @@ final class StorageManagerTests: XCTestCase { sizeBytes: 0, modificationDate: Date(), titleId: "0100000000010000") game.buildCachePath = cache.path + game.workspacePath = cache.deletingLastPathComponent().path let manager = StorageManager( supportRoot: root.appendingPathComponent("Support"), applicationsRoot: root.appendingPathComponent("Applications"), diff --git a/Tests/NXLibraryCoreTests/ToolchainRequirementsTests.swift b/Tests/NXLibraryCoreTests/ToolchainRequirementsTests.swift index f2c22e1..246e512 100644 --- a/Tests/NXLibraryCoreTests/ToolchainRequirementsTests.swift +++ b/Tests/NXLibraryCoreTests/ToolchainRequirementsTests.swift @@ -18,7 +18,7 @@ final class ToolchainRequirementsTests: XCTestCase { } func testMetalDownloadPinsTheCertifiedAsset() { - let requirements = ToolchainRequirements(minimumXcode: "27.1", dotnetMajor: 10, fallbackMetal: "32023.921", + let requirements = ToolchainRequirements(minimumXcode: "27.1", fallbackMetal: "32023.921", metalAssets: ["32023.921": "27A266a"]) XCTAssertEqual(requirements.metalDownload(for: "32023.921"), ["-downloadComponent", "MetalToolchain", "-buildVersion", "27A266a"]) @@ -29,18 +29,10 @@ final class ToolchainRequirementsTests: XCTestCase { func testCurrentCoversItsOwnFallback() { let current = ToolchainRequirements.current XCTAssertNotNil(current.metalAssets[current.fallbackMetal]) - XCTAssertEqual(current.dotnetMajor, 10) } func testXcodeVersionParse() { XCTAssertEqual(ToolchainRequirements.xcodeVersion(from: "Xcode 27.1\nBuild version 27A9275\n"), "27.1") XCTAssertNil(ToolchainRequirements.xcodeVersion(from: "xcode-select: error")) } - - func testNewestDotnetSDKForMajor() { - let lines = ["8.0.414 [/usr/local/share/dotnet/sdk]", "10.0.401 [/usr/local/share/dotnet/sdk]", - "10.0.100 [/usr/local/share/dotnet/sdk]"] - XCTAssertEqual(ToolchainRequirements.newestSDK(major: 10, in: lines), "10.0.401") - XCTAssertNil(ToolchainRequirements.newestSDK(major: 10, in: ["8.0.414 [/x]"])) - } } diff --git a/Tests/PublicationChecks/test_localizations.py b/Tests/PublicationChecks/test_localizations.py new file mode 100644 index 0000000..68db335 --- /dev/null +++ b/Tests/PublicationChecks/test_localizations.py @@ -0,0 +1,53 @@ +import importlib.util +import json +import plistlib +from pathlib import Path +import tempfile +import unittest + +SCRIPT = Path(__file__).resolve().parents[2] / 'scripts/check_localizations.py' + + +class LocalizationChecks(unittest.TestCase): + def module(self): + spec = importlib.util.spec_from_file_location('check_localizations', SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + def test_missing_translation_and_changed_placeholder_are_rejected(self): + module = self.module() + with tempfile.TemporaryDirectory() as root: + path = Path(root) / 'Localizable.xcstrings' + translations = {language: {'stringUnit': {'state': 'translated', 'value': 'Game %@'}} + for language in module.LANGUAGES} + path.write_text(json.dumps({'sourceLanguage': 'en', 'strings': {'Game %@': {'localizations': translations}}, 'version': '1.0'})) + self.assertEqual(module.check_catalog(path), []) + del translations['de'] + translations['it']['stringUnit']['value'] = 'Gioco %lld' + path.write_text(json.dumps({'sourceLanguage': 'en', 'strings': {'Game %@': {'localizations': translations}}, 'version': '1.0'})) + errors = module.check_catalog(path) + self.assertTrue(any('de' in error for error in errors)) + self.assertTrue(any('placeholder' in error.lower() for error in errors)) + + def test_empty_or_incomplete_bundle_is_rejected(self): + module = self.module() + with tempfile.TemporaryDirectory() as root: + errors = module.check_app(Path(root) / 'Missing.app') + self.assertTrue(errors) + self.assertTrue(any('bundle' in error.lower() or 'plist' in error.lower() for error in errors)) + + def test_utf16_compiled_xml_with_utf8_declaration_is_accepted(self): + module = self.module() + with tempfile.TemporaryDirectory() as root: + app = Path(root) / 'Game.app' + info = app / 'Contents/Info.plist' + info.parent.mkdir(parents=True) + info.write_bytes(plistlib.dumps({'CFBundleLocalizations': list(module.LANGUAGES)})) + for language in module.LANGUAGES: + folder = app / 'Contents/Resources' / module.BUNDLE / 'Contents/Resources' / (language + '.lproj') + folder.mkdir(parents=True) + for name in ('Localizable.strings', 'Localizable.stringsdict'): + xml = plistlib.dumps({'test': 'translated'}).decode('utf-8').encode('utf-16') + (folder / name).write_bytes(xml) + self.assertEqual(module.check_app(app), []) diff --git a/VERSION b/VERSION index de629fc..b658675 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.4.2 +0.1.4.3 diff --git a/docs/architecture.md b/docs/architecture.md index 4eebc4a..3b873fc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,6 +1,6 @@ # NX Library — guida tecnica -Aggiornamento 9 novembre 2026 per NX Library 0.1.4.2. Il pin è `79741184`, con Metal 32023.922 / Xcode 27.2: adozione esplicitamente richiesta dal maintainer dopo la revisione di 71 commit. Gate e test automatici superati; una build/avvio manuale sull'esatto nuovo hash resta da confermare e non viene dichiarata eseguita. Ocarina installato è già su `7ffa63cc`, con la stessa catena shader e cinque commit di differenza. Vedi la [revisione engine](upstream-upgrades/2026-11-09-bd979ced-79741184-review.md) per evidenze, decisioni UI/backend e limiti. +Aggiornamento 12 novembre 2026 per NX Library 0.1.4.3. Il pin è `8a5c5e7e`, con Metal 32023.922 / Xcode 27.2: adozione esplicitamente richiesta dal maintainer dopo la revisione di 152 commit. I tool dell'engine sono nativi (niente .NET); NX Library ferma i build dando all'engine il tempo di chiudere i tool nelle loro sessioni e rifiuta import/ripristino dei salvataggi mentre un download cloud dell'engine è incompleto. Build e avvio reali sull'esatto hash restano da confermare dal maintainer. Vedi la [revisione engine](upstream-upgrades/2026-11-12-79741184-8a5c5e7e-review.md). Questa guida descrive il funzionamento attuale. Il [catalogo dei file](file-map.md) assegna una responsabilità a ogni file versionato, suddiviso per area. @@ -107,17 +107,29 @@ Il catalogo Settings descrive i singoli controlli; Changed riceve anche le diffe Le viste principali sono Spotlight, Collection e Details. Ricerca, Add Game, Console, Game Options e Settings sono raggiunti dalla shell e dalle azioni del router. Le estensioni `…+Input.swift` traducono tastiera, mouse e controller in modifiche ai modelli di focus. Le righe di Spotlight e Collection mettono ogni tessera al suo posto nella riga e scorrono con un'unica camera (`SpotlightRowGeometry`): la riga si muove come un pezzo solo, e una tessera creata al bordo mentre la riga ancora scorre non arriva prima delle vicine. Lo scorrimento è una molla smorzata, che a pressioni rapide riprende la velocità che ha invece di ripartire a ogni pressione; le tessere si creano quattro oltre ogni bordo dello schermo. In Spotlight posto e dimensione di ogni tessera e la camera si ricavano da un solo valore animato, la posizione della selezione (un indice, con frazioni a metà strada): animati separatamente (offset, scala, camera), la crescita di una tessera iniziata mentre la riga ancora scorreva partiva un paio di fotogrammi dopo il suo spostamento, e la tessera si staccava da quella alla sua destra. Una selezione che salta più tessere scorre attraverso quelle in mezzo; mentre una tessera è sollevata per spostarla cresce solo lei. +### Lingua e regione (non pubblicato, validato dal maintainer) + +`AppLanguage` definisce auto/en/it/fr/de/es/pt-BR; `AppLocale` risolve le preferenze ordinate del Mac e una regione indipendente, valida i codici e mappa pt-BR a pt soltanto per Nlib. `AppSettings` conserva i due override in modo additivo: vecchi file e scelte non riconosciute tornano ad Automatic. `NXLibraryApp.init` carica un solo `AppSettingsStore` e configura il locale prima della creazione delle scene; lo stesso store passa a `MainView` e `AppShellView`. Gli initializer delle viste non pubblicano cambi di locale: il controllo sincrono della toolchain può far rientrare il run loop mentre SwiftUI costruisce i propri StateObject. `AppLocalization` pubblica il contesto; la shell lo propaga alle viste tramite `nxAppLocale`, una dipendenza SwiftUI esplicita che non ricrea identità o stato. Settings passa questo valore alla sidebar e `SettingsSection.title(in:)` risolve le categorie nel contesto ricevuto, anche con focus sulle righe. Righe, informazioni, pannelli e suggerimenti condivisi consumano lo stesso valore. `L10n` mantiene al massimo un bundle per ciascuna delle sei lingue; le risorse iniettate restano isolate, senza memorizzare testi interpolati o dati personali. Cattura il contesto per il lavoro asincrono e legge esplicitamente il catalogo compilato, cercando prima nel bundle installato. Nessun servizio di posizione o traduzione esterno viene usato. + +I testi controllati dall'app sono nel catalogo condiviso `NXLibraryCore/Resources/Localizable.xcstrings`, con interpolazioni e plurali. Dati personali, rawValue persistiti, regex, eventi dell'engine, variabili DM_* e identità dei controlli restano indipendenti dalla lingua. I componenti Settings risolvono le etichette alla presentazione e mantengono i rowID originali; le righe coi nomi dei giochi usano titoli verbatim. Search confronta anche i testi tradotti. I testi upstream noti di launcher.json si localizzano solo nella presentazione; valori e note sconosciute conservano il fallback upstream. + +Start-up ospita lingua e regione. `LocalePicker` offre una scelta diretta delle lingue e ricerca i nomi localizzati delle regioni per codice stabile, con input tastiera/controller distinto da Settings. Le righe distinguono la lingua di menu, generi e descrizioni dai soli formati regionali. Il cambio non ricrea il router o le sessioni e non cambia la lingua di alcun gioco. Date e dimensioni rispettano il locale scelto; nomi dei giorni e durate hanno una presentazione localizzata. + +`GameGenre` traduce alla presentazione i generi noti senza modificare i metadati e le scelte salvate; generi sconosciuti restano verbatim. `NintendoStoreMetadataService` conserva le vecchie cache come inglesi e i percorsi per lingua/regione, riusando i dati tra regioni della stessa lingua. Un cambio di sola regione non aggiorna i metadati; un cambio lingua conserva la scheda visibile, legge la cache fuori dal thread principale e, con dati freschi già presenti, richiede soltanto la descrizione tradotta. Gli ID 0400 dei giochi Switch 2 sono ammessi al lookup Nlib come gli 0100. `descriptionCheckedAt` distingue un ripiego già controllato da una vecchia cache mai cercata nella lingua richiesta: una traduzione vuota viene ricontrollata dopo 24 ore, un errore temporaneo consente un nuovo tentativo dopo un minuto. Richiede una descrizione Nlib localizzata anche quando il catalogo US ne ha già una inglese; non invia parametri regione non documentati. Fallimenti parziali conservano prima una descrizione tradotta già presente in cache, senza rinnovarne artificialmente la freschezza; in sua assenza, fallimenti o descrizioni vuote conservano il ripiego inglese; il modello conserva la lingua della descrizione e Details e Spotlight indicano la lingua effettiva quando differisce dalla lingua dell’app. Scelte personali restano prioritarie. `metadataLocaleGeneration` impedisce a una risposta precedente di ripopolare la UI dopo un cambio lingua; screenshot e artwork rimangono condivisi. + +SwiftPM compila le risorse; build_app.sh le include prima della firma, mentre `check_localizations.py` verifica copertura delle sei lingue, placeholder, plurali e risorse compilate. release.sh ripete il controllo sullo ZIP estratto. Le traduzioni iniziali sono bozze dell'agente, non revisioni madrelingua certificate. Il maintainer ha confermato nella Dev i flussi e il cambio immediato di lingua, inclusa la sidebar senza spostare il focus; nessun servizio Crowdin/Transifex è stato aperto e la issue #10 non viene chiusa automaticamente. + ## Setup e compatibilità -`SystemDetector` e `EngineLocation` individuano checkout e cartelle dell’utente. `NXNativeEnvironment` raccoglie lo stato di Python, strumenti, Xcode, Metal e .NET. `EngineToolchain` risolve Python e `PATH`, preferendo il runtime incluso nel bundle quando disponibile. +`SystemDetector` e `EngineLocation` individuano checkout e cartelle dell’utente. `NXNativeEnvironment` raccoglie lo stato di Python, strumenti, Xcode e Metal; da nxnative 641636e2 i tool dell'engine sono eseguibili nativi e .NET non serve più. `EngineToolchain` risolve Python e `PATH`, preferendo il runtime incluso nel bundle quando disponibile. -`SetupProbe` costruisce i dati del Setup. `SetupCheck` li traduce in schede, stati e azioni. `SetupCheckModel` esegue i controlli in background e coordina installazioni e avanzamento. `SetupActions` prepara gli argomenti Git per portare un checkout gestibile al pin. `DotnetRelease` seleziona il pacchetto Microsoft e verifica SHA-512 prima dell’apertura nell’Installer di macOS. +`SetupProbe` costruisce i dati del Setup. `SetupCheck` li traduce in schede, stati e azioni. `SetupCheckModel` esegue i controlli in background e coordina installazioni e avanzamento. `SetupActions` prepara gli argomenti Git per portare un checkout gestibile al pin. -Get ready mostra soltanto i sei requisiti tecnici, in una griglia 3×2; i problemi alle cartelle non riaprono questa schermata. Your games presenta Game apps, la cartella unica in cui cercare giochi nxnative esistenti e installare le prime build. Il default `~/Applications` può essere preparato al primo uso solo se non è già stato scelto o ricordato un percorso. Le destinazioni personalizzate o confermate rimangono ricordate anche quando non disponibili. La lettura basta per scoperta e Play; le nuove installazioni richiedono scrittura. Game files rimane una preferenza facoltativa in Settings → Library, separata dalla selezione temporanea del wizard. Cambiare Game apps non sposta app già registrate; cambiare Game files non sostituisce i sorgenti di un titolo senza conferma esplicita. `GameFoldersCopy` mantiene i nomi coerenti. +Get ready mostra soltanto i cinque requisiti tecnici, in una griglia 3 + 2; i problemi alle cartelle non riaprono questa schermata. Your games presenta Game apps, la cartella unica in cui cercare giochi nxnative esistenti e installare le prime build. Il default `~/Applications` può essere preparato al primo uso solo se non è già stato scelto o ricordato un percorso. Le destinazioni personalizzate o confermate rimangono ricordate anche quando non disponibili. La lettura basta per scoperta e Play; le nuove installazioni richiedono scrittura. Game files rimane una preferenza facoltativa in Settings → Library, separata dalla selezione temporanea del wizard. Cambiare Game apps non sposta app già registrate; cambiare Game files non sostituisce i sorgenti di un titolo senza conferma esplicita. `GameFoldersCopy` mantiene i nomi coerenti. -`CompatibilityChecker` verifica commit, modifiche del checkout, contratto CLI e compilatore Metal richiesto dal lock del motore. `ToolchainRequirements.current` contiene Xcode minimo, major .NET e asset Metal. `ToolchainGateCopy` produce la spiegazione del risultato per la UI. +`CompatibilityChecker` verifica commit, modifiche del checkout, contratto CLI e compilatore Metal richiesto dal lock del motore. `ToolchainRequirements.current` contiene Xcode minimo e asset Metal. `ToolchainGateCopy` produce la spiegazione del risultato per la UI. -Il lock del pin attuale certifica Metal `32023.922`, asset Apple `27B47`, con Xcode minimo `27.2`. La [nota dell’upgrade del 6 novembre](upstream-upgrades/2026-11-06-4a0551e4-04df08d4-review.md) registra 04df08d4, confermato dal maintainer con un build di Ocarina of Time; quella [bd979ced → 79741184](upstream-upgrades/2026-11-09-bd979ced-79741184-review.md) registra il pin della 0.1.4.2, adottato su richiesta esplicita del maintainer con gate e test automatici superati ma build/avvio sull'esatto hash ancora non confermati; quella [e5f03403 → bd979ced](upstream-upgrades/2026-11-08-e5f03403-bd979ced-review.md) registra il pin della 0.1.4.1; quella [7fe9fc4d → e5f03403](upstream-upgrades/2026-11-07-7fe9fc4d-e5f03403-review.md) registra il pin precedente, confermato dal maintainer con un rebuild di Ocarina of Time; quella [b2d46b86 → 7fe9fc4d](upstream-upgrades/2026-11-07-b2d46b86-7fe9fc4d-review.md) registra il pin precedente, confermato dal maintainer con un rebuild di Ocarina of Time; quella [04df08d4 → b2d46b86](upstream-upgrades/2026-11-06-04df08d4-b2d46b86-review.md) registra b2d46b86, che aggiunge il controllo dei file `.zs` danneggiati in NUP e cartelle (riga `files.format` del classificatore); quella [del 5 novembre](upstream-upgrades/2026-11-05-b64a5ff0-4a0551e4-review.md) registra gate certificato senza override, build reale di Wonder con archivio pipeline e avvio confermato dall’utente per 4a0551e4. Un build con override riceve `toolchainVerified=false` e `--allow-other-metal`. Play valuta la disponibilità dell’app già installata separatamente dal gate per un nuovo porting. +Il lock del pin attuale certifica Metal `32023.922`, asset Apple `27B47`, con Xcode minimo `27.2`. La [revisione 79741184 → 8a5c5e7e](upstream-upgrades/2026-11-12-79741184-8a5c5e7e-review.md) registra il pin della 0.1.4.3, adottato su richiesta esplicita del maintainer; la [nota dell’upgrade del 6 novembre](upstream-upgrades/2026-11-06-4a0551e4-04df08d4-review.md) registra 04df08d4, confermato dal maintainer con un build di Ocarina of Time; quella [bd979ced → 79741184](upstream-upgrades/2026-11-09-bd979ced-79741184-review.md) registra il pin della 0.1.4.2, adottato su richiesta esplicita del maintainer con gate e test automatici superati ma build/avvio sull'esatto hash ancora non confermati; quella [e5f03403 → bd979ced](upstream-upgrades/2026-11-08-e5f03403-bd979ced-review.md) registra il pin della 0.1.4.1; quella [7fe9fc4d → e5f03403](upstream-upgrades/2026-11-07-7fe9fc4d-e5f03403-review.md) registra il pin precedente, confermato dal maintainer con un rebuild di Ocarina of Time; quella [b2d46b86 → 7fe9fc4d](upstream-upgrades/2026-11-07-b2d46b86-7fe9fc4d-review.md) registra il pin precedente, confermato dal maintainer con un rebuild di Ocarina of Time; quella [04df08d4 → b2d46b86](upstream-upgrades/2026-11-06-04df08d4-b2d46b86-review.md) registra b2d46b86, che aggiunge il controllo dei file `.zs` danneggiati in NUP e cartelle (riga `files.format` del classificatore); quella [del 5 novembre](upstream-upgrades/2026-11-05-b64a5ff0-4a0551e4-review.md) registra gate certificato senza override, build reale di Wonder con archivio pipeline e avvio confermato dall’utente per 4a0551e4. Un build con override riceve `toolchainVerified=false` e `--allow-other-metal`. Play valuta la disponibilità dell’app già installata separatamente dal gate per un nuovo porting. L'engine più nuovo è una scelta esplicita del giocatore (Settings → Engine → **Newest engine**). Dopo il controllo Upstream, che misura sempre i commit rispetto al pin, `NewestEngine.fetchLock` legge il lock di main da GitHub: se certifica un Metal diverso da quello del lock del pin, la riga dice che serve un NX Library più nuovo e non offre il cambio. `NewestEngineSwitcher` rifiuta un checkout con file modificati, scarica, rilegge il lock con git e solo allora fa `checkout --detach`. La scelta (`NewestEngineChoice`) è salvata, diventa il commit atteso dal gate e quello registrato nelle build, accende l'override (che resta bloccato finché la scelta è attiva) e marca le build come non testate; il gate mostra **Newest engine** in arancione, mai Certified. Il Setup continua a installare il pin, ma per il commit scelto mostra la scheda pronta in arancione con **Back to tested** e non la riporta indietro da solo. La scelta decade quando il checkout non è più su quel commit o quando un aggiornamento di NX Library porta un pin che lo contiene; se l'override l'aveva acceso la scelta, torna spento. Settings e Setup usano la stessa frase (`NewestEngineCopy.note`). @@ -206,6 +218,14 @@ All’avvio o al refresh, un journal incompleto porta al rollback del salvataggi Storage → Save backups apre la cartella comune degli snapshot. Il vecchio export ZIP di Storage è stato rimosso; la creazione delle copie passa dal pannello Saves. +### Spazio per gioco (non pubblicato, validato dal maintainer) + +Settings → Storage riusa `StorageManager.snapshot`: `GameStorageUsage` distingue app, cache e log per voce, compresi i giochi nascosti, ordinando le cache dalla più grande. `StorageSize` distingue file assenti da percorsi non leggibili o volumi scollegati. Le misure sono dimensioni logiche dei file, inclusi quelli nascosti; la visita fisica fts non segue i collegamenti interni e scarta misure incomplete in caso di errore. I percorsi canonici condivisi vengono misurati e contati una sola volta nei totali. Lo spazio libero è quello della cartella Game apps corrente e non ripiega sul disco interno se questa manca. + +La conferma per gioco conserva identità e percorso selezionati. Prima della cancellazione, il coordinatore ricontrolla la voce corrente; `StorageManager` richiede la cartella `<title ID>_build` immediatamente sotto il workspace registrato, rifiuta file ordinari e cache che siano symlink e protegge app, sorgenti e installazioni differite di tutte le voci. Un collegamento dentro la cache viene rimosso senza visitarne la destinazione. Le cache condivise sono indicate sia nella riga sia nella conferma. Non viene rimossa l'intera cartella workspace né modificato il catalogo: una ricompilazione ricrea la cache. + +Misura e pulizia girano sulla coda utility. `isCleaningStorage` blocca nuove pulizie, build e installazioni dell'updater; i giochi già installati restano avviabili. Una misura superata da cambi di libreria o destinazione viene ripetuta. Il termine della pulizia pubblica l'esito, espone eventuali errori e ricalcola le dimensioni. Le righe dei giochi hanno identità UUID stabili per mouse, tastiera e controller. Il maintainer ha confermato il funzionamento nella Dev il 9 novembre 2026; test automatici e build sostengono la verifica. + I backup sono copie locali conservate fino alla gestione dell’utente. Sono automatici prima di importazione e ripristino, oppure manuali con Back up now. Importazione e ripristino richiedono sincronizzazione cloud effettivamente disattivata. I casi rifiutati e le verifiche eseguite sono descritti nelle sezioni Saves del README e dei test. ## Artwork, metadati e sfondi diff --git a/docs/development.md b/docs/development.md index 38ba777..d911e2f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -10,7 +10,7 @@ This guide covers building NX Library itself. For app setup, game preparation an - Python 3 for the publication checks and packaging helpers. - Homebrew's `zstd` and `glslang`, at the versions recorded in [runtime.lock](../runtime.lock), when preparing the bundled runtime. The script checks installed versions; it does not install or silently upgrade them. -Building the frontend alone does not need games or keys. Running the game's build workflow also needs the .NET 10 SDK, the pinned nxnative checkout and the user's own authorised sources and keys. Setup explains and prepares the supported prerequisites. +Building the frontend alone does not need games or keys. Running the game's build workflow also needs the pinned nxnative checkout (its tools are native executables since 641636e2, so no .NET) and the user's own authorised sources and keys. Setup explains and prepares the supported prerequisites. ## Build a development app @@ -56,6 +56,7 @@ Debug builds recognise `--show-setup` and `--preview-build` for manual inspectio ~~~sh python3 scripts/check_publication.py +python3 scripts/check_localizations.py python3 -m unittest discover -s Tests/PublicationChecks DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test bash scripts/build_app.sh @@ -141,3 +142,7 @@ These are distribution decisions. Do not bypass the dirty-tree check, change the | `docs/` | Current guides, audits, dated designs and engine-adoption evidence. | For component interactions, read the [architecture guide](architecture.md). For the role of every individual file, use the [file catalog](file-map.md). Screenshots and demo placement are described in the [media guide](media/README.md). + +### Localization resources + +SwiftPM processes the shared string catalog into `NXLibrary_NXLibraryCore.bundle`. `build_app.sh` requires and copies that bundle into `Contents/Resources`, declares the six supported app languages and validates the compiled strings/plurals before signing. `scripts/check_localizations.py --app <path.app>` verifies the same resources independently. Missing resources or placeholder mismatches fail packaging. Locale selection is independent from each game's DM_LANG setting. diff --git a/docs/file-map.md b/docs/file-map.md index 7fbdd2b..0508861 100644 --- a/docs/file-map.md +++ b/docs/file-map.md @@ -115,6 +115,7 @@ La pubblicazione comprende anche [CONTRIBUTING.md](../CONTRIBUTING.md), [SECURIT | File | Responsabilità | |---|---| | [Sources/NXLibraryCore/Models/PortedGame.swift](../Sources/NXLibraryCore/Models/PortedGame.swift) | Modello persistente del gioco con bundle, sorgenti, impostazioni, build e installazione in attesa. | +| [Sources/NXLibraryCore/Models/GameStorageUsage.swift](../Sources/NXLibraryCore/Models/GameStorageUsage.swift) | Dimensioni per gioco e totali: distingue assenza e indisponibilità, segnala cache condivise e conserva identità stabili per il focus. | | [Sources/NXLibraryCore/Models/LibraryFile.swift](../Sources/NXLibraryCore/Models/LibraryFile.swift) | Contenitore versionato del catalogo della libreria. | | [Sources/NXLibraryCore/Models/GameStatus.swift](../Sources/NXLibraryCore/Models/GameStatus.swift) | Enumera disponibilità e stato del gioco rispetto ad app, sorgenti e porting. | | [Sources/NXLibraryCore/Models/GameResourceState.swift](../Sources/NXLibraryCore/Models/GameResourceState.swift) | Distingue app mancanti, sorgenti assenti e workspace non compilati, riconcilia gli stati e conserva Play per una build esistente. | @@ -141,7 +142,7 @@ La pubblicazione comprende anche [CONTRIBUTING.md](../CONTRIBUTING.md), [SECURIT | File | Responsabilità | |---|---| -| [Sources/NXLibraryCore/Models/ToolchainRequirements.swift](../Sources/NXLibraryCore/Models/ToolchainRequirements.swift) | Centralizza requisiti Xcode, .NET e asset Metal e confronta versioni numeriche. | +| [Sources/NXLibraryCore/Models/ToolchainRequirements.swift](../Sources/NXLibraryCore/Models/ToolchainRequirements.swift) | Centralizza requisiti Xcode e asset Metal e confronta versioni numeriche. | | [Sources/NXLibraryCore/Models/ToolchainInfo.swift](../Sources/NXLibraryCore/Models/ToolchainInfo.swift) | Registra provenienza e verifica della toolchain associata a un build. | | [Sources/NXLibraryCore/Models/ToolchainGateCopy.swift](../Sources/NXLibraryCore/Models/ToolchainGateCopy.swift) | Traduce il gate in testi distinguendo Metal certificato, precedente e successivo. | | [Sources/NXLibraryCore/Models/SetupCheck.swift](../Sources/NXLibraryCore/Models/SetupCheck.swift) | Converte fatti e override del Setup in schede, stati, azioni e condizioni di riapertura. | @@ -149,7 +150,7 @@ La pubblicazione comprende anche [CONTRIBUTING.md](../CONTRIBUTING.md), [SECURIT | [Sources/NXLibraryCore/Models/GameFoldersCopy.swift](../Sources/NXLibraryCore/Models/GameFoldersCopy.swift) | Condivide nomi delle cartelle, formati e descrizioni tra Setup, Settings e selettori dei percorsi. | | [Sources/NXLibraryCore/Models/SetupGrid.swift](../Sources/NXLibraryCore/Models/SetupGrid.swift) | Calcola disposizione, centratura e navigazione del focus nella griglia del Setup. | | [Sources/NXLibraryCore/Models/UpstreamCopy.swift](../Sources/NXLibraryCore/Models/UpstreamCopy.swift) | Prepara i testi del changelog upstream e dell’adozione degli aggiornamenti tramite NX Library. | -| [Sources/NXLibraryCore/Services/NXNativeEnvironment.swift](../Sources/NXLibraryCore/Services/NXNativeEnvironment.swift) | Ispeziona strumenti, Python, Xcode, Metal e .NET e restituisce uno stato dell’ambiente. | +| [Sources/NXLibraryCore/Services/NXNativeEnvironment.swift](../Sources/NXLibraryCore/Services/NXNativeEnvironment.swift) | Ispeziona strumenti, Python, Xcode e Metal e restituisce uno stato dell’ambiente. | | [Sources/NXLibraryCore/Services/EngineLocation.swift](../Sources/NXLibraryCore/Services/EngineLocation.swift) | Risolve checkout predefinito o scelto e produce i candidati per prod.keys. | | [Sources/NXLibraryCore/Services/EngineToolchain.swift](../Sources/NXLibraryCore/Services/EngineToolchain.swift) | Individua runtime incluso o venv del checkout e prepara ambiente e PATH dei processi. | | [Sources/NXLibraryCore/Services/EnginePackageReader.swift](../Sources/NXLibraryCore/Services/EnginePackageReader.swift) | Legge capacità delle sorgenti e content meta di tutti i pacchetti in un solo passaggio del lettore upstream (`package_reader`, o `package_title_reader` sugli engine precedenti a 5c4dc18f), validando identità e tipi senza esporre chiavi. | @@ -164,7 +165,6 @@ La pubblicazione comprende anche [CONTRIBUTING.md](../CONTRIBUTING.md), [SECURIT | [Sources/NXLibraryCore/Services/SetupProbe.swift](../Sources/NXLibraryCore/Services/SetupProbe.swift) | Raccoglie i fatti mostrati dal Setup usando comandi e controlli mirati. | | [Sources/NXLibraryCore/Services/SetupActions.swift](../Sources/NXLibraryCore/Services/SetupActions.swift) | Costruisce le operazioni Git ammesse per installare o allineare il motore al pin. | | [Sources/NXLibraryCore/Services/InstallProgress.swift](../Sources/NXLibraryCore/Services/InstallProgress.swift) | Interpreta avanzamento Git, percentuali, byte e righe parziali per le installazioni. | -| [Sources/NXLibraryCore/Services/DotnetRelease.swift](../Sources/NXLibraryCore/Services/DotnetRelease.swift) | Seleziona il pacchetto SDK Microsoft dai metadati e ne verifica SHA-512. | | [Sources/NXLibraryCore/Services/KeyStore.swift](../Sources/NXLibraryCore/Services/KeyStore.swift) | Valida le chiavi, espone solo il riepilogo e pubblica atomicamente la copia gestita con permessi 0600. | | [Sources/NXLibraryCore/Services/KeyCheck.swift](../Sources/NXLibraryCore/Services/KeyCheck.swift) | Legge intestazioni NCA e ticket e verifica se le chiavi possono aprire le sorgenti selezionate. | | [Sources/NXLibraryCore/Services/CommandRunner.swift](../Sources/NXLibraryCore/Services/CommandRunner.swift) | Confine sostituibile per comandi e output, con esecuzione sincrona o streaming delle righe. | @@ -201,7 +201,7 @@ La pubblicazione comprende anche [CONTRIBUTING.md](../CONTRIBUTING.md), [SECURIT | [Sources/NXLibraryCore/Services/BuildOutputInstaller.swift](../Sources/NXLibraryCore/Services/BuildOutputInstaller.swift) | Pubblica il bundle con rinomina o scambio atomico, prepara copie fra volumi e riconosce i tentativi già completati tramite ricevuta. | | [Sources/NXLibraryCore/Services/GameAppDestination.swift](../Sources/NXLibraryCore/Services/GameAppDestination.swift) | Risolve la destinazione condivisa da review e porting, conserva i percorsi dei rebuild, evita conflitti di nome e verifica la cartella di destinazione. | | [Sources/NXLibraryCore/Services/LaunchService.swift](../Sources/NXLibraryCore/Services/LaunchService.swift) | Scrive l’ambiente runtime e avvia il bundle del gioco tramite NSWorkspace. | -| [Sources/NXLibraryCore/Services/StorageManager.swift](../Sources/NXLibraryCore/Services/StorageManager.swift) | Misura app, cache e log e ne gestisce la pulizia. I backup appartengono al flusso Saves. | +| [Sources/NXLibraryCore/Services/StorageManager.swift](../Sources/NXLibraryCore/Services/StorageManager.swift) | Misura app, cache e log per gioco e totali senza duplicare percorsi condivisi; verifica cartelle e sorgenti prima della pulizia selettiva. I backup appartengono al flusso Saves. | | [Sources/NXLibraryCore/Services/DiagnosticsReport.swift](../Sources/NXLibraryCore/Services/DiagnosticsReport.swift) | Genera il rapporto diagnostico e rimuove percorso home e valori riconosciuti come chiavi. | | [Sources/NXLibraryCore/Services/AppUpdate.swift](../Sources/NXLibraryCore/Services/AppUpdate.swift) | Definisce repository, versioni, requisiti, limiti di download, digest, firma e regole di verifica e sostituzione dell’app NX Library. | | [Sources/NXLibraryCore/Models/AppUpdatePresentation.swift](../Sources/NXLibraryCore/Models/AppUpdatePresentation.swift) | Definisce gli stati dell’updater e il gate di installazione per versione e tipo di copia. | @@ -492,7 +492,6 @@ La pubblicazione comprende anche [CONTRIBUTING.md](../CONTRIBUTING.md), [SECURIT | [Tests/NXLibraryCoreTests/DetailsFocusTests.swift](../Tests/NXLibraryCoreTests/DetailsFocusTests.swift) | Spostamento nelle bande di Details, limiti e sezioni vuote. | | [Tests/NXLibraryCoreTests/DirectorySizeTests.swift](../Tests/NXLibraryCoreTests/DirectorySizeTests.swift) | Somma dei file regolari ignorando nascosti e collegamenti; cartella mancante a zero. | | [Tests/NXLibraryCoreTests/DiagnosticsReportTests.swift](../Tests/NXLibraryCoreTests/DiagnosticsReportTests.swift) | Rimozione di percorsi personali e chiavi e contenuto delle sezioni diagnostiche. | -| [Tests/NXLibraryCoreTests/DotnetReleaseTests.swift](../Tests/NXLibraryCoreTests/DotnetReleaseTests.swift) | Selezione del pacchetto ARM64 e verifica del suo hash. | | [Tests/NXLibraryCoreTests/EngineLocationTests.swift](../Tests/NXLibraryCoreTests/EngineLocationTests.swift) | Percorso predefinito, cartella scelta e fallback quando scompare. | | [Tests/NXLibraryCoreTests/EngineToolchainTests.swift](../Tests/NXLibraryCoreTests/EngineToolchainTests.swift) | Rilevamento del runtime incluso e fallback a Python e strumenti del checkout. | | [Tests/NXLibraryCoreTests/EnvironmentTests.swift](../Tests/NXLibraryCoreTests/EnvironmentTests.swift) | Parsing di Metal e controlli dell’ambiente mediante runner sintetico. | @@ -562,7 +561,7 @@ La pubblicazione comprende anche [CONTRIBUTING.md](../CONTRIBUTING.md), [SECURIT | [Tests/NXLibraryCoreTests/SpotlightPageTests.swift](../Tests/NXLibraryCoreTests/SpotlightPageTests.swift) | Estratti del testo e stati dello scorrimento della pagina del gioco. | | [Tests/NXLibraryCoreTests/SpotlightRowTests.swift](../Tests/NXLibraryCoreTests/SpotlightRowTests.swift) | Ordine, card, riordino delle tile e selezione Surprise. | | [Tests/NXLibraryCoreTests/SpotlightStateTests.swift](../Tests/NXLibraryCoreTests/SpotlightStateTests.swift) | Focus, selezione, filtri, azioni e spostamento delle tile Spotlight. | -| [Tests/NXLibraryCoreTests/StorageManagerTests.swift](../Tests/NXLibraryCoreTests/StorageManagerTests.swift) | Misure e cancellazioni di cache e log. | +| [Tests/NXLibraryCoreTests/StorageManagerTests.swift](../Tests/NXLibraryCoreTests/StorageManagerTests.swift) | Misure per gioco, cache condivise e nascoste, pulizia selettiva, protezione di app/sorgenti/symlink, file incoerenti e volumi/cartelle non disponibili. | | [Tests/NXLibraryCoreTests/ThemePickerTests.swift](../Tests/NXLibraryCoreTests/ThemePickerTests.swift) | Navigazione della griglia, tab e selezione degli sfondi. | | [Tests/NXLibraryCoreTests/ToolchainGateCopyTests.swift](../Tests/NXLibraryCoreTests/ToolchainGateCopyTests.swift) | Testi del gate per Metal certificato, precedente, successivo e altre cause di blocco. | | [Tests/NXLibraryCoreTests/ToolchainRequirementsTests.swift](../Tests/NXLibraryCoreTests/ToolchainRequirementsTests.swift) | Versioni minime e identificativi degli asset Metal da scaricare. | @@ -646,3 +645,17 @@ La pubblicazione comprende anche [CONTRIBUTING.md](../CONTRIBUTING.md), [SECURIT ## Aggiornare questo catalogo Quando si aggiunge, rinomina o rimuove un file versionato, aggiornare la sua riga e il totale iniziale. Quando cambia un flusso o una responsabilità, aggiornare anche la sezione corrispondente della guida tecnica. Le descrizioni dei test indicano l’ambito dei controlli, mentre l’esito di esecuzione appartiene ai rapporti di verifica. + +## Localizzazione (hotfix non pubblicato) + +| File | Responsabilità | +|---|---| +| [Sources/NXLibraryCore/Models/AppLocale.swift](../Sources/NXLibraryCore/Models/AppLocale.swift) | Sei lingue, risoluzione delle preferenze e regione indipendente; nomi e ricerca delle regioni. | +| [Sources/NXLibraryCore/Models/GameGenre.swift](../Sources/NXLibraryCore/Models/GameGenre.swift) | Generi noti tradotti alla presentazione; dati originali e generi personali conservati. | +| [Sources/NXLibrary/Views/Theme/AppLocaleEnvironment.swift](../Sources/NXLibrary/Views/Theme/AppLocaleEnvironment.swift) | Contesto della lingua reattivo per le viste; conserva identità, selezione e focus. | +| [Sources/NXLibraryCore/Services/AppLocalization.swift](../Sources/NXLibraryCore/Services/AppLocalization.swift) | Contesto osservabile, lookup portabile delle risorse e interpolazioni/plurali. | +| [Sources/NXLibraryCore/Resources/Localizable.xcstrings](../Sources/NXLibraryCore/Resources/Localizable.xcstrings) | Testi condivisi e traduzioni en/it/fr/de/es/pt-BR. | +| [Sources/NXLibrary/Views/Settings/LocalePicker.swift](../Sources/NXLibrary/Views/Settings/LocalePicker.swift) | Scelta diretta della lingua e ricerca dei formati regionali, con focus tastiera/controller e codici stabili. | +| [Tests/NXLibraryCoreTests/AppLocaleTests.swift](../Tests/NXLibraryCoreTests/AppLocaleTests.swift) | Preferenze, migrazione, formati, ricerca, identità e traduzioni native. | +| [scripts/check_localizations.py](../scripts/check_localizations.py) | Verifica copertura, placeholder, plurali e risorse nel bundle installato. | +| [Tests/PublicationChecks/test_localizations.py](../Tests/PublicationChecks/test_localizations.py) | Rifiuto di cataloghi incompleti e bundle senza risorse. | diff --git a/docs/upstream-upgrades/2026-11-12-79741184-8a5c5e7e-review.md b/docs/upstream-upgrades/2026-11-12-79741184-8a5c5e7e-review.md new file mode 100644 index 0000000..e3f3493 --- /dev/null +++ b/docs/upstream-upgrades/2026-11-12-79741184-8a5c5e7e-review.md @@ -0,0 +1,212 @@ +# 2026-11-12 — nxnative 79741184 → 8a5c5e7e review + +**Status: adopted and published in 0.1.4.3 at the maintainer's explicit direction on 2026-11-12.** Previous pin: `79741184af1558fa1ada0f61741eeecbd615b3f6`. New pin: `8a5c5e7e1ee606c650fe1aef6959a83009b6a2fc` (upstream `main` fetched again just before adoption), **152 commits, 572 files**, all from 9–12 November. The maintainer asked to publish 0.1.4.3 with the latest upstream engine rather than keep the 0.1.4.2 pin. That overrides the 0.1.4.3 readiness note's "defer speculative engine upgrades" for this release only. **This review does not claim a real build or a visible launch on the exact new commit.** Upstream commits after 8a5c5e7e are outside it. + +Upstream changed quickly in this range. Several commits repair regressions that other commits in the same range introduced: the iOS link, the gfx tests, lint baselines, `BrokenProcessPool` in the new shader workers (bcc4483d after 8b55ebb3), and the native translator on macOS 26 (380ff71a after 641636e2). That is a reason to validate on this Mac before reporting the engine as working, not a reason to skip the release. + +## Contract surface + +| Surface | Change | Effect on NX Library | +|---|---|---| +| `dmsh_chain.lock` Metal | unchanged: `32023.922` (macOS and iOS) | `ToolchainRequirements.current` unchanged; the gate certifies the same compiler | +| `dmsh_chain.lock` tools | `dmsh_xlate.dll` + Ryujinx dlls → native `dmsh_xlate` | The chain changes (also f75ac06e, 511a0a2c): **every game re-translates its shaders once at its next build** (`shaders_stale` reason `chain`) | +| README `pip install` | unchanged | `runtime.lock` unchanged; `EngineRuntimeRequirementsTests` passes against the new pin | +| `build_ipa_from_nsp.py` options | one new option: `--fallback-icon` | Not adopted (see below) | +| Exit codes (`docs/frontends.md`) | new 143 (SIGTERM), 129 (SIGHUP); a step whose tool leaves a process running 5 s after exiting now fails | A cancel already counts as cancelled whatever the code; stop timing adapted (see below) | +| `game` event `icon` | may be the `--fallback-icon` image | NX Library passes none, so unchanged | +| Stages | `build nx_extract` and `survey: build nx_extract` are gone | Progress shows fewer stages; no hard-coded stage list | +| `docs/env_switches.tsv` | ~25 new rows, nearly all `diagnostic` or measurement `switch` | No main UI control (guardrail). Player-level rows: `DM_RENDER_SCALE` max can exceed 1.0 (Hades II: 3.0); `DM_PAD_PRESS_TO_JOIN` is unchanged in `settings.tsv` | +| Engine internals NX Library imports | `configure_dotnet` removed; `package_reader`, `pack_dump.*`, `pack_nup.*` unchanged | **Fixed**: the package probe guarded | +| Cloud saves | Mac launcher asks which save to keep (`save_sync_report`); pull journal `<DM_SAVE>.pull` | Asks only with the launcher window; NX Library starts with `DM_LAUNCHER=0` (see below) | +| Relaunch (4ce1449f) | `oe::RequestToRelaunchApplication` execs the host in place | Same PID: session tracking and the session card are unaffected | + +`Front-ends:` lines read in the whole range: 0a764c09, e6048dea, 84d36c63, 0b124486 (×2), 07d6ea46 (×2), 5714e328, 511a0a2c, 641636e2 (×2), b84ee29a. The last two commits (637f3fa7, 8a5c5e7e) are host-only and have no such line. + +## Integration decisions + +| Capability / change | Decision | Reason / practical effect | +|---|---|---| +| **No .NET** (641636e2, 380ff71a) | **Backend + Setup/UI change** | `EnginePackageReader`'s probe called `engine.configure_dotnet`, which the engine removed. With a Newest-engine checkout at or after 641636e2 (the maintainer's released app was at a77c0ee8), **every NSP/XCI/NSZ/XCZ add failed** with `module 'nxlibrary_probe' has no attribute 'configure_dotnet'`, reproduced with the exact probe. It is now called only when it exists, and a regression test covers an engine without it. The .NET 10 SDK card, its Microsoft downloader (`DotnetRelease`), the .NET row in Settings → Engine, the build gate's ".NET 10 SDK is missing" issue and the diagnostics line are removed: they would block or prompt for a tool the engine no longer uses. Setup always moves the checkout to at least this pin, and Newest only moves forward, so no supported engine needs .NET. Get ready now shows five cards (3 + 2). The `translator` failure now says the engine's translator is missing or damaged and points at the nxnative engine card, matching the new engine message; it no longer suggests installing .NET. | +| **Tools in sessions of their own** (0b124486, 84d36c63) | **Backend** | NX Library stopped a build with `killpg(SIGTERM)` and `SIGKILL` 5 s later, both on the script's group. Each tool now leads its own session, which neither signal reaches; the engine's own SIGTERM handler stops it within 5 s. So a SIGKILL at 5 s could race the engine, and **quitting NX Library mid-build (immediate SIGKILL) left the shader build or xcodebuild running with no parent**. Now a cancel waits 8 s before killing, a quit sends the same SIGTERM and waits up to 8 s, and the fallback kills the build's group and every descendant listed before any is killed. Three tests use real process trees with a tool in its own session. | +| **Cloud-save pull journal** (e6048dea, 06c2ea4c) | **Backend** | While `<save>.pull` exists, the game's next start puts this device's save back. NX Library already leaves the file alone (it reads only `save/` and `save_device/`), and refuses writes while cloud saves are on. It now also refuses an import or restore while a journal is pending (cloud turned off after a cut-short pull) and says to play once first. Tested; the message is translated into six languages. | +| **Cloud-save conflict question** (07d6ea46, 0a764c09) | **Not adopted in this release (follow-up)** | The engine asks only in its launcher (window shown, or `window.show` off without `DM_LAUNCHER=0`). NX Library starts every game with `DM_LAUNCHER=0` so its Game options replace the window. Its players therefore keep the host's previous rule (the newer file wins, the other kept in `-backups`), the same as 0.1.4.2. Upstream documents why that rule can lose progress. Fixing it needs a product decision: NX Library's own Play-time comparison through the documented formats, or letting the engine's alerts run. Neither is built or claimed here. | +| Render scale above 100 % (b84ee29a) | **Not adopted (follow-up)** | Only Hades II offers it. NX Library's fixed 50–100 % row keeps working; the engine's own range is not exposed above 100 %. | +| `--fallback-icon` (5714e328) | **Not adopted** | Same reasoning as `--icon` in the 0.1.4.2 review: NX Library's reversible icon writer keeps true original provenance; a packed stand-in would look like the game's own icon. | +| Shader/pipeline, GPU memory, textures, movies, input, ProMotion 60 fps, Switch 2 full docked picture, -O2 host | **Backend** | Takes effect in rebuilt game apps; installed apps keep their engine until rebuilt. | +| New or newly playable games (Red Dead Redemption, TUNIC, both Ori games, Metroid Prime 4: Beyond, The Binding of Isaac: Rebirth, PRAGMATA, Diablo III, Diablo II: Resurrected, Nintendo Switch Sports Resort, Hades II 1.0.141667, Mario Kart World 1.8.0 frame step, Smash language restart, Drag x Drive) | **Backend** | Profiles come from the engine. None of these titles was tested by the maintainer in this review. | +| Diagnostics (`DM_IMU_*`, `DM_NVN_*` measurement switches, `DM_CRASH_PEEK`, `DM_GFX_SCALE`…) | **No UI** | Guardrail: diagnostic switches stay out of the main UI. | + +## Verification and adoption record + +- The engine's message table (`PortingFailureReasonTests`, with `NX_TEST_ENGINE_COMMIT` set) and `EngineRuntimeRequirementsTests` passed against upstream `main` before the change, on the 0.1.4.3 candidate 6ab6ba1 in a temporary worktree. +- The new and changed suites pass: package probe without `configure_dotnet`, Setup without .NET, build stop with tools in their own session (quit with a graceful engine, quit and cancel with a script that ignores SIGTERM), refusal while a pull journal is pending. The localization check passes for six languages. The full suite and the Dev build are recorded in the release notes for this candidate. +- Real build on exactly 8a5c5e7e, in NX Library Dev after Setup moved the checkout: The Legend of Zelda: Ocarina of Time (0400486024DEC000, v131072) built with every stage ok. Shader split 42,848 shaders; Mac shaders 42,848 of 42,848 ready, 0 from the cache (the chain change), in 1,179 s; Mac host 15 s; pipeline archive `applegpu_g17s`, 4.3 GB, in 802 s; Mac app 34.5 GB in 121 s; the icon was applied. The installed app's `DMCommit` is `8a5c5e7e1ee6`, and the game was running when the maintainer asked to publish. +- Not performed before publication: the cancel and quit-mid-build checks on a real build (covered only by the automated process-tree tests), an NSP/XCI Add Game on the new engine, and other games. These remain open manual checks. + +## Focused maintainer check + +1. **Setup / Settings → Engine.** Update to 8a5c5e7e: the released app's Newest choice at a77c0ee8 is included in the pin, so it is retired and the engine card offers **Update**. Get ready shows five cards (Xcode, Metal compiler, nxnative engine, Python and tools, Keys) in a 3 + 2 grid, with no .NET card. The compatibility gate is green, Metal 32023.922. +2. **Add Game with an NSP/XCI** (the reproduced failure). Title and version are read; no `configure_dotnet` error. +3. **Build one real game** with its existing cache. The shader step reports re-translation for the new chain. Check that the build finishes and the app launches to a visible screen. +4. **Cancel a build** during the shader step: it stops within about 8 s as **Cancelled**. Then run `pgrep -fl "dmsh|metal|xcodebuild"`: it should list nothing left from the build. +5. **Quit NX Library during a build**: it closes within about 8 s. Run the same `pgrep`: nothing left running. +6. Saves panel with cloud saves off: import/restore still work normally (there is no journal in normal use). + +## Every commit classified (oldest first) + +Areas come from each subject's keywords and the text is its first clause; the upstream commit has the full message. + +| Commit | Area | Upstream change | +|---|---|---| +| [26062f9e](https://github.com/seedds/nxnative/commit/26062f9e) | Shader / pipeline | A read of a game uniform slot past the size the game bound gives 0, as Maxwell's constant buffers do | +| [c66a36ab](https://github.com/seedds/nxnative/commit/c66a36ab) | Input / display / audio | nn::ndve's movie network runs as the host's own Metal kernels, on by default | +| [2a6f9b32](https://github.com/seedds/nxnative/commit/2a6f9b32) | Docs / tests / CI | t_render_counter_overlap prints the bytes a GPU copy reads after a report as OPEN lines instead of checking them | +| [e121931f](https://github.com/seedds/nxnative/commit/e121931f) | Host / runtime | Compute shared memory past 32 KB takes a slice for every workgroup of the grid | +| [9983d659](https://github.com/seedds/nxnative/commit/9983d659) | Host / runtime | iOS builds link again | +| [c06d1e79](https://github.com/seedds/nxnative/commit/c06d1e79) | Host / runtime | A GPU-written span's sliver inside a texture does not make it the GPU's | +| [1d377206](https://github.com/seedds/nxnative/commit/1d377206) | Host / runtime | A pool buffer retired between submissions is released without waiting for another one | +| [843081d0](https://github.com/seedds/nxnative/commit/843081d0) | Game profiles | GetCommandMemorySize is the bytes the game last passed to AddCommandMemory, as in NVN, stored and read in one load | +| [b001c49b](https://github.com/seedds/nxnative/commit/b001c49b) | Host / runtime | Textures of a resolution family are created uncompressed | +| [a2cfd407](https://github.com/seedds/nxnative/commit/a2cfd407) | Input / display / audio | Fullscreen on a ProMotion screen holds 60 fps | +| [caa4a8a0](https://github.com/seedds/nxnative/commit/caa4a8a0) | Host / runtime | A pool buffer's protected pages are opened, not counted as written | +| [0e6c128e](https://github.com/seedds/nxnative/commit/0e6c128e) | Host / runtime | The graphics tests' guest clock is the Mach clock, as the host's with no pause | +| [356a7132](https://github.com/seedds/nxnative/commit/356a7132) | Shader / pipeline | HMMA with an f16 accumulator rounds once per 8 products, as Ampere does, instead of after every product | +| [c68706f9](https://github.com/seedds/nxnative/commit/c68706f9) | Build / extraction | A linear texture no view can take is uploaded from its storage while the GPU has not written there, whatever its IMAGE or LINEAR_RENDER_TARGET flag | +| [68869cca](https://github.com/seedds/nxnative/commit/68869cca) | Input / display / audio | A presented texture is resolved on the submit thread, and the present thread never uploads | +| [8d5d4f1b](https://github.com/seedds/nxnative/commit/8d5d4f1b) | Host / runtime | Guest files give the disk back what the game gives back, and stop on the room left after dirty pages (2 GB, counting purgeable space) | +| [81906931](https://github.com/seedds/nxnative/commit/81906931) | Game profiles | The depth attachment sampled in its own pass follows the colour rule | +| [04811a63](https://github.com/seedds/nxnative/commit/04811a63) | Docs / tests / CI | Pool marks test their bit with a plain load | +| [0c8a3ac0](https://github.com/seedds/nxnative/commit/0c8a3ac0) | Shader / pipeline | textureGatherOffsets whose four offsets form a 2x2 square is one gather and a swizzle instead of four gathers | +| [797ca451](https://github.com/seedds/nxnative/commit/797ca451) | Shader / pipeline | Compute threads that return before a barrier count as arrived on an Apple M6, as on NVIDIA's GPUs | +| [b54968e7](https://github.com/seedds/nxnative/commit/b54968e7) | Host / runtime | A snapshot of an attachment lands a clear still waiting for the draw's pass before it copies | +| [dbf84998](https://github.com/seedds/nxnative/commit/dbf84998) | Shader / pipeline | Ocarina of Time's prewarm list gets 41 more task / mesh pipelines from later play | +| [013c8755](https://github.com/seedds/nxnative/commit/013c8755) | Shader / pipeline | An HMMA whose B repeats takes it from a slot of matrices in the SIMD-group instead of the scratch round trip | +| [6f50de00](https://github.com/seedds/nxnative/commit/6f50de00) | Host / runtime | The pool walk's statistics no longer make or lengthen a chunk of a large pool for every texture they ask about | +| [a888e668](https://github.com/seedds/nxnative/commit/a888e668) | Game profiles | Switch 2 pixel shaders honour their early fragment tests flag, control byte +0x5b | +| [5d61592f](https://github.com/seedds/nxnative/commit/5d61592f) | Build / extraction | Splatoon Raiders' shader tables are split again with the current shader_split.py | +| [d4cc0eee](https://github.com/seedds/nxnative/commit/d4cc0eee) | Host / runtime | An sRGB texture in an image slot is loaded and stored raw, as on NVN: Star Fox's picture was encoded twice. NVN's image loads and stores convert no sRGB (as OpenGL's image units), while Metal decodes a read and encodes a write through an sRGB format, and th… | +| [b84ee29a](https://github.com/seedds/nxnative/commit/b84ee29a) | Game profiles | Hades II renders at up to 3840x2160 | +| [804809c6](https://github.com/seedds/nxnative/commit/804809c6) | Build / extraction | Hades II registers its update 1.0.141667 (v655360, SDK 20.5.15) | +| [12a44e09](https://github.com/seedds/nxnative/commit/12a44e09) | Shader / pipeline | The uniform copy thread hands off without a mutex, and a shadow um_overlay writes into is copied on the submit thread | +| [5e1c1aec](https://github.com/seedds/nxnative/commit/5e1c1aec) | Shader / pipeline | Whether a vertex shader writes the layer is in its reflection (layer_out), and the host and pso_offline.py read nothing else for it | +| [bbea4c90](https://github.com/seedds/nxnative/commit/bbea4c90) | Host / runtime | A GPU copy into a texture uploaded from its storage no longer uploads all of it again | +| [6d9edb07](https://github.com/seedds/nxnative/commit/6d9edb07) | Shader / pipeline | A texture ID a shader has read keeps its read mark when the game puts another texture into it, and loses it only when emptied | +| [6c676444](https://github.com/seedds/nxnative/commit/6c676444) | Host / runtime | A pool texture the walk leaves out holds a stand-in of its own colours until its first read | +| [3ade2f2e](https://github.com/seedds/nxnative/commit/3ade2f2e) | Shader / pipeline | Keep the HMMA parenthesis counter separate from the depth function | +| [01f33e31](https://github.com/seedds/nxnative/commit/01f33e31) | Game profiles | Hades II's render size is found as the one caller of nn::vi::CreateLayer, not by per-build fingerprints | +| [47773ed1](https://github.com/seedds/nxnative/commit/47773ed1) | Shader / pipeline | Ocarina of Time's prewarm list gets the pipelines of first looks at new places and of an iPad's own lists | +| [6c3aeb37](https://github.com/seedds/nxnative/commit/6c3aeb37) | Host / runtime | What the GPU writes into a GPU texture in storage the CPU can read is written into that storage on the GPU, in the same submission | +| [30c9b163](https://github.com/seedds/nxnative/commit/30c9b163) | Game profiles | nn::hid's Home button protection is answered by the host | +| [641636e2](https://github.com/seedds/nxnative/commit/641636e2) | Build / extraction | Building no longer needs .NET: the shader translator and nx_extract are committed as native universal executables (NativeAOT, arm64 and x86_64), with output byte-identical to the .NET builds | +| [976fbd57](https://github.com/seedds/nxnative/commit/976fbd57) | Game profiles | Red Dead Redemption (01007820196A6000) plays on the Mac | +| [80fc2ecb](https://github.com/seedds/nxnative/commit/80fc2ecb) | Shader / pipeline | The shader build's MSL rewrites, reflection and entry points are modules of their own that tests import, with the same output | +| [af33939c](https://github.com/seedds/nxnative/commit/af33939c) | Game profiles | Red Dead Redemption runs at 60 fps | +| [3bbe2094](https://github.com/seedds/nxnative/commit/3bbe2094) | Game profiles | TUNIC plays on the Mac at 60 fps (1.0.3, Unity 2020.3.36f1 IL2CPP, from the provisional profile with fps_target 60) | +| [c4ffb9df](https://github.com/seedds/nxnative/commit/c4ffb9df) | Host / runtime | The Mac host builds at -O2, as the other native code here | +| [629dd752](https://github.com/seedds/nxnative/commit/629dd752) | Game profiles | Ori and the Blind Forest | +| [f02d0474](https://github.com/seedds/nxnative/commit/f02d0474) | Host / runtime | NVN recordings take no more command memory than the driver's commands | +| [ab41d6a1](https://github.com/seedds/nxnative/commit/ab41d6a1) | Host / runtime | What the GPU writes into a texture uploaded from its storage is written into that storage on the GPU, as for GPU textures | +| [25c2546e](https://github.com/seedds/nxnative/commit/25c2546e) | Host / runtime | An NVN event signal no longer ends its Metal command buffer, and with it the open render pass | +| [fd8188c9](https://github.com/seedds/nxnative/commit/fd8188c9) | Host / runtime | The submissions of one frame share a command allocator, and a submission takes a visibility buffer only when it counts samples | +| [156de5a1](https://github.com/seedds/nxnative/commit/156de5a1) | Game profiles | Ori and the Will of the Wisps plays on the Mac at 60 fps (1.2.1, Unity 2018.4.24f1 IL2CPP, SDK 9.4.0, from the provisional profile with depth_mode zero) | +| [918e9f34](https://github.com/seedds/nxnative/commit/918e9f34) | Shader / pipeline | Bounded uniform reads are written as select(0, read, cond), the translator's own load and choice, not a branch for each | +| [e45694b7](https://github.com/seedds/nxnative/commit/e45694b7) | Shader / pipeline | A uniform read at a fixed offset is bounded once a binding, by the host, instead of at every read in the shader | +| [73354368](https://github.com/seedds/nxnative/commit/73354368) | Host / runtime | DLSS scalers outlive the submissions that encoded them | +| [adaff735](https://github.com/seedds/nxnative/commit/adaff735) | Shader / pipeline | An argument block whose only change is what its uniform slots point at is patched instead of encoded again | +| [f3ec8fc8](https://github.com/seedds/nxnative/commit/f3ec8fc8) | Host / runtime | A system register access whose register is x18 goes through the thread slot K18 | +| [fec8e1f2](https://github.com/seedds/nxnative/commit/fec8e1f2) | Host / runtime | Short sleeps are polls | +| [2f131edc](https://github.com/seedds/nxnative/commit/2f131edc) | Shader / pipeline | A pipeline base the archives lack is remembered for the run | +| [3304d6a0](https://github.com/seedds/nxnative/commit/3304d6a0) | Game profiles | Timestamp reports record their segment's GPU time for every game, without the GPU's idle time between a submission's command buffers | +| [4c574a6f](https://github.com/seedds/nxnative/commit/4c574a6f) | Game profiles | A counter report of a running submission refuses a uniform shadow only over its own 16 bytes, not its whole 4 KB page | +| [a77c0ee8](https://github.com/seedds/nxnative/commit/a77c0ee8) | Game profiles | The gfx tests build again | +| [01de04fd](https://github.com/seedds/nxnative/commit/01de04fd) | Host / runtime | New multisample colour textures start as zero | +| [ced36121](https://github.com/seedds/nxnative/commit/ced36121) | Host / runtime | Vulkan layer | +| [0662f9d8](https://github.com/seedds/nxnative/commit/0662f9d8) | Cloud saves | A NaN or infinite colour under a zero source blend weight adds nothing, as on the console | +| [0cad9500](https://github.com/seedds/nxnative/commit/0cad9500) | Input / display / audio | With present snapshots on, a swapchain texture is the game's again only once its snapshot's copy has run on the GPU: a snapshot present keeps its ticket and the queue's receipt carries the copy (mtl_present's copy), so the acquire of that texture waits for… | +| [0d84a3f7](https://github.com/seedds/nxnative/commit/0d84a3f7) | Game profiles | A console mode change at run time signals the style update events of the npads it moves | +| [f1701b98](https://github.com/seedds/nxnative/commit/f1701b98) | Host / runtime | Guest files | +| [380ff71a](https://github.com/seedds/nxnative/commit/380ff71a) | Build / extraction | The committed shader translator starts on macOS 26 again | +| [c823c3c0](https://github.com/seedds/nxnative/commit/c823c3c0) | Shader / pipeline | test_dmsh_pool_arrays passes reflect() the translator's cbufs list | +| [f20f6507](https://github.com/seedds/nxnative/commit/f20f6507) | Input / display / audio | Mac | +| [1d090a66](https://github.com/seedds/nxnative/commit/1d090a66) | Shader / pipeline | A uniform slot is bounded at its bound size rounded up to 256 bytes, the size the console's driver gives the GPU, not at the exact size | +| [6a6a018d](https://github.com/seedds/nxnative/commit/6a6a018d) | Shader / pipeline | The prediction makes no pulling stand-in for a proposed pipeline key whose base is made | +| [81b21cb8](https://github.com/seedds/nxnative/commit/81b21cb8) | Docs / tests / CI | The CPU conversion of vertex attributes takes ranges up to 64 MiB and keeps a conversion while its source is unchanged | +| [430ace12](https://github.com/seedds/nxnative/commit/430ace12) | Game profiles | Metroid Prime 4 | +| [707cac95](https://github.com/seedds/nxnative/commit/707cac95) | Game profiles | Textures that dynamic resolution resizes take their memory from the recycled heaps instead of new Metal memory each time | +| [511a0a2c](https://github.com/seedds/nxnative/commit/511a0a2c) | Shader / pipeline | Unused border corrections are compiled out of pipelines that bind no sampler with a twin | +| [01ec314e](https://github.com/seedds/nxnative/commit/01ec314e) | Input / display / audio | Touch screen states are numbered from one count in docked and handheld mode, so a game started docked takes touches after leaving the dock | +| [7ef83394](https://github.com/seedds/nxnative/commit/7ef83394) | Input / display / audio | Switch 2 controllers with Press a button to join | +| [3553400d](https://github.com/seedds/nxnative/commit/3553400d) | Build / extraction | An Ampere BRX finds its c1 jump table when the block that loads it was split later | +| [5714e328](https://github.com/seedds/nxnative/commit/5714e328) | Build / extraction | The NSP script takes a fallback app icon | +| [477b4f85](https://github.com/seedds/nxnative/commit/477b4f85) | Input / display / audio | A DualSense keeps its left and right handle haptics when its server offers no advanced players, instead of one engine for both | +| [1e69c180](https://github.com/seedds/nxnative/commit/1e69c180) | Input / display / audio | A write to a host page two pools share is no longer missed while a pool buffer is made | +| [2d3fb83b](https://github.com/seedds/nxnative/commit/2d3fb83b) | Game profiles | A log line from a hook is written on the thread's host stack, and the controller applet's stand-in runs there | +| [3e940796](https://github.com/seedds/nxnative/commit/3e940796) | Input / display / audio | The graphics layer's log lines are written on the host stack as the host's are | +| [084f5aa5](https://github.com/seedds/nxnative/commit/084f5aa5) | Docs / tests / CI | The graphics scale is tier 0 in both modes, so Switch 2 games draw their full docked picture | +| [8fb5ffeb](https://github.com/seedds/nxnative/commit/8fb5ffeb) | Game profiles | Port The Binding of Isaac | +| [352e4a6e](https://github.com/seedds/nxnative/commit/352e4a6e) | Input / display / audio | A storage takeover finds its candidates by pool cell instead of walking the sharing table, so a scene that shares tens of thousands of textures loads | +| [f75ac06e](https://github.com/seedds/nxnative/commit/f75ac06e) | Input / display / audio | A game's Mac shaders are compiled for the Mac host's minimum macOS, so one chain version gives the same result on every Mac | +| [8b55ebb3](https://github.com/seedds/nxnative/commit/8b55ebb3) | Build / extraction | The shader build finishes its shaders in worker processes, so a cold build uses the cores it has | +| [47343a42](https://github.com/seedds/nxnative/commit/47343a42) | Input / display / audio | The controller applet leaves out the players past the game's maximum, so a one-player game with two players no longer opens it in a loop | +| [d2556cff](https://github.com/seedds/nxnative/commit/d2556cff) | Game profiles | A re-upload of a texture taken from guest memory reads only the images written, and a submission looks at a texture's pages once | +| [e2df31ca](https://github.com/seedds/nxnative/commit/e2df31ca) | Input / display / audio | A serial entry that holds a view holds the textures it is a view of, so a storage takeover leaves a live sRGB / linear twin's parent | +| [68dde14b](https://github.com/seedds/nxnative/commit/68dde14b) | Input / display / audio | A HID entry counts as a pad only while it sends reports, so one Pro Controller with a second, silent I/O Registry entry stays one player | +| [07d6ea46](https://github.com/seedds/nxnative/commit/07d6ea46) | Cloud saves | Cloud saves | +| [47607fe0](https://github.com/seedds/nxnative/commit/47607fe0) | Game profiles | A packaged texture's layout keeps its width alignment, so a texture whose width is not a whole number of tiles is padded as the game laid it out | +| [9d607760](https://github.com/seedds/nxnative/commit/9d607760) | Game profiles | WriteTexels writes a linear texture by rows of its stride, so a native-texture movie into a LINEAR texture is no longer black | +| [4ce1449f](https://github.com/seedds/nxnative/commit/4ce1449f) | Game profiles | oe::RequestToRelaunchApplication ends the application and starts it again from its first program, so Super Smash Bros. Ultimate no longer crashes after a language change | +| [06c2ea4c](https://github.com/seedds/nxnative/commit/06c2ea4c) | Cloud saves | A cloud-save pull swaps each of the cloud's folders in from this device's backup folder, so the staging folder never holds this device's save and a failed pull puts it back whole | +| [4b616bb5](https://github.com/seedds/nxnative/commit/4b616bb5) | Build / extraction | Automated host runs keep cloud saves off, so a regression, benchmark or NSP build run no longer syncs its scratch save with the player's cloud save | +| [35a5aa2b](https://github.com/seedds/nxnative/commit/35a5aa2b) | Docs / tests / CI | A JIT plugin loads again from its data as rewritten and a zero bss, so a second CreateJitEnvironment after DestroyJitEnvironment no longer crashes | +| [7c384e73](https://github.com/seedds/nxnative/commit/7c384e73) | Input / display / audio | An OpenGL shader deleted while attached is only flagged and goes with its last attachment, and the current program deleted stays in use until another is made current, so a link after glDeleteShader no longer finds no shader or another one | +| [b149a1e4](https://github.com/seedds/nxnative/commit/b149a1e4) | Docs / tests / CI | A Vulkan descriptor copied into a binding with immutable samplers takes the source's image and keeps the destination's sampler, so a copy no longer replaces the sampler the layout fixed | +| [0b124486](https://github.com/seedds/nxnative/commit/0b124486) | Build / extraction | A tool the NSP script or regress.py runs leads a process group of its own, stopped whole on an interrupt, a SIGTERM or an error, so stopping a build no longer leaves the tool's descendants running | +| [ccd834e0](https://github.com/seedds/nxnative/commit/ccd834e0) | Game profiles | Port PRAGMATA (0400C76024710000, Switch 2, RE Engine) | +| [3bf15409](https://github.com/seedds/nxnative/commit/3bf15409) | Build / extraction | Diablo III: Eternal Collection (01001B300B9BE000, update 2.7.7.92380 = v1441792, SDK 8.2.0, Blizzard's engine, NVN) plays on the Mac, and the shader split reads Diablo III's console packages, so its 1,324 shaders are found | +| [5f6f1622](https://github.com/seedds/nxnative/commit/5f6f1622) | Build / extraction | The NSP script replaces an output by renaming the old one aside before the new one goes in, so a stop or a failure while it replaces one no longer leaves that output half deleted | +| [78671525](https://github.com/seedds/nxnative/commit/78671525) | Shader / pipeline | Fix the shader entry-point lint failures | +| [d2ceb8fa](https://github.com/seedds/nxnative/commit/d2ceb8fa) | Shader / pipeline | The Mac compile of a game's shaders, MAC_METAL, is defined in dmsh_chain.py, which imports without running a build, so tests compile with the build's own command instead of evaluating a line of dmsh_build.py or copying its flags | +| [b21eb3fb](https://github.com/seedds/nxnative/commit/b21eb3fb) | Host / runtime | iOS app builds link again | +| [6da6900e](https://github.com/seedds/nxnative/commit/6da6900e) | Build / extraction | Diablo II: Resurrected (0100726014352000, update 1.0.34.0 = v2228224, SDK 11.4.3, Blizzard's D2R engine, NVN) plays on the Mac, and the shader split reads Blizzard's CASC storage, so its 6,931 shaders are found | +| [c0ddade8](https://github.com/seedds/nxnative/commit/c0ddade8) | Docs / tests / CI | Two switches the docs name but no code reads are marked as never on main, and DM_NVN_VCONV_KEEP's row says what =1 gives | +| [cd28a4c2](https://github.com/seedds/nxnative/commit/cd28a4c2) | Game profiles | test_dmsh_meshstage's GPU case writes the primitive id at the guest's address again | +| [c761e6a7](https://github.com/seedds/nxnative/commit/c761e6a7) | Game profiles | The offline pipeline archive specializes a function that is not its library's first from a library of its own, so a near = 0 game whose vertex shaders declare the border corrections' constant gets an archive again | +| [bcc4483d](https://github.com/seedds/nxnative/commit/bcc4483d) | Build / extraction | A forked shader worker no longer opens the compile cache, so the Mac shader build no longer stops with BrokenProcessPool where Python uses Apple's SQLite | +| [224ecb18](https://github.com/seedds/nxnative/commit/224ecb18) | Game profiles | An Ampere IPA at an offset reads the centre value plus its screen derivatives times the offset, and a[0x3a8] / a[0x3b4] are the barycentric coordinates, so PRAGMATA's last failing shader builds | +| [dfc5a15f](https://github.com/seedds/nxnative/commit/dfc5a15f) | Game profiles | A game uniform word tested against integer constants more than once is read by a volatile load per test, so Metal's front end folds no chain of those tests into a switch, which Apple's GPU compiler took minutes to hours and gigabytes over (PR #2372) | +| [e7750541](https://github.com/seedds/nxnative/commit/e7750541) | Game profiles | draw_barriers game works again while images share memory, and a fragment-only game barrier no longer forces the full one | +| [be23843f](https://github.com/seedds/nxnative/commit/be23843f) | Docs / tests / CI | The C and Objective-C analyzer ratchet passes again (lint_c.py tidy, LLVM 23.1.4 | +| [c24ab142](https://github.com/seedds/nxnative/commit/c24ab142) | Game profiles | pso_offline.py leaves out the pipelines metal-tt cannot compile instead of losing the whole archive to one of them | +| [1e3fd031](https://github.com/seedds/nxnative/commit/1e3fd031) | Game profiles | Three late frames in a row raise the frame step's hold only when the frames at that hold also fall short of it on average, and a frame-step stub declares the largest step it hands the game, so Mario Kart World no longer drops to about 40 fps on an M4 Max an… | +| [6815810d](https://github.com/seedds/nxnative/commit/6815810d) | Game profiles | Mario Kart World 1.8.0 (v917504) gets the host's frame step | +| [1ee02525](https://github.com/seedds/nxnative/commit/1ee02525) | Docs / tests / CI | The NVN serial tables move out of the replay layer into an owner of their own, dm/gfx/nvn/nvn_owner.m, which nvn_mtl.m changes only through its functions, and which a test now runs without nvn_mtl.m | +| [2ba06d76](https://github.com/seedds/nxnative/commit/2ba06d76) | Game profiles | A stale sRGB / linear twin view no longer stays reachable in the NVN serial tables | +| [09a91c5c](https://github.com/seedds/nxnative/commit/09a91c5c) | Input / display / audio | Each nn::codec video decoder decodes on a thread of its own, and dav1d on 4 threads with 3 frames at once, so movies no longer stutter at a steady 60 fps (PR #2240) | +| [96259d45](https://github.com/seedds/nxnative/commit/96259d45) | Game profiles | The NVN sharing table (textures shared by storage | +| [ad808671](https://github.com/seedds/nxnative/commit/ad808671) | Input / display / audio | Retirement's eligibility and the serial tables' change generation move into the NVN resource owner, dm/gfx/nvn/nvn_owner.m, which now holds serials, ghosts, pins, view parents, the sharing table, the generation and who may leave the residency set | +| [591d605d](https://github.com/seedds/nxnative/commit/591d605d) | Build / extraction | A dump tool's .listing files are left out of a RomFS image, a NUP's RomFS and the survey's file list, so Nintendo Switch Sports Resort built from an FTP dump no longer crashes at main+0x9d7c60 | +| [835aac11](https://github.com/seedds/nxnative/commit/835aac11) | Game profiles | File calls on a mount only the SDK holds (a RAM disk) go to the SDK's own calls, so Nintendo Switch Sports Resort no longer aborts at boot on its VoiceText RAM disk (PR #2376) | +| [953d15f4](https://github.com/seedds/nxnative/commit/953d15f4) | Cloud saves | A read-only mount of the game's own save data mounts its committed save, so Nintendo Switch Sports Resort no longer aborts about 48 s into every start after its first save (PR #2377) | +| [037b869d](https://github.com/seedds/nxnative/commit/037b869d) | Game profiles | The host answers the virtual output device's properties, the OutputStream open without flags and the release of the output device notification, so Nintendo Switch Sports Resort no longer aborts in GetVirtualOutputDeviceProperties (PR #2380) | +| [247f0265](https://github.com/seedds/nxnative/commit/247f0265) | Game profiles | A whole controller is shown as a Joy-Con pair when the game supports JoyDual but not full-key, and the Joy-Con icon decoration colour is answered, so Nintendo Switch Sports Resort no longer opens the controller applet every frame or aborts as it draws (PR #… | +| [167a6dc5](https://github.com/seedds/nxnative/commit/167a6dc5) | Game profiles | The Switch 2 hid IMU calls the SDK's sensor fusion reads are served, with the acceleration first in each sample and a live controller's motion interpolated, so Nintendo Switch Sports Resort boots past InitializeSensorFusion, passes its motion calibration an… | +| [b581efc5](https://github.com/seedds/nxnative/commit/b581efc5) | Build / extraction | The fragment's shading-rate size (system register 0x84) reads 1 x 1, so Nintendo Switch Sports Resort's sea is no longer black and its avatars' hair and clothes are no longer flat black (PR #2103) | +| [14a4c273](https://github.com/seedds/nxnative/commit/14a4c273) | Game profiles | A colour render-target view of another format with the same texel size upgrades its texture and is attached, so a uint draw into an R32_UI view of an RGB9E5 target stores its bits and Nintendo Switch Sports Resort's upscaled picture is no longer solid red (… | +| [f4b7e77c](https://github.com/seedds/nxnative/commit/f4b7e77c) | Build / extraction | Nintendo Switch Sports Resort (04008E001F098000, Switch 2, v65536) gets its profile, note, games table row and README line | +| [c9d57d2e](https://github.com/seedds/nxnative/commit/c9d57d2e) | Build / extraction | Isolate and bound per-game regression shader caches | +| [84d36c63](https://github.com/seedds/nxnative/commit/84d36c63) | Build / extraction | Closing the terminal stops a build's or a regression run's tool as SIGTERM does | +| [e6048dea](https://github.com/seedds/nxnative/commit/e6048dea) | Build / extraction | A pull of the cloud's save is recoverable from wherever it stopped, an unsafe save is not mounted, and automated runs share one cloud-save policy. Pull (dm/host/save_sync.c) | +| [ed323824](https://github.com/seedds/nxnative/commit/ed323824) | Game profiles | CI's tests and lint-c jobs pass again on main | +| [3f10d4ff](https://github.com/seedds/nxnative/commit/3f10d4ff) | Build / extraction | CI's lint-python job passes again on main | +| [a2514d7d](https://github.com/seedds/nxnative/commit/a2514d7d) | Game profiles | The host evaluates NVN conditional rendering for every game, and the profile key render_cond is gone | +| [34608c72](https://github.com/seedds/nxnative/commit/34608c72) | Game profiles | NVN bind ranges are checked at recording, GL depth textures keep the data glTexImage2D gave them, and a save commit names files still open for writing | +| [ddc063b6](https://github.com/seedds/nxnative/commit/ddc063b6) | Game profiles | A JIT plugin loaded alone stays out of the game's linking and ends through the loader | +| [73302614](https://github.com/seedds/nxnative/commit/73302614) | Build / extraction | The NSP script's swap_in keeps the old output recoverable in the three cases it lost it, each reproduced against the script before | +| [0a764c09](https://github.com/seedds/nxnative/commit/0a764c09) | Cloud saves | The launchers' cloud-save report says when a pull of the cloud's save did not finish | +| [652f8dc0](https://github.com/seedds/nxnative/commit/652f8dc0) | Input / display / audio | The OpenGL and Vulkan fixes of the review get the tests the step 15 audit found missing, and a second shader of a GL stage says so. t_gl_imports links a program for real (shaders given their table entries, as a compile from the library leaves them) | +| [fe082730](https://github.com/seedds/nxnative/commit/fe082730) | Shader / pipeline | t_pso_skey's four remaining byte comparisons of a pso_skey carry the same justified suppression as its other four, and its baseline entry goes | +| [8bfa6b5e](https://github.com/seedds/nxnative/commit/8bfa6b5e) | Docs / tests / CI | Retirement is tested across threads, as the host runs it | +| [5582c54a](https://github.com/seedds/nxnative/commit/5582c54a) | Input / display / audio | A screenshot of a window the game crops is the picture as presented | +| [0f19bdf7](https://github.com/seedds/nxnative/commit/0f19bdf7) | Build / extraction | The shader chain's configuration imports without reading anything, and the iPhone's deployment target has one place | +| [637f3fa7](https://github.com/seedds/nxnative/commit/637f3fa7) | Input / display / audio | A movie decoder keeps input buffers sent after end of stream instead of decoding them, as ACodec does (PR #1938) | +| [8a5c5e7e](https://github.com/seedds/nxnative/commit/8a5c5e7e) | Shader / pipeline | The OpenGL layer keeps GL's error flag | diff --git a/docs/upstream-upgrades/README.md b/docs/upstream-upgrades/README.md index 7c8d74e..1d65abd 100644 --- a/docs/upstream-upgrades/README.md +++ b/docs/upstream-upgrades/README.md @@ -80,3 +80,4 @@ This directory records the review that must happen before the pin changes, plus ## Maintainer-directed adoption - [2026-11-09 — bd979ced → 79741184](2026-11-09-bd979ced-79741184-review.md) (0.1.4.2: 71 commits; adopted at the maintainer's explicit request after automated review/gate checks; exact-hash real game build/launch remains unconfirmed). +- [2026-11-12 — 79741184 → 8a5c5e7e](2026-11-12-79741184-8a5c5e7e-review.md) (0.1.4.3: 152 commits; adopted at the maintainer's explicit request; builds need no .NET; build stop and package probe adapted; real build/launch awaiting the maintainer) diff --git a/scripts/build_app.sh b/scripts/build_app.sh index 489aa9f..43e60c9 100755 --- a/scripts/build_app.sh +++ b/scripts/build_app.sh @@ -43,6 +43,9 @@ APP="$STAGE/NXLibrary.app" mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" cp "$BIN" "$APP/Contents/MacOS/NXLibrary" +LOCALIZATION_BUNDLE="$ROOT/.build/$CONFIG/NXLibrary_NXLibraryCore.bundle" +[ -d "$LOCALIZATION_BUNDLE" ] || { echo "error: compiled localization bundle missing" >&2; exit 1; } +cp -R "$LOCALIZATION_BUNDLE" "$APP/Contents/Resources/" if [ -f "$ROOT/Resources/AppIcon.icns" ]; then cp "$ROOT/Resources/AppIcon.icns" "$APP/Contents/Resources/AppIcon.icns" @@ -69,6 +72,9 @@ cat > "$APP/Contents/Info.plist" <<PLIST <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key><string>en</string> + <key>CFBundleLocalizations</key><array> + <string>en</string><string>it</string><string>fr</string><string>de</string><string>es</string><string>pt-BR</string> + </array> <key>CFBundleDisplayName</key><string>$NAME</string> <key>CFBundleExecutable</key><string>NXLibrary</string> <key>CFBundleIconFile</key><string>AppIcon</string> @@ -90,6 +96,7 @@ $RELEASE_KEY </plist> PLIST +python3 "$ROOT/scripts/check_localizations.py" --app "$APP" codesign --force --deep --sign - "$APP" codesign --verify --deep --strict "$APP" swiftc -I "$ROOT/.build/$CONFIG" "$ROOT/scripts/install_built_app.swift" \ diff --git a/scripts/check_localizations.py b/scripts/check_localizations.py new file mode 100644 index 0000000..f2bcf21 --- /dev/null +++ b/scripts/check_localizations.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Validate translated copy and the localization resources shipped in a built app.""" +import argparse +from collections import Counter +import json +from pathlib import Path +import plistlib +import re +from xml.parsers.expat import ExpatError + +LANGUAGES = ('en', 'it', 'fr', 'de', 'es', 'pt-BR') +BUNDLE = 'NXLibrary_NXLibraryCore.bundle' +PLACEHOLDER = re.compile(r'%(?:(\d+)\$)?(lld|ld|d|@|(?:\.\d+)?f)') + + +def placeholders(text): + matches = PLACEHOLDER.findall(text.replace('%%', '')) + return Counter((int(position) if position else index + 1, kind) for index, (position, kind) in enumerate(matches)) + + +def check_catalog(path): + errors = [] + try: + catalog = json.loads(path.read_text()) + except (OSError, ValueError) as error: + return [f'Cannot read catalog: {error}'] + if catalog.get('sourceLanguage') != 'en': + errors.append('Catalog source language must be en') + for key, item in catalog.get('strings', {}).items(): + for language in LANGUAGES: + entry = item.get('localizations', {}).get(language) + if entry is None: + errors.append(f'{language}: missing translation for {key!r}') + continue + variants = entry.get('variations', {}).get('plural') + units = [item.get('stringUnit', {}) for item in variants.values()] if variants else [entry.get('stringUnit', {})] + if variants and not {'one', 'other'}.issubset(variants): + errors.append(f'{language}: incomplete plurals for {key!r}') + for unit in units: + value = unit.get('value', '') + if not value.strip() or unit.get('state') != 'translated': + errors.append(f'{language}: unfinished translation for {key!r}') + if placeholders(value) != placeholders(key): + errors.append(f'{language}: placeholder mismatch for {key!r}') + return errors + + +def read_compiled_plist(path): + data = path.read_bytes() + # Xcode emits UTF-16 XML strings with an UTF-8 declaration. Foundation accepts this; + # normalize the BOM encoding before handing the XML to Python's stricter parser. + if data.startswith((b'\xff\xfe', b'\xfe\xff')): + text = data.decode('utf-16') + text = re.sub(r'encoding="[^"]+"', 'encoding="UTF-8"', text, count=1) + data = text.encode('utf-8') + return plistlib.loads(data) + + +def check_app(app): + errors = [] + try: + info = plistlib.loads((app / 'Contents/Info.plist').read_bytes()) + except (OSError, ValueError, plistlib.InvalidFileException, ExpatError, UnicodeError): + return ['App Info.plist missing or invalid'] + if not set(LANGUAGES).issubset(info.get('CFBundleLocalizations', [])): + errors.append('App Info.plist does not declare all six languages') + resources = app / 'Contents/Resources' / BUNDLE / 'Contents/Resources' + if not resources.is_dir(): + return errors + ['Compiled localization bundle missing'] + for language in LANGUAGES: + folder = resources / f'{language}.lproj' + for name in ('Localizable.strings', 'Localizable.stringsdict'): + try: + value = read_compiled_plist(folder / name) + if not value: + errors.append(f'{language}: empty {name}') + except (OSError, ValueError, plistlib.InvalidFileException, ExpatError, UnicodeError): + errors.append(f'{language}: missing or invalid compiled {name}') + return errors + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--catalog', type=Path, default=Path(__file__).resolve().parents[1] / 'Sources/NXLibraryCore/Resources/Localizable.xcstrings') + parser.add_argument('--app', type=Path) + args = parser.parse_args() + errors = check_catalog(args.catalog) + if args.app: + errors += check_app(args.app) + if errors: + for error in errors: + print(error) + return 1 + print('Localization checks passed (six languages, placeholders and plurals).') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/release.sh b/scripts/release.sh index 694722c..678bb68 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -84,6 +84,7 @@ ditto --norsrc -c -k --keepParent "$APP" "$ZIP" "$APP/Contents/Resources/Runtime/python/bin/python3" -I -B \ "$APP/Contents/Resources/validate_update.py" "$ZIP" "$OUT/.update-check" codesign --verify --deep --strict "$OUT/.update-check/NXLibrary.app" +python3 "$ROOT/scripts/check_localizations.py" --app "$OUT/.update-check/NXLibrary.app" rm -rf "$OUT/.update-check" echo "Zip: $ZIP" echo "SHA-256: $(shasum -a 256 "$ZIP" | cut -d' ' -f1)"