diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index 76e552029..f1ae540f7 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -27,17 +27,15 @@ body:
id: logs
attributes:
label: Logs
- description: Please provide the logs from the launched Wine process. These can be found by pressing `CMD + L` in Whisky.
+ description: Please provide the logs from the launched Wine process. These can be found by pressing `CMD + L` in Brandy.
render: bash
validations:
required: true
- - type: dropdown
- id: whisky-version
+ - type: input
+ id: brandy-version
attributes:
- label: What version of Whisky are you using?
- options:
- - "2.3.2"
- - "<2.3.2"
+ label: What version of Brandy are you using?
+ description: Shown in Brandy > About Brandy.
validations:
required: true
- type: dropdown
@@ -45,6 +43,8 @@ body:
attributes:
label: What version of macOS are you using?
options:
+ - "Tahoe (macOS 26)"
+ - "Sequoia (macOS 15)"
- "Sonoma (macOS 14)"
validations:
required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index bbbeb5e3f..3ba13e0ce 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,4 +1 @@
blank_issues_enabled: false
-- name: Discord Support
- url: https://discord.gg/K4AQukwAke
- about: Please ask for support first to confirm the issue before submitting a report.
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 70d749330..ad87c1ccd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,20 +1,20 @@
# How to contribute
-Thanks for your interest! First, make a fork of Whisky, make a new branch for your changes, and get coding!
+Thanks for your interest! First, make a fork of Brandy, make a new branch for your changes, and get coding!
# Build environment
-Whisky is built using Xcode 15 on macOS Sonoma. All external dependencies are handled through the Swift Package Manager.
+Brandy is built using Xcode 26 on macOS Sequoia or later. All external dependencies are handled through the Swift Package Manager.
# Code style
-Every Whisky commit is automatically linted using SwiftLint. You can run these checks locally simply by building in Xcode, violations will appear as errors or warnings. For your pull request to be merged, you must meet all the requirements outlined by SwiftLint and have no violations.
+Every Brandy commit is automatically linted using SwiftLint. You can run these checks locally simply by building in Xcode, violations will appear as errors or warnings. For your pull request to be merged, you must meet all the requirements outlined by SwiftLint and have no violations.
Generally, it is not advised to disable a SwiftLint rule, but there are certain situations where it is necessary. Please use your discretion when disabling rules temporarily.
SwiftLint does not fully check indentation, but we ask that you indent with 4-width spaces. This can be automatically configured in Xcode's settings.
-All added strings must be properly localised and added to the EN strings file. Do not add keys for other languages or translate within your PR. All translations should be handled on [Crowdin](https://crowdin.com/project/whisky).
+All added strings must be properly localised and added to the English strings in `Whisky/Localizable.xcstrings`. Do not add keys for other languages or translate within your PR.
# Making your PR
diff --git a/README.md b/README.md
index d2910cbf4..ce6dfed00 100644
--- a/README.md
+++ b/README.md
@@ -1,81 +1,48 @@
- # Whisky 🥃
- *Wine but a bit stronger*
-
- 
- [](https://discord.gg/CsqAfs9CnM)
-
-
-## Maintenance Notice
-
-[Whisky is no longer actively maintained](https://docs.getwhisky.app/maintenance-notice). Apps and games may break at any time.
+
-
+ # Brandy
-Familiar UI that integrates seamlessly with macOS
+ *Windows games on your Mac, set up in a few clicks*
-
-
-
- One-click bottle creation and management
+ 
-
-
-Debug and profile with ease
-
---
-Whisky provides a clean and easy to use graphical wrapper for Wine built in native SwiftUI. You can make and manage bottles, install and run Windows apps and games, and unlock the full potential of your Mac with no technical knowledge required. Whisky is built on top of CrossOver 22.1.1, and Apple's own `Game Porting Toolkit`.
+Brandy is a native SwiftUI app for running Windows games and apps on your Mac, made for people who don't want to learn how Wine works. Open it, click **Set Up Steam**, and Brandy prepares your Mac, downloads its Windows layer, creates a place for your games, and installs Steam from Valve. After that, it's one button: **Open Steam**.
-Translated on [Crowdin](https://crowdin.com/project/whisky).
+Everything that used to be required is still there for those who want it. Turn on **Show advanced options** in Settings to manage bottles, installed programs, Windows versions, DXVK, Winetricks and Wine's own tools.
----
+Brandy is a fork of [Whisky](https://github.com/Whisky-App/Whisky), which is no longer maintained. Brandy is distilled from wine, just as this app is distilled from Whisky.
## System Requirements
+
- CPU: Apple Silicon (M-series chips)
- OS: macOS Sonoma 14.0 or later
+- About 3 GB of free space for the Windows layer and Steam
-## Homebrew
-
-Whisky is on homebrew! Install with
-`brew install --cask whisky`.
-
-## My game isn't working!
+## What's Inside
-Some games need special steps to get working. Check out the [wiki](https://github.com/IsaacMarovitz/Whisky/wiki/Game-Support).
+- **Wine 11.0**, the official WineHQ macOS build by Gcenx
+- **DXVK-macOS** for Direct3D 10 and 11 through Vulkan, off by default
+- **Windows 10 or 11** for each bottle
----
+The Windows layer is published as the `whiskywine-3` release of this repository. `scripts/build-whiskywine.sh` rebuilds it from pinned, checksum-verified upstream releases.
## Credits & Acknowledgments
-Whisky is possible thanks to the magic of several projects:
+Brandy exists thanks to:
-- [msync](https://github.com/marzent/wine-msync) by marzent
-- [DXVK-macOS](https://github.com/Gcenx/DXVK-macOS) by Gcenx and doitsujin
+- [Whisky](https://github.com/Whisky-App/Whisky) by Isaac Marovitz and contributors, which Brandy is built on
+- [Wine](https://www.winehq.org) by the Wine project and CodeWeavers
+- [macOS Wine builds](https://github.com/Gcenx/macOS_Wine_builds) and [DXVK-macOS](https://github.com/Gcenx/DXVK-macOS) by Gcenx and doitsujin
- [MoltenVK](https://github.com/KhronosGroup/MoltenVK) by KhronosGroup
+- [Winetricks](https://github.com/Winetricks/winetricks)
- [Sparkle](https://github.com/sparkle-project/Sparkle) by sparkle-project
- [SemanticVersion](https://github.com/SwiftPackageIndex/SemanticVersion) by SwiftPackageIndex
- [swift-argument-parser](https://github.com/apple/swift-argument-parser) by Apple
-- [SwiftTextTable](https://github.com/scottrhoyt/SwiftyTextTable) by scottrhoyt
-- [CrossOver 22.1.1](https://www.codeweavers.com/crossover) by CodeWeavers and WineHQ
-- D3DMetal by Apple
-
-Special thanks to Gcenx, ohaiibuzzle, and Nat Brown for their support and contributions!
-
----
+- [SwiftyTextTable](https://github.com/scottrhoyt/SwiftyTextTable) by scottrhoyt
-
-
-
-
-
-
-
-
-
- Whisky doesn't exist without CrossOver. Support the work of CodeWeavers using our affiliate link .
-
-
-
+Brandy is free software under the [GNU General Public License v3](LICENSE).
diff --git a/Whisky.xcodeproj/project.pbxproj b/Whisky.xcodeproj/project.pbxproj
index bac6cee2a..37a67e0e3 100644
--- a/Whisky.xcodeproj/project.pbxproj
+++ b/Whisky.xcodeproj/project.pbxproj
@@ -10,7 +10,6 @@
6330DD962B1B0EA4007A625A /* RenameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6330DD952B1B0EA4007A625A /* RenameView.swift */; };
6365C4C12B1AA69D00AAE1FD /* Animation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6365C4C02B1AA69D00AAE1FD /* Animation+Extensions.swift */; };
6365C4C32B1AA8CD00AAE1FD /* BottleListEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6365C4C22B1AA8CD00AAE1FD /* BottleListEntry.swift */; };
- 63FFDE862ADF0C7700178665 /* BottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63FFDE852ADF0C7700178665 /* BottomBar.swift */; };
6763D8F62BC6314100651D27 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6763D8F52BC6314100651D27 /* Constants.swift */; };
67D278512BC5907E006F9A1E /* ActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67D278502BC5907E006F9A1E /* ActionView.swift */; };
6E064B1229DD32A200D9A2D2 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 6E064B1129DD32A200D9A2D2 /* Sparkle */; };
@@ -35,11 +34,6 @@
6E59676B2A9A424900D64F70 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 6E59676A2A9A424900D64F70 /* ArgumentParser */; };
6E5B61B62AC34E6D007B1913 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 6E5B61B52AC34E6D007B1913 /* SemanticVersion */; };
6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E621CEE2A5F631200C9AAB3 /* Winetricks.swift */; };
- 6E6C0CF22A419A6800356232 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6C0CF12A419A6800356232 /* WelcomeView.swift */; };
- 6E7A1F012F00000000000001 /* SetupFailureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7A1F012F00000000000002 /* SetupFailureView.swift */; };
- 6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6C0CF32A419A7600356232 /* RosettaView.swift */; };
- 6E6C0CF62A419A8300356232 /* WhiskyWineDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6C0CF52A419A8300356232 /* WhiskyWineDownloadView.swift */; };
- 6E6C0CF82A419A8C00356232 /* WhiskyWineInstallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E6C0CF72A419A8C00356232 /* WhiskyWineInstallView.swift */; };
6E70A49B2A9A2197007799E9 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E70A49A2A9A2197007799E9 /* Main.swift */; };
6E70A4A12A9A280C007799E9 /* WhiskyCmd.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E70A4A02A9A280C007799E9 /* WhiskyCmd.swift */; };
6E7C07BE2AAE7B0100F6E66B /* ProgramShortcut.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7C07BD2AAE7B0100F6E66B /* ProgramShortcut.swift */; };
@@ -48,7 +42,6 @@
6E95F6702AB3F33C00D585D1 /* SwiftyTextTable in Frameworks */ = {isa = PBXBuildFile; productRef = 6E95F66F2AB3F33C00D585D1 /* SwiftyTextTable */; };
6E95F6732AB3F67200D585D1 /* Progress in Frameworks */ = {isa = PBXBuildFile; productRef = 6E95F6722AB3F67200D585D1 /* Progress */; };
6EB58AC92AEACE5E00B1ED3F /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 6EB58AC82AEACE5E00B1ED3F /* Localizable.xcstrings */; };
- 6EF557982A410599001A4F09 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF557972A410599001A4F09 /* SetupView.swift */; };
6EFDF64C2AAE2DA800EF622F /* QuickLookThumbnailing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EFDF64B2AAE2DA800EF622F /* QuickLookThumbnailing.framework */; };
6EFDF64E2AAE2DA800EF622F /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EFDF64D2AAE2DA800EF622F /* Quartz.framework */; };
6EFDF6512AAE2DA800EF622F /* ThumbnailProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EFDF6502AAE2DA800EF622F /* ThumbnailProvider.swift */; };
@@ -59,10 +52,18 @@
8C73E1342AF472FC00B6FB45 /* ProgramMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C73E1332AF472FC00B6FB45 /* ProgramMenuView.swift */; };
8CB681E52AED7C6F0018D319 /* WhiskyKit in Resources */ = {isa = PBXBuildFile; fileRef = 8CB681E42AED7C6F0018D319 /* WhiskyKit */; };
8CB681E72AED7CD00018D319 /* WhiskyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8CB681E62AED7CD00018D319 /* WhiskyKit */; };
- AB0BCABD2B61036E00E21C31 /* RunningProcessesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0BCABC2B61036E00E21C31 /* RunningProcessesView.swift */; };
DB696FC82AFAE5DA0037EB2F /* PinCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */; };
EB58FB552A499896002DC184 /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = EB58FB542A499896002DC184 /* SemanticVersion */; };
EEA5A2462A31DD65008274AE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA5A2452A31DD65008274AE /* AppDelegate.swift */; };
+ 7AC35FF857263758125C7A31 /* ProgramIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3FC8B99010FD02742231AE /* ProgramIconView.swift */; };
+ 7A4C2D88025658F76A141654 /* Program+Display.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A93F021F55DAD2BC6722D54 /* Program+Display.swift */; };
+ 7ABF25C1A893C7716A1C83DB /* PinnedProgramsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A110F88A1199B4E9C0C8F3A /* PinnedProgramsGrid.swift */; };
+ 7A69955CE36D4C4767FE307A /* OnboardingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A30D4DC6FC022EF1446508D /* OnboardingModel.swift */; };
+ 7A3FA122329C4A8846CA0588 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7B1B81885C080827CC7B0F /* OnboardingView.swift */; };
+ 7A43CB6C325CAA981E94DDE7 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A53D33606FF388802121151 /* HomeView.swift */; };
+ 7A14B0D5D96E14098322DFC0 /* WhiskyWineUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3E5CD4F34E72EE880BF131 /* WhiskyWineUpdate.swift */; };
+ 7AA2C89FB1934624B6FFA190 /* SteamSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACB6C31E47142A637015EA4 /* SteamSession.swift */; };
+ 7A5E34BEDDA87936C50FB0A2 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5CAFC811A7E1E60978D49E /* ErrorAlert.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -114,7 +115,6 @@
6330DD952B1B0EA4007A625A /* RenameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameView.swift; sourceTree = ""; };
6365C4C02B1AA69D00AAE1FD /* Animation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Animation+Extensions.swift"; sourceTree = ""; };
6365C4C22B1AA8CD00AAE1FD /* BottleListEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleListEntry.swift; sourceTree = ""; };
- 63FFDE852ADF0C7700178665 /* BottomBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BottomBar.swift; sourceTree = ""; };
6763D8F52BC6314100651D27 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
67D278502BC5907E006F9A1E /* ActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionView.swift; sourceTree = ""; };
6E064B1329DD331F00D9A2D2 /* SparkleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SparkleView.swift; sourceTree = ""; };
@@ -138,11 +138,6 @@
6E50D98229CD6066008C39F6 /* BottleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleVM.swift; sourceTree = ""; };
6E50D98429CDF25B008C39F6 /* BottleCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottleCreationView.swift; sourceTree = ""; };
6E621CEE2A5F631200C9AAB3 /* Winetricks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Winetricks.swift; sourceTree = ""; };
- 6E6C0CF12A419A6800356232 /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; };
- 6E7A1F012F00000000000002 /* SetupFailureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupFailureView.swift; sourceTree = ""; };
- 6E6C0CF32A419A7600356232 /* RosettaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RosettaView.swift; sourceTree = ""; };
- 6E6C0CF52A419A8300356232 /* WhiskyWineDownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiskyWineDownloadView.swift; sourceTree = ""; };
- 6E6C0CF72A419A8C00356232 /* WhiskyWineInstallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiskyWineInstallView.swift; sourceTree = ""; };
6E70A4982A9A2197007799E9 /* WhiskyCmd */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = WhiskyCmd; sourceTree = BUILT_PRODUCTS_DIR; };
6E70A49A2A9A2197007799E9 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Main.swift; sourceTree = ""; };
6E70A4A02A9A280C007799E9 /* WhiskyCmd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiskyCmd.swift; sourceTree = ""; };
@@ -150,7 +145,6 @@
6E7C07BF2AAF570100F6E66B /* FileOpenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOpenView.swift; sourceTree = ""; };
6E899D1129CFC41D00A4A083 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
6EB58AC82AEACE5E00B1ED3F /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; };
- 6EF557972A410599001A4F09 /* SetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupView.swift; sourceTree = ""; };
6EFDF64A2AAE2DA800EF622F /* WhiskyThumbnail.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WhiskyThumbnail.appex; sourceTree = BUILT_PRODUCTS_DIR; };
6EFDF64B2AAE2DA800EF622F /* QuickLookThumbnailing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLookThumbnailing.framework; path = System/Library/Frameworks/QuickLookThumbnailing.framework; sourceTree = SDKROOT; };
6EFDF64D2AAE2DA800EF622F /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; };
@@ -160,9 +154,17 @@
6EFDF6652AAE303300EF622F /* Icons.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Icons.xcassets; sourceTree = ""; };
8C73E1332AF472FC00B6FB45 /* ProgramMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramMenuView.swift; sourceTree = ""; };
8CB681E42AED7C6F0018D319 /* WhiskyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = WhiskyKit; sourceTree = ""; };
- AB0BCABC2B61036E00E21C31 /* RunningProcessesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunningProcessesView.swift; sourceTree = ""; };
DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinCreationView.swift; sourceTree = ""; };
EEA5A2452A31DD65008274AE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7A3FC8B99010FD02742231AE /* ProgramIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgramIconView.swift; sourceTree = ""; };
+ 7A93F021F55DAD2BC6722D54 /* Program+Display.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Program+Display.swift"; sourceTree = ""; };
+ 7A110F88A1199B4E9C0C8F3A /* PinnedProgramsGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedProgramsGrid.swift; sourceTree = ""; };
+ 7A30D4DC6FC022EF1446508D /* OnboardingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingModel.swift; sourceTree = ""; };
+ 7A7B1B81885C080827CC7B0F /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; };
+ 7A53D33606FF388802121151 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; };
+ 7A3E5CD4F34E72EE880BF131 /* WhiskyWineUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhiskyWineUpdate.swift; sourceTree = ""; };
+ 7ACB6C31E47142A637015EA4 /* SteamSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteamSession.swift; sourceTree = ""; };
+ 7A5CAFC811A7E1E60978D49E /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -206,6 +208,7 @@
children = (
6E40498229CCA91B006E3F1B /* Bottle+Extensions.swift */,
6365C4C02B1AA69D00AAE1FD /* Animation+Extensions.swift */,
+ 7A93F021F55DAD2BC6722D54 /* Program+Display.swift */,
);
path = Extensions;
sourceTree = "";
@@ -213,9 +216,9 @@
63FFDE822ADEFADF00178665 /* Common */ = {
isa = PBXGroup;
children = (
- 63FFDE852ADF0C7700178665 /* BottomBar.swift */,
6330DD952B1B0EA4007A625A /* RenameView.swift */,
67D278502BC5907E006F9A1E /* ActionView.swift */,
+ 7A3FC8B99010FD02742231AE /* ProgramIconView.swift */,
);
path = Common;
sourceTree = "";
@@ -226,6 +229,7 @@
6E17B6452AF3FDC100831173 /* PinView.swift */,
6E2B25C02B0E20F50084A67A /* PinAddView.swift */,
DB696FC72AFAE5DA0037EB2F /* PinCreationView.swift */,
+ 7A110F88A1199B4E9C0C8F3A /* PinnedProgramsGrid.swift */,
);
path = Pins;
sourceTree = "";
@@ -257,7 +261,6 @@
6E50D98429CDF25B008C39F6 /* BottleCreationView.swift */,
6E355E5729D78249002D83BE /* ConfigView.swift */,
6E182FC92B0BF64E00AADE81 /* WinetricksView.swift */,
- AB0BCABC2B61036E00E21C31 /* RunningProcessesView.swift */,
6E17B6422AF3FD6E00831173 /* Pins */,
);
path = Bottle;
@@ -326,13 +329,14 @@
children = (
63FFDE822ADEFADF00178665 /* Common */,
6E49E01F2AECB7D000009CAC /* Settings */,
- 6E6C0CF02A419A5800356232 /* Setup */,
6E17B6472AF3FE2E00831173 /* Programs */,
6E355E5629D78202002D83BE /* Bottle */,
6E40495529CCA19C006E3F1B /* WhiskyApp.swift */,
6E40495729CCA19C006E3F1B /* ContentView.swift */,
6E064B1329DD331F00D9A2D2 /* SparkleView.swift */,
6E7C07BF2AAF570100F6E66B /* FileOpenView.swift */,
+ 7A99CC47BF0D98721EA18788 /* Onboarding */,
+ 7A4D66163040AFD339A6F343 /* Home */,
);
path = Views;
sourceTree = "";
@@ -352,6 +356,8 @@
6E70A4A02A9A280C007799E9 /* WhiskyCmd.swift */,
6E7C07BD2AAE7B0100F6E66B /* ProgramShortcut.swift */,
6763D8F52BC6314100651D27 /* Constants.swift */,
+ 7A3E5CD4F34E72EE880BF131 /* WhiskyWineUpdate.swift */,
+ 7A5CAFC811A7E1E60978D49E /* ErrorAlert.swift */,
);
path = Utils;
sourceTree = "";
@@ -365,19 +371,6 @@
name = Frameworks;
sourceTree = "";
};
- 6E6C0CF02A419A5800356232 /* Setup */ = {
- isa = PBXGroup;
- children = (
- 6EF557972A410599001A4F09 /* SetupView.swift */,
- 6E6C0CF12A419A6800356232 /* WelcomeView.swift */,
- 6E7A1F012F00000000000002 /* SetupFailureView.swift */,
- 6E6C0CF32A419A7600356232 /* RosettaView.swift */,
- 6E6C0CF52A419A8300356232 /* WhiskyWineDownloadView.swift */,
- 6E6C0CF72A419A8C00356232 /* WhiskyWineInstallView.swift */,
- );
- path = Setup;
- sourceTree = "";
- };
6E70A4992A9A2197007799E9 /* WhiskyCmd */ = {
isa = PBXGroup;
children = (
@@ -397,6 +390,24 @@
path = WhiskyThumbnail;
sourceTree = "";
};
+ 7A99CC47BF0D98721EA18788 /* Onboarding */ = {
+ isa = PBXGroup;
+ children = (
+ 7A30D4DC6FC022EF1446508D /* OnboardingModel.swift */,
+ 7A7B1B81885C080827CC7B0F /* OnboardingView.swift */,
+ );
+ path = Onboarding;
+ sourceTree = "";
+ };
+ 7A4D66163040AFD339A6F343 /* Home */ = {
+ isa = PBXGroup;
+ children = (
+ 7A53D33606FF388802121151 /* HomeView.swift */,
+ 7ACB6C31E47142A637015EA4 /* SteamSession.swift */,
+ );
+ path = Home;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -594,21 +605,15 @@
6E70A4A12A9A280C007799E9 /* WhiskyCmd.swift in Sources */,
6E40495829CCA19C006E3F1B /* ContentView.swift in Sources */,
67D278512BC5907E006F9A1E /* ActionView.swift in Sources */,
- 6EF557982A410599001A4F09 /* SetupView.swift in Sources */,
6E355E5A29D782B2002D83BE /* ProgramsView.swift in Sources */,
DB696FC82AFAE5DA0037EB2F /* PinCreationView.swift in Sources */,
6E7C07BE2AAE7B0100F6E66B /* ProgramShortcut.swift in Sources */,
6E355E5829D78249002D83BE /* ConfigView.swift in Sources */,
- 63FFDE862ADF0C7700178665 /* BottomBar.swift in Sources */,
- 6E6C0CF62A419A8300356232 /* WhiskyWineDownloadView.swift in Sources */,
6365C4C32B1AA8CD00AAE1FD /* BottleListEntry.swift in Sources */,
6E50D98529CDF25B008C39F6 /* BottleCreationView.swift in Sources */,
6E182FCA2B0BF64E00AADE81 /* WinetricksView.swift in Sources */,
6330DD962B1B0EA4007A625A /* RenameView.swift in Sources */,
- AB0BCABD2B61036E00E21C31 /* RunningProcessesView.swift in Sources */,
6E17B6492AF4118F00831173 /* EnvironmentArgView.swift in Sources */,
- 6E6C0CF42A419A7600356232 /* RosettaView.swift in Sources */,
- 6E6C0CF82A419A8C00356232 /* WhiskyWineInstallView.swift in Sources */,
6365C4C12B1AA69D00AAE1FD /* Animation+Extensions.swift in Sources */,
6E40498329CCA91B006E3F1B /* Bottle+Extensions.swift in Sources */,
6E621CEF2A5F631300C9AAB3 /* Winetricks.swift in Sources */,
@@ -620,10 +625,17 @@
6E2B25C12B0E20F50084A67A /* PinAddView.swift in Sources */,
6E49E0212AECB7DB00009CAC /* SettingsView.swift in Sources */,
6E7C07C02AAF570100F6E66B /* FileOpenView.swift in Sources */,
- 6E6C0CF22A419A6800356232 /* WelcomeView.swift in Sources */,
- 6E7A1F012F00000000000001 /* SetupFailureView.swift in Sources */,
6E40498129CCA8B0006E3F1B /* BottleView.swift in Sources */,
6E355E5E29D7D85D002D83BE /* ProgramView.swift in Sources */,
+ 7AC35FF857263758125C7A31 /* ProgramIconView.swift in Sources */,
+ 7A4C2D88025658F76A141654 /* Program+Display.swift in Sources */,
+ 7ABF25C1A893C7716A1C83DB /* PinnedProgramsGrid.swift in Sources */,
+ 7A69955CE36D4C4767FE307A /* OnboardingModel.swift in Sources */,
+ 7A3FA122329C4A8846CA0588 /* OnboardingView.swift in Sources */,
+ 7A43CB6C325CAA981E94DDE7 /* HomeView.swift in Sources */,
+ 7A14B0D5D96E14098322DFC0 /* WhiskyWineUpdate.swift in Sources */,
+ 7AA2C89FB1934624B6FFA190 /* SteamSession.swift in Sources */,
+ 7A5E34BEDDA87936C50FB0A2 /* ErrorAlert.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -799,9 +811,9 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Whisky/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
- INFOPLIST_KEY_NSAppleEventsUsageDescription = "Whisky needs to access Terminal to launcher processes in a Terminal window.";
+ INFOPLIST_KEY_NSAppleEventsUsageDescription = "Brandy needs to control Terminal to run programs in a Terminal window.";
INFOPLIST_KEY_NSCameraUsageDescription = "A Windows application is trying to access the camera.";
- INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Whisky";
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Brandy and Whisky contributors";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A Windows application is trying to access the microphone.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -809,8 +821,9 @@
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 2.3.5;
- PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Whisky;
- PRODUCT_NAME = "$(TARGET_NAME)";
+ PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Brandy;
+ PRODUCT_MODULE_NAME = Whisky;
+ PRODUCT_NAME = Brandy;
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = complete;
@@ -838,9 +851,9 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Whisky/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
- INFOPLIST_KEY_NSAppleEventsUsageDescription = "Whisky needs to access Terminal to launcher processes in a Terminal window.";
+ INFOPLIST_KEY_NSAppleEventsUsageDescription = "Brandy needs to control Terminal to run programs in a Terminal window.";
INFOPLIST_KEY_NSCameraUsageDescription = "A Windows application is trying to access the camera.";
- INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Whisky";
+ INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © Brandy and Whisky contributors";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A Windows application is trying to access the microphone.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -848,8 +861,9 @@
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 2.3.5;
- PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Whisky;
- PRODUCT_NAME = "$(TARGET_NAME)";
+ PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Brandy;
+ PRODUCT_MODULE_NAME = Whisky;
+ PRODUCT_NAME = Brandy;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = Whisky;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -874,7 +888,7 @@
LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.istorry.WhiskyCmd;
+ PRODUCT_BUNDLE_IDENTIFIER = com.istorry.BrandyCmd;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
@@ -900,7 +914,7 @@
LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks";
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
- PRODUCT_BUNDLE_IDENTIFIER = com.istorry.WhiskyCmd;
+ PRODUCT_BUNDLE_IDENTIFIER = com.istorry.BrandyCmd;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
@@ -933,7 +947,7 @@
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 2.3.5;
- PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Whisky.WhiskyThumbnail;
+ PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Brandy.Thumbnail;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
@@ -968,7 +982,7 @@
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 2.3.5;
- PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Whisky.WhiskyThumbnail;
+ PRODUCT_BUNDLE_IDENTIFIER = com.istorry.Brandy.Thumbnail;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SKIP_INSTALL = YES;
diff --git a/Whisky/AppDelegate.swift b/Whisky/AppDelegate.swift
index 5eb58cc46..7962ac0fc 100644
--- a/Whisky/AppDelegate.swift
+++ b/Whisky/AppDelegate.swift
@@ -32,6 +32,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
}
+ func applicationWillFinishLaunching(_ notification: Notification) {
+ // Matches the default SettingsView shows, which `UserDefaults.bool(forKey:)` would otherwise read as off
+ UserDefaults.standard.register(defaults: [SettingsView.killOnTerminateKey: true])
+ }
+
func applicationDidFinishLaunching(_ notification: Notification) {
if !hasShownMoveToApplicationsAlert && !AppDelegate.insideAppsFolder {
DispatchQueue.main.asyncAfter(deadline: .now()) {
@@ -43,8 +48,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
func applicationWillTerminate(_ notification: Notification) {
- if UserDefaults.standard.bool(forKey: "killOnTerminate") {
- WhiskyApp.killBottles()
+ if UserDefaults.standard.bool(forKey: SettingsView.killOnTerminateKey) {
+ WhiskyApp.killBottlesBeforeExit()
}
}
@@ -56,7 +61,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
Bundle.main.resourceURL?.deletingLastPathComponent().deletingLastPathComponent()
}
- private static let expectedUrl = URL(fileURLWithPath: "/Applications/Whisky.app")
+ private static let expectedUrl = URL(fileURLWithPath: "/Applications/Brandy.app")
private static var insideAppsFolder: Bool {
if let url = appUrl {
@@ -82,7 +87,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
_ = try FileManager.default.replaceItemAt(AppDelegate.expectedUrl, withItemAt: appURL)
NSWorkspace.shared.open(AppDelegate.expectedUrl)
} catch {
- print("Failed to move the app: \(error)")
+ ErrorAlert.show(String(localized: "error.moveApp"), error: error)
}
}
}
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/128@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/128@2x.png
deleted file mode 100644
index a23d868e9..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/128@2x.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/128R128x1.png b/Whisky/Assets.xcassets/AppIcon.appiconset/128R128x1.png
deleted file mode 100644
index 7f1eef5b6..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/128R128x1.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/16@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/16@2x.png
deleted file mode 100644
index 04fb84354..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/16@2x.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/16R64x1.png b/Whisky/Assets.xcassets/AppIcon.appiconset/16R64x1.png
deleted file mode 100644
index bda6c8924..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/16R64x1.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/256@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/256@2x.png
deleted file mode 100644
index c89c5d927..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/256@2x.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/256R256x1.png b/Whisky/Assets.xcassets/AppIcon.appiconset/256R256x1.png
deleted file mode 100644
index 02cdd20b4..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/256R256x1.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/32@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/32@2x.png
deleted file mode 100644
index df2211a6d..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/32@2x.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/32R64x1.png b/Whisky/Assets.xcassets/AppIcon.appiconset/32R64x1.png
deleted file mode 100644
index c0dc44d64..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/32R64x1.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/512@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/512@2x.png
deleted file mode 100644
index 1fa623786..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/512@2x.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/512R512x1.png b/Whisky/Assets.xcassets/AppIcon.appiconset/512R512x1.png
deleted file mode 100644
index 3793b5fd9..000000000
Binary files a/Whisky/Assets.xcassets/AppIcon.appiconset/512R512x1.png and /dev/null differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/Contents.json b/Whisky/Assets.xcassets/AppIcon.appiconset/Contents.json
index af42c34da..64dc11ee7 100644
--- a/Whisky/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/Whisky/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,61 +1,61 @@
{
"images" : [
{
- "filename" : "16R64x1.png",
+ "filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
- "filename" : "16@2x.png",
+ "filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
- "filename" : "32R64x1.png",
+ "filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
- "filename" : "32@2x.png",
+ "filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
- "filename" : "128R128x1.png",
+ "filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
- "filename" : "128@2x.png",
+ "filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
- "filename" : "256R256x1.png",
+ "filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
- "filename" : "256@2x.png",
+ "filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
- "filename" : "512R512x1.png",
+ "filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
- "filename" : "512@2x.png",
+ "filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png
new file mode 100644
index 000000000..d97747d70
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png
new file mode 100644
index 000000000..543bb52a4
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png
new file mode 100644
index 000000000..90d063fba
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png
new file mode 100644
index 000000000..25f96f32e
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png
new file mode 100644
index 000000000..543bb52a4
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png
new file mode 100644
index 000000000..5f13526ea
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png
new file mode 100644
index 000000000..25f96f32e
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png
new file mode 100644
index 000000000..1c28a9e48
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png
new file mode 100644
index 000000000..5f13526ea
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512.png differ
diff --git a/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png
new file mode 100644
index 000000000..f4f67562d
Binary files /dev/null and b/Whisky/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png differ
diff --git a/Whisky/Extensions/Bottle+Extensions.swift b/Whisky/Extensions/Bottle+Extensions.swift
index 06bb41fba..30767c066 100644
--- a/Whisky/Extensions/Bottle+Extensions.swift
+++ b/Whisky/Extensions/Bottle+Extensions.swift
@@ -22,75 +22,35 @@ import WhiskyKit
import os.log
extension Bottle {
+ /// Windows version, Wine version and program count, e.g. `Windows 10 · Wine 7.7 · 23 programs`
+ var summary: String {
+ let wineVersion = "\(settings.wineVersion.major).\(settings.wineVersion.minor)"
+ return [
+ settings.windowsVersion.pretty(),
+ String(localized: "bottle.wineVersion.\(wineVersion)"),
+ String(localized: "bottle.programCount.\(programs.count)")
+ ].joined(separator: " · ")
+ }
+
func openCDrive() {
NSWorkspace.shared.open(url.appending(path: "drive_c"))
}
+ @MainActor
func openTerminal() {
- let whiskyCmdURL = Bundle.main.url(forResource: "WhiskyCmd", withExtension: nil)
- if let whiskyCmdURL = whiskyCmdURL {
- let whiskyCmd = whiskyCmdURL.path(percentEncoded: false)
- let cmd = "eval \\\"$(\\\"\(whiskyCmd)\\\" shellenv \\\"\(settings.name)\\\")\\\""
-
- let script = """
- tell application "Terminal"
- activate
- do script "\(cmd)"
- end tell
- """
-
- Task.detached(priority: .userInitiated) {
- var error: NSDictionary?
- guard let appleScript = NSAppleScript(source: script) else { return }
- appleScript.executeAndReturnError(&error)
-
- if let error = error {
- Logger.wineKit.error("Failed to run terminal script \(error)")
- guard let description = error["NSAppleScriptErrorMessage"] as? String else { return }
- await self.showRunError(message: String(describing: description))
- }
- }
+ guard let brandyCmd = Bundle.main.url(forResource: "WhiskyCmd", withExtension: nil) else { return }
+ let shellenv = "\(brandyCmd.path(percentEncoded: false).shellQuoted) shellenv \(settings.name.shellQuoted)"
+ do {
+ try Terminal.run("eval \"$(\(shellenv))\"")
+ } catch {
+ ErrorAlert.show(String(localized: "alert.message"), error: error)
}
}
@discardableResult
func getStartMenuPrograms() -> [Program] {
- let globalStartMenu = url
- .appending(path: "drive_c")
- .appending(path: "ProgramData")
- .appending(path: "Microsoft")
- .appending(path: "Windows")
- .appending(path: "Start Menu")
-
- let userStartMenu = url
- .appending(path: "drive_c")
- .appending(path: "users")
- .appending(path: "crossover")
- .appending(path: "AppData")
- .appending(path: "Roaming")
- .appending(path: "Microsoft")
- .appending(path: "Windows")
- .appending(path: "Start Menu")
-
var startMenuPrograms: [Program] = []
- var linkURLs: [URL] = []
- let globalEnumerator = FileManager.default.enumerator(at: globalStartMenu,
- includingPropertiesForKeys: [.isRegularFileKey],
- options: [.skipsHiddenFiles])
- while let url = globalEnumerator?.nextObject() as? URL {
- if url.pathExtension == "lnk" {
- linkURLs.append(url)
- }
- }
-
- let userEnumerator = FileManager.default.enumerator(at: userStartMenu,
- includingPropertiesForKeys: [.isRegularFileKey],
- options: [.skipsHiddenFiles])
- while let url = userEnumerator?.nextObject() as? URL {
- if url.pathExtension == "lnk" {
- linkURLs.append(url)
- }
- }
+ var linkURLs = startMenuFolders.flatMap(Self.shortcuts(in:))
linkURLs.sort(by: { $0.lastPathComponent.lowercased() < $1.lastPathComponent.lowercased() })
@@ -105,97 +65,126 @@ extension Bottle {
}
}
} catch {
- print(error)
+ Logger.wineKit.error("Failed to read Start Menu shortcut `\(link.lastPathComponent)`: \(error)")
}
}
return startMenuPrograms
}
- func updateInstalledPrograms() {
+ /// The shared Start Menu and the user's; Wine names the profile after the macOS user,
+ /// CrossOver-based runtimes named it `crossover`
+ private var startMenuFolders: [URL] {
+ let driveC = url.appending(path: "drive_c")
+ let shared = driveC.appending(path: "ProgramData")
+ let users = [NSUserName(), "crossover"].map { profile in
+ driveC
+ .appending(path: "users")
+ .appending(path: profile)
+ .appending(path: "AppData")
+ .appending(path: "Roaming")
+ }
+ return ([shared] + users).map { folder in
+ folder
+ .appending(path: "Microsoft")
+ .appending(path: "Windows")
+ .appending(path: "Start Menu")
+ }
+ }
+
+ private static func shortcuts(in folder: URL) -> [URL] {
+ let enumerator = FileManager.default.enumerator(at: folder, includingPropertiesForKeys: [.isRegularFileKey],
+ options: [.skipsHiddenFiles])
+ var shortcuts: [URL] = []
+ while let url = enumerator?.nextObject() as? URL {
+ if url.pathExtension == "lnk" {
+ shortcuts.append(url)
+ }
+ }
+ return shortcuts
+ }
+
+ /// Finds the bottle's programs. The walk runs in the background: Program Files includes Steam's whole
+ /// game library, which can be thousands of folders.
+ @MainActor
+ func updateInstalledPrograms() async {
let driveC = url.appending(path: "drive_c")
- var programs: [Program] = []
- var foundURLS: Set = []
+ let hidden = Set(settings.blocklist)
+ let installed = await Task.detached(priority: .userInitiated) {
+ Self.executables(in: driveC, excluding: hidden)
+ }.value
+
+ let pinnedElsewhere = settings.pins.compactMap(\.url).filter { !installed.contains($0) }
+ programs = (installed + pinnedElsewhere)
+ .map { Program(url: $0, bottle: self) }
+ .sorted { $0.name.lowercased() < $1.name.lowercased() }
+ }
+ private nonisolated static func executables(in driveC: URL, excluding hidden: Set) -> [URL] {
+ var executables: [URL] = []
for folderName in ["Program Files", "Program Files (x86)"] {
- let folderURL = driveC.appending(path: folderName)
let enumerator = FileManager.default.enumerator(
- at: folderURL, includingPropertiesForKeys: [.isExecutableKey], options: [.skipsHiddenFiles]
+ at: driveC.appending(path: folderName), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]
)
-
- while let url = enumerator?.nextObject() as? URL {
- guard !url.hasDirectoryPath && url.pathExtension == "exe" else { continue }
- guard !settings.blocklist.contains(url) else { continue }
- foundURLS.insert(url)
- programs.append(Program(url: url, bottle: self))
+ while let file = enumerator?.nextObject() as? URL {
+ guard file.pathExtension.lowercased() == "exe", !file.hasDirectoryPath, !hidden.contains(file)
+ else { continue }
+ executables.append(file)
}
}
-
- // Add missing programs from pins
- for pin in settings.pins {
- guard let url = pin.url else { continue }
- guard !foundURLS.contains(url) else { continue }
- programs.append(Program(url: url, bottle: self))
- }
-
- self.programs = programs.sorted { $0.name.lowercased() < $1.name.lowercased() }
+ return executables
}
@MainActor
func move(destination: URL) {
+ guard let bottle = BottleVM.shared.bottles.first(where: { $0.url == url }) else { return }
+ bottle.inFlight = true
do {
- if let bottle = BottleVM.shared.bottles.first(where: { $0.url == url }) {
- bottle.inFlight = true
- for index in 0..
SUFeedURL
- https://data.getwhisky.app/appcast.xml
- SUPublicEDKey
- tnZFAvPUfCpM7Tr7Sx5gKRm6BUQQ6htJQeOMP44evms=
+ https://github.com/iStorry/Whisky/releases/latest/download/appcast.xml
UTExportedTypeDeclarations
diff --git a/Whisky/Localizable.xcstrings b/Whisky/Localizable.xcstrings
index fe8583a20..68ce1b674 100644
--- a/Whisky/Localizable.xcstrings
+++ b/Whisky/Localizable.xcstrings
@@ -273,6 +273,58 @@
}
}
},
+ "bottle.more" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "More"
+ }
+ }
+ }
+ },
+ "bottle.pinned" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Pinned"
+ }
+ }
+ }
+ },
+ "bottle.programCount.%lld" : {
+ "localizations" : {
+ "en" : {
+ "variations" : {
+ "plural" : {
+ "one" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "%lld program"
+ }
+ },
+ "other" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "%lld programs"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "bottle.wineVersion.%@" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wine %@"
+ }
+ }
+ }
+ },
"button.cDrive" : {
"localizations" : {
"ar" : {
@@ -3266,6 +3318,16 @@
}
}
},
+ "config.avx.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Lets programs that require AVX instructions start."
+ }
+ }
+ }
+ },
"config.avx.warning" : {
"localizations" : {
"ar" : {
@@ -3538,138 +3600,12 @@
}
}
},
- "config.controlPanel" : {
+ "config.buildVersion.info" : {
"localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "فتح لوحة التحكم"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Otevřít Ovládací Panel"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Åbn Kontrolpanel"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Systemsteuerung öffnen"
- }
- },
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Open Control Panel"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abrir Panel de Control"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Avaa ohjauspaneeli"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ouvrir le Panneau de Configuration"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Apri Pannello di controllo"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "コントロール パネルを開く"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "제어판 열기"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Open configuratiescherm"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Otwórz Panel Sterowania"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abrir Painel de Controle"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abrir Painel de Controlo"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Deschide Panoul de Control"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Открыть Панель управления"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kontrol Panelini Aç"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Відкрити Панель Керування"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Mở Control Panel"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "打开控制面板"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "開啟控制台"
+ "value" : "Change only if a program asks for a newer Windows build."
}
}
}
@@ -3810,6 +3746,26 @@
}
}
},
+ "config.dpi.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Scales text and controls in Windows programs."
+ }
+ }
+ }
+ },
+ "config.dpi.value.%lld" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "%lld DPI"
+ }
+ }
+ }
+ },
"config.dxr" : {
"extractionState" : "manual",
"localizations" : {
@@ -4356,6 +4312,26 @@
}
}
},
+ "config.dxvk.async.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Compiles shaders in the background to reduce stutter."
+ }
+ }
+ }
+ },
+ "config.dxvk.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Translates DirectX 9–11 to Vulkan. Try it if a game renders incorrectly."
+ }
+ }
+ }
+ },
"config.dxvkHud" : {
"localizations" : {
"ar" : {
@@ -5036,12878 +5012,9136 @@
}
}
},
- "config.enhacnedSync.esync" : {
+ "config.inspect" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "إعداد..."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Konfigurovat..."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Konfigurer..."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Konfigurieren..."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Configure..."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Configurar..."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Konfiguroi..."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Configurer..."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Configura..."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "設定..."
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "구성..."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Configureer..."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Konfiguruj..."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Configurar..."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Configurar..."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Configurează..."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Настроить..."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Konfigüre et..."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Налаштувати..."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "Cài đặt..."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "配置..."
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ESync"
+ "value" : "設定⋯"
}
}
}
},
- "config.enhacnedSync.msync" : {
+ "config.metalHud" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "HUD Metal"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Hud Metal"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "HUD Metal"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal HUD"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "MSync"
+ "value" : "Metal 抬頭顯示"
+ }
+ }
+ }
+ },
+ "config.metalHud.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Shows frame rate and GPU details over games."
}
}
}
},
- "config.enhancedSync" : {
+ "config.metalTrace" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "مزامنة محسنة"
+ "value" : "Metal Trace"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Trasa Metal"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Metal Trace"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verbessertes Sync"
+ "value" : "Metal Trace"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Metal Trace"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sincronización mejorada"
+ "value" : "Metal Trace"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Parannettu Synkronointi"
+ "value" : "Metal Jäljitin"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sync Avancée"
+ "value" : "Diagnostic de Metal"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Tracciatura Metal"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Metal トレース"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "향상된 동기 처리"
+ "value" : "Metal Trace"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Metal tracering"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rodzaj synchronizacji"
+ "value" : "Śledź Metal"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Registros Metal"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sincronização melhorada"
+ "value" : "Diagnóstico Metal"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Trace Metal"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Улучшенная синхронизация"
+ "value" : "Трассировка Metal"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enhanced Sync"
+ "value" : "Metal İzleme"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Розширена синхронізація"
+ "value" : "Metal Trace"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sync nâng cao"
+ "value" : "Metal Trace"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "增强同步"
+ "value" : "Metal 性能分析"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "增強同步"
+ "value" : "Metal 追蹤"
}
}
}
},
- "config.enhancedSync.none" : {
+ "config.metalTrace.info" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "لا شَيْء"
+ "value" : "يسمح لك بالتقاط عبء عمل الـGPU في Xcode"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Žádný"
+ "value" : "Umožňuje ti zachytit zatížení GPU v Xcode"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ingen"
+ "value" : "Giver dig mulighed for at fange GPU-workload i Xcode"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Keiner"
+ "value" : "Erlaubt es dir, den GPU-Workload in Xcode aufzunehmen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "None"
+ "value" : "Allows you to capture GPU workload in Xcode"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ninguna"
+ "value" : "Permite capturar la carga de trabajo de la GPU en Xcode"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ei mitään"
+ "value" : "Mahdollistaa GPU työn nappaamisen Xcodessa"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Aucun"
+ "value" : "Permet de capturer la charge de travail GPU dans Xcode"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nessuno"
+ "value" : "Permette di catturare il carico di lavoro della GPU in Xcode"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "なし"
+ "value" : "Xcode で GPU ワークロードをキャプチャできるようにします"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "끄기"
+ "value" : "Xcode에서 GPU 워크로드를 기록할 수 있습니다."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Geen"
+ "value" : "Maakt GPU werklast captures mogelijk"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Brak"
+ "value" : "Umożliwia przechwytywanie obciążenia GPU w Xcode"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nenhum"
+ "value" : "Permite capturar a carga de trabalho da GPU no Xcode"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nenhum"
+ "value" : "Permite capturar a carga de trabalho da GPU no Xcode"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Niciunul"
+ "value" : "Permite captarea sarcinii de lucru GPU în Xcode"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Не задана"
+ "value" : "Позволяет отслеживать рабочую нагрузку видеокарты через Xcode"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hiçbiri"
+ "value" : "Xcode'da GPU iş yükünü yakalamak için kullanılır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Немає"
+ "value" : "Дозволяє вам відстежувати навантаження GPU в Xcode"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Không có"
+ "value" : "Cho phép lấy thông tin GPU trong Xcode"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "无"
+ "value" : "允许你在 Xcode 中抓取 GPU 工作负载"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "無"
+ "value" : "允許在 Xcode 中擷取 GPU 工作負載"
}
}
}
},
- "config.inspect" : {
+ "config.notAvailable" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إعداد..."
+ "value" : "لا يوجد"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Konfigurovat..."
+ "value" : "N/A"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Konfigurer..."
+ "value" : "Ikke tilgængelig"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Konfigurieren..."
+ "value" : "Keine Angabe"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configure..."
+ "value" : "N/A"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurar..."
+ "value" : "No disponible"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Konfiguroi..."
+ "value" : "Ei saatavilla"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurer..."
+ "value" : "N/A"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configura..."
+ "value" : "N/D"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "設定..."
+ "value" : "存在しません"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "구성..."
+ "value" : "사용 불가"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configureer..."
+ "value" : "N.v.t."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Konfiguruj..."
+ "value" : "Nie dotyczy"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurar..."
+ "value" : "Indisponível"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurar..."
+ "value" : "N/D"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurează..."
+ "value" : "N/A"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Настроить..."
+ "value" : "Недоступно"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Konfigüre et..."
+ "value" : "Mevcut değil"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Налаштувати..."
+ "value" : "Н/Д"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cài đặt..."
+ "value" : "Không có sẵn"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "配置..."
+ "value" : "不适用"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "設定⋯"
+ "value" : "不適用"
}
}
}
},
- "config.metalHud" : {
+ "config.retinaMode" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "وضع Retina"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Režim Retina"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina Tilstand"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina-Modus"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina Mode"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Modo Retina"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina-tila"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "HUD Metal"
+ "value" : "Mode Retina"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Modalità Retina"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina モード"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina 모드"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina Modus"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Tryb Retina"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Modo Retina"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hud Metal"
+ "value" : "Modo Retina"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "HUD Metal"
+ "value" : "Mod Retina"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Режим Retina"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina Modu"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Режим Retina"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Chế độ Retina"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal HUD"
+ "value" : "Retina 模式"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal 抬頭顯示"
+ "value" : "Retina 模式"
}
}
}
},
- "config.metalTrace" : {
+ "config.retinaMode.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Render at full Retina resolution. Sharper, but slower in games."
+ }
+ }
+ }
+ },
+ "config.summary.avx" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "AVX"
+ }
+ }
+ }
+ },
+ "config.summary.default" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Default settings"
+ }
+ }
+ }
+ },
+ "config.title.dxvk" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Trace"
+ "value" : "DXVK"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Trasa Metal"
+ "value" : "DXVK"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Trace"
+ "value" : "DXVK"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Trace"
+ "value" : "DXVK"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Trace"
+ "value" : "DXVK"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Trace"
+ "value" : "DXVK"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Jäljitin"
+ "value" : "DXVK"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Diagnostic de Metal"
+ "value" : "DXVK"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tracciatura Metal"
+ "value" : "DXVK"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal トレース"
+ "value" : "DXVK"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Trace"
+ "value" : "DXVK"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal tracering"
+ "value" : "DXVK"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Śledź Metal"
+ "value" : "DXVK"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Registros Metal"
+ "value" : "DXVK"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Diagnóstico Metal"
+ "value" : "DXVK"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Trace Metal"
+ "value" : "DXVK"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Трассировка Metal"
+ "value" : "DXVK"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal İzleme"
+ "value" : "DXVK"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Trace"
+ "value" : "DXVK"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal Trace"
+ "value" : "DXVK"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal 性能分析"
+ "value" : "DXVK"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal 追蹤"
+ "value" : "DXVK"
}
}
}
},
- "config.metalTrace.info" : {
+ "config.title.metal" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "يسمح لك بالتقاط عبء عمل الـGPU في Xcode"
+ "value" : "Metal"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Umožňuje ti zachytit zatížení GPU v Xcode"
+ "value" : "Metal"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Giver dig mulighed for at fange GPU-workload i Xcode"
+ "value" : "Metal"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Erlaubt es dir, den GPU-Workload in Xcode aufzunehmen"
+ "value" : "Metal"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Allows you to capture GPU workload in Xcode"
+ "value" : "Metal"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Permite capturar la carga de trabajo de la GPU en Xcode"
+ "value" : "Metal"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Mahdollistaa GPU työn nappaamisen Xcodessa"
+ "value" : "Metal"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Permet de capturer la charge de travail GPU dans Xcode"
+ "value" : "Metal"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Permette di catturare il carico di lavoro della GPU in Xcode"
+ "value" : "Metal"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Xcode で GPU ワークロードをキャプチャできるようにします"
+ "value" : "Metal"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Xcode에서 GPU 워크로드를 기록할 수 있습니다."
+ "value" : "Metal"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Maakt GPU werklast captures mogelijk"
+ "value" : "Metal"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Umożliwia przechwytywanie obciążenia GPU w Xcode"
+ "value" : "Metal"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Permite capturar a carga de trabalho da GPU no Xcode"
+ "value" : "Metal"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Permite capturar a carga de trabalho da GPU no Xcode"
+ "value" : "Metal"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Permite captarea sarcinii de lucru GPU în Xcode"
+ "value" : "Metal"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Позволяет отслеживать рабочую нагрузку видеокарты через Xcode"
+ "value" : "Metal"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Xcode'da GPU iş yükünü yakalamak için kullanılır"
+ "value" : "Metal"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Дозволяє вам відстежувати навантаження GPU в Xcode"
+ "value" : "Metal"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cho phép lấy thông tin GPU trong Xcode"
+ "value" : "Metal"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "允许你在 Xcode 中抓取 GPU 工作负载"
+ "value" : "Metal"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "允許在 Xcode 中擷取 GPU 工作負載"
+ "value" : "Metal"
}
}
}
},
- "config.notAvailable" : {
+ "config.title.tools" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wine Tools"
+ }
+ }
+ }
+ },
+ "config.title.wine" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "لا يوجد"
+ "value" : "Wine"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "N/A"
+ "value" : "Wine"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ikke tilgængelig"
+ "value" : "Wine"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Keine Angabe"
+ "value" : "Wine"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "N/A"
+ "value" : "Wine"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "No disponible"
+ "value" : "Wine"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ei saatavilla"
+ "value" : "Wine"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "N/A"
+ "value" : "Wine"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "N/D"
+ "value" : "Wine"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "存在しません"
+ "value" : "Wine"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "사용 불가"
+ "value" : "Wine"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "N.v.t."
+ "value" : "Wine"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nie dotyczy"
+ "value" : "Wine"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Indisponível"
+ "value" : "Wine"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "N/D"
+ "value" : "Wine"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "N/A"
+ "value" : "Wine"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Недоступно"
+ "value" : "Wine"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Mevcut değil"
+ "value" : "Wine"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Н/Д"
+ "value" : "Wine"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Không có sẵn"
+ "value" : "Wine"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "不适用"
+ "value" : "Wine"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "不適用"
+ "value" : "Wine"
+ }
+ }
+ }
+ },
+ "config.tool.controlPanel" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Control Panel"
+ }
+ }
+ }
+ },
+ "config.tool.controlPanel.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Game controllers, sound and other Windows settings."
+ }
+ }
+ }
+ },
+ "config.tool.open" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Open"
+ }
+ }
+ }
+ },
+ "config.tool.regedit" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Registry Editor"
+ }
+ }
+ }
+ },
+ "config.tool.regedit.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "View and edit this bottle's Windows registry."
+ }
+ }
+ }
+ },
+ "config.tool.winecfg" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Wine Configuration"
+ }
+ }
+ }
+ },
+ "config.tool.winecfg.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Drives, graphics and library overrides."
}
}
}
},
- "config.regedit" : {
+ "config.winVersion" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "فتح محرر التسجيل"
+ "value" : "إصدار Windows"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Otevřít editor databáze registrů"
+ "value" : "Verze Windows"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Åbn Registry Editor"
+ "value" : "Windows-version"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Registry-Editor öffnen"
+ "value" : "Windows Version"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Open Registry Editor"
+ "value" : "Windows Version"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abrir Editor de Registro"
+ "value" : "Versión de Windows"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Avaa rekisterin muokkain"
+ "value" : "Windows versio"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ouvrir l'Éditeur de Registre"
+ "value" : "Version de Windows"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Apri editor del Registro"
+ "value" : "Versione di Windows"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "レジストリ エディターを開く"
+ "value" : "Windows のバージョン"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "레지스트리 편집기 열기"
+ "value" : "Windows 버전"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Open Register Editor"
+ "value" : "Windows-versie"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Otwórz edytor rejestru"
+ "value" : "Wersja Windows"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abrir Editor do Registro"
+ "value" : "Versão do Windows"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abrir Editor de Registo"
+ "value" : "Versão do Windows"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Deschide Editorul de Registre"
+ "value" : "Versiune Windows"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Открыть Редактор реестра"
+ "value" : "Версия Windows"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Kayıt Düzenleyicisi'ni Aç"
+ "value" : "Windows Sürümü"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Відкрити Редактор Реєстру"
+ "value" : "Версія Windows"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Mở Registry Editor"
+ "value" : "Phiên bản Windows"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "打开注册表编辑器"
+ "value" : "Windows 版本"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "開啟註冊表編輯器"
+ "value" : "Windows 版本"
}
}
}
},
- "config.retinaMode" : {
+ "config.winVersion.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "The Windows version that programs in this bottle see."
+ }
+ }
+ }
+ },
+ "configDpi.dpi" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "وضع Retina"
+ "value" : "DPI"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Režim Retina"
+ "value" : "DPI"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina Tilstand"
+ "value" : "DPI"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina-Modus"
+ "value" : "DPI"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina Mode"
+ "value" : "DPI"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Modo Retina"
+ "value" : "DPI"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina-tila"
+ "value" : "DPI"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Mode Retina"
+ "value" : "PPP"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Modalità Retina"
+ "value" : "DPI"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina モード"
+ "value" : "DPI"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina 모드"
+ "value" : "DPI"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina Modus"
+ "value" : "DPI"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tryb Retina"
+ "value" : "DPI"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Modo Retina"
+ "value" : "DPI"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Modo Retina"
+ "value" : "DPI"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Mod Retina"
+ "value" : "DPI"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Режим Retina"
+ "value" : "DPI"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina Modu"
+ "value" : "DPI"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Режим Retina"
+ "value" : "DPI"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chế độ Retina"
+ "value" : "DPI"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina 模式"
+ "value" : "DPI"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retina 模式"
+ "value" : "DPI"
}
}
}
},
- "config.title.dxvk" : {
+ "configDpi.preview" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "سيبدو النص هكذا:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Text bude vypadat takto:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Teksten vil se sådan ud:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Der Text wird wie folgt aussehen:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Text will look like this:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Se verá así:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Teksti tulee näyttämään tältä:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Le texte ressemblera à ça :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Il testo apparirà così:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "テキストはこのように見えます:"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "텍스트는 다음과 같이 표시될 것입니다:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Tekst ziet er als volgt uit:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Tekst będzie wyglądał tak:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "O texto ficará assim:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "O texto ficará assim:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Textul va arăta astfel:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Текст будет выглядеть так:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Metin bu şekilde gözükecek:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Текст виглядатиме так:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "Chữ sẽ trông như thế này:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "文字外观示例:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DXVK"
+ "value" : "文本外觀範例:"
}
}
}
},
- "config.title.metal" : {
+ "configDpi.previewText" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "الثعالب البني السريع يقفز فوق الكلب الكسول."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Příliš žluťoučký kůň úpěl ďábelské ódy."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "The quick brown fox jumps over the lazy dog."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "El zorro rápido y marrón saltó sobre el perro perezoso."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Albert osti fagotin ja töräytti puhkuvan melodian."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Voix ambiguë d'un cœur qui, au zéphyr, préfère les jattes de kiwis."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Ma la volpe, col suo balzo, ha raggiunto il quieto Fido."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "色は匂へど 散りぬるを 我が世誰ぞ 常ならむ 有為の奥山 今日越えて 浅き夢見じ 酔ひもせず。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "다람쥐 헌 쳇바퀴에 타고파."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Pa’s wijze lynx bezag vroom het fikse aquaduct."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Szybki brązowy lis przeskorzył przez leniwego psa."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "A rápida raposa marrom pula sobre o cachorro preguiçoso."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "A raposa castanha ágil salta por cima do cão preguiçoso."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "The quick brown fox jumps over the lazy dog."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Съешь же ещё этих мягких французских булок, да выпей чаю."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Pijamalı hasta yağız şoföre çabucak güvendi."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Жебракують філософи при ґанку церкви в Гадячі, ще й шатро їхнє п’яне знаємо."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "Chú cáo nâu nhanh nhẹn nhảy qua chú chó lười biếng."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "中国智造,慧及全球"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metal"
+ "value" : "敏捷的棕色狐狸跳過了懶狗。"
}
}
}
},
- "config.title.wine" : {
+ "configDpi.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "إعدادات DPI"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Nastavení DPI"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI Indstillinger"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI-Einstellungen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI Settings"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Ajustes DPI"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI asetukset"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Paramètres du PPP"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Impostazioni DPI"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI 設定"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI 설정"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI-instellingen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Ustawienia DPI"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Configurações de DPI"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Configurações de DPI"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Setări DPI"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Настройка DPI"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI Ayarları"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Налаштування DPI"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "Cài đặt DPI"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI 设置"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine"
+ "value" : "DPI 設定"
}
}
}
},
- "config.winecfg" : {
+ "create.browse" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "فتح تكوين Wine"
+ "value" : "تصفح"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Otevřít konfiguraci Wine"
+ "value" : "Procházet"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Åben Wine Konfiguration"
+ "value" : "Gennemse"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Öffne die Wine-Konfiguration"
+ "value" : "Durchsuchen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Open Wine Configuration"
+ "value" : "Browse"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abrir configuración de Wine"
+ "value" : "Explorar"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Avaa Wine konfiguraatio"
+ "value" : "Selaa"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ouvrir la configuration de Wine"
+ "value" : "Explorer"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Apri configurazione Wine"
+ "value" : "Esplora"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine 設定を開く"
+ "value" : "変更..."
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine 설정 열기"
+ "value" : "탐색"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Open Wine Configuratie"
+ "value" : "Zoek"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Otwórz konfigurację Wine"
+ "value" : "Przeglądaj"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abrir Configuração do Wine"
+ "value" : "Procurar"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abrir Configuração do Wine"
+ "value" : "Explorar"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Deschide Configurarea Wine"
+ "value" : "Caută"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Открыть конфигурацию Wine"
+ "value" : "Обзор"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine Yapılandırmasını Aç"
+ "value" : "Gözat"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Відкрити налаштування Wine"
+ "value" : "Огляд"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Mở cài đặt Wine"
+ "value" : "Duyệt"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "打开 Wine 配置"
+ "value" : "浏览"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "開啟Wine設定"
+ "value" : "瀏覽"
}
}
}
},
- "config.winVersion" : {
+ "create.cancel" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إصدار Windows"
+ "value" : "إلغاء"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verze Windows"
+ "value" : "Zrušit"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows-version"
+ "value" : "Annuller"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows Version"
+ "value" : "Abbrechen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows Version"
+ "value" : "Cancel"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versión de Windows"
+ "value" : "Cancelar"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows versio"
+ "value" : "Peru"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Version de Windows"
+ "value" : "Annuler"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versione di Windows"
+ "value" : "Annulla"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows のバージョン"
+ "value" : "キャンセル"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows 버전"
+ "value" : "취소"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows-versie"
+ "value" : "Annuleren"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wersja Windows"
+ "value" : "Anuluj"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versão do Windows"
+ "value" : "Cancelar"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versão do Windows"
+ "value" : "Cancelar"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versiune Windows"
+ "value" : "Anulează"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Версия Windows"
+ "value" : "Отмена"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows Sürümü"
+ "value" : "İptal"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Версія Windows"
+ "value" : "Скасувати"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Phiên bản Windows"
+ "value" : "Huỷ"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows 版本"
+ "value" : "取消"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows 版本"
+ "value" : "取消"
}
}
}
},
- "configDpi.dpi" : {
+ "create.create" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "إنشاء"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Vytořit"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Opret"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Erstellen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Create"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Crear"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Luo"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "PPP"
+ "value" : "Créer"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Crea"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "作成"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "생성"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Aanmaken"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Utwórz"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Criar"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Criar"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Creează"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Создать"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Oluştur"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Створити"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "Tạo"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "创建"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI"
+ "value" : "建立"
}
}
}
},
- "configDpi.preview" : {
+ "create.error.message.%@.%@" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "“%1$@” could not be created. %2$@"
+ }
+ }
+ }
+ },
+ "create.error.title" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Couldn't Create Bottle"
+ }
+ }
+ }
+ },
+ "create.name" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "سيبدو النص هكذا:"
+ "value" : "اسم القارورة:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Text bude vypadat takto:"
+ "value" : "Název láhve:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Teksten vil se sådan ud:"
+ "value" : "Bottle navn:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Der Text wird wie folgt aussehen:"
+ "value" : "Bottle-Name:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Text will look like this:"
+ "value" : "Bottle name:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Se verá así:"
+ "value" : "Nombre de la botella:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Teksti tulee näyttämään tältä:"
+ "value" : "Pullon nimi:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Le texte ressemblera à ça :"
+ "value" : "Nom de la bouteille :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Il testo apparirà così:"
+ "value" : "Nome bottiglia:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "テキストはこのように見えます:"
+ "value" : "Bottle 名:"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "텍스트는 다음과 같이 표시될 것입니다:"
+ "value" : "Bottle 이름:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tekst ziet er als volgt uit:"
+ "value" : "Fles naam:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tekst będzie wyglądał tak:"
+ "value" : "Nazwa butelki:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "O texto ficará assim:"
+ "value" : "Nome da Bottle:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "O texto ficará assim:"
+ "value" : "Nome da Bottle:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Textul va arăta astfel:"
+ "value" : "Numele sticlei:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Текст будет выглядеть так:"
+ "value" : "Название бутылки:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Metin bu şekilde gözükecek:"
+ "value" : "Bottle Adı:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Текст виглядатиме так:"
+ "value" : "Назва пляшки:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chữ sẽ trông như thế này:"
+ "value" : "Tên Bottle:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "文字外观示例:"
+ "value" : "容器名称:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "文本外觀範例:"
+ "value" : "容器名稱:"
}
}
}
},
- "configDpi.previewText" : {
+ "create.path" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "الثعالب البني السريع يقفز فوق الكلب الكسول."
+ "value" : "مسار القارورة:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Příliš žluťoučký kůň úpěl ďábelské ódy."
+ "value" : "Cesta k lahvi:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Walther spillede på xylofon."
+ "value" : "Bottle sti:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich."
+ "value" : "Bottle-Pfad:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "The quick brown fox jumps over the lazy dog."
+ "value" : "Bottle path:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "El zorro rápido y marrón saltó sobre el perro perezoso."
+ "value" : "Ubicación de la botella:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Albert osti fagotin ja töräytti puhkuvan melodian."
+ "value" : "Pullon polku:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Voix ambiguë d'un cœur qui, au zéphyr, préfère les jattes de kiwis."
+ "value" : "Chemin de la bouteille :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ma la volpe, col suo balzo, ha raggiunto il quieto Fido."
+ "value" : "Percorso della bottiglia:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "色は匂へど 散りぬるを 我が世誰ぞ 常ならむ 有為の奥山 今日越えて 浅き夢見じ 酔ひもせず。"
+ "value" : "Bottle の場所:"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "다람쥐 헌 쳇바퀴에 타고파."
+ "value" : "Bottle 경로:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pa’s wijze lynx bezag vroom het fikse aquaduct."
+ "value" : "Fles pad:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Szybki brązowy lis przeskorzył przez leniwego psa."
+ "value" : "Ścieżka butelki:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "A rápida raposa marrom pula sobre o cachorro preguiçoso."
+ "value" : "Caminho da Bottle:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "A raposa castanha ágil salta por cima do cão preguiçoso."
+ "value" : "Caminho do Bottle:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "The quick brown fox jumps over the lazy dog."
+ "value" : "Locația sticlei:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Съешь же ещё этих мягких французских булок, да выпей чаю."
+ "value" : "Путь к бутылке:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pijamalı hasta yağız şoföre çabucak güvendi."
+ "value" : "Bottle Dosya Yolu:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Жебракують філософи при ґанку церкви в Гадячі, ще й шатро їхнє п’яне знаємо."
+ "value" : "Шлях до пляшки:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chú cáo nâu nhanh nhẹn nhảy qua chú chó lười biếng."
+ "value" : "Đường dẫn bottle:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "中国智造,慧及全球"
+ "value" : "容器路径:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "敏捷的棕色狐狸跳過了懶狗。"
+ "value" : "容器路徑:"
}
}
}
},
- "configDpi.title" : {
+ "create.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إعدادات DPI"
+ "value" : "إنشاء قارورة جديدة"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nastavení DPI"
+ "value" : "Vytvořit novou láhev"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI Indstillinger"
+ "value" : "Opret en ny bottle"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI-Einstellungen"
+ "value" : "Erstelle eine neue Bottle"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI Settings"
+ "value" : "Create a new bottle"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ajustes DPI"
+ "value" : "Crear una botella nueva"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI asetukset"
+ "value" : "Luo uusi pullo"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Paramètres du PPP"
+ "value" : "Créer une nouvelle bouteille"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Impostazioni DPI"
+ "value" : "Crea una nuova bottiglia"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI 設定"
+ "value" : "新しい Bottle を作成"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI 설정"
+ "value" : "새로운 Bottle 생성"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI-instellingen"
+ "value" : "Maak een nieuwe fles"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ustawienia DPI"
+ "value" : "Utwórz nową butelkę"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurações de DPI"
+ "value" : "Criar uma nova Bottle"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurações de DPI"
+ "value" : "Criar uma nova Bottle"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Setări DPI"
+ "value" : "Creează o sticlă nouă"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Настройка DPI"
+ "value" : "Создать новую бутылку"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI Ayarları"
+ "value" : "Yeni bir Bottle Oluştur"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Налаштування DPI"
+ "value" : "Створити нову пляшку"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cài đặt DPI"
+ "value" : "Tạo bottle mới"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI 设置"
+ "value" : "创建新的容器"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "DPI 設定"
+ "value" : "建立一個新的容器"
}
}
}
},
- "create.browse" : {
+ "create.win" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "تصفح"
+ "value" : "إصدار Windows:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Procházet"
+ "value" : "Verze Windows:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gennemse"
+ "value" : "Windows-version:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Durchsuchen"
+ "value" : "Windows Version:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Browse"
+ "value" : "Windows version:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Explorar"
+ "value" : "Versión de Windows:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Selaa"
+ "value" : "Windows versio:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Explorer"
+ "value" : "Version de Windows :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Esplora"
+ "value" : "Versione di Windows:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "変更..."
+ "value" : "Windows バージョン:"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "탐색"
+ "value" : "Windows 버전:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Zoek"
+ "value" : "Windows versie:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Przeglądaj"
+ "value" : "Wersja Windows:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Procurar"
+ "value" : "Versão do Windows:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Explorar"
+ "value" : "Versão do Windows:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Caută"
+ "value" : "Versiune Windows:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Обзор"
+ "value" : "Версия Windows:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gözat"
+ "value" : "Windows sürümü:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Огляд"
+ "value" : "Версія Windows:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Duyệt"
+ "value" : "Phiên bản Windows:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "浏览"
+ "value" : "Windows 版本:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "瀏覽"
+ "value" : "Windows 版本:"
}
}
}
},
- "create.cancel" : {
+ "environment.add" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إلغاء"
+ "value" : "إضافة"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Zrušit"
+ "value" : "Přidat"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Annuller"
+ "value" : "Tilføj"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abbrechen"
+ "value" : "Hinzufügen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cancel"
+ "value" : "Add"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cancelar"
+ "value" : "Añadir"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Peru"
+ "value" : "Lisää"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Annuler"
+ "value" : "Ajouter"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Annulla"
+ "value" : "Aggiungi"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "キャンセル"
+ "value" : "追加"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "취소"
+ "value" : "추가"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Annuleren"
+ "value" : "Toevoegen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Anuluj"
+ "value" : "Dodaj"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cancelar"
+ "value" : "Add"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cancelar"
+ "value" : "Adicionar"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Anulează"
+ "value" : "Adăugați"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Отмена"
+ "value" : "Добавить"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "İptal"
+ "value" : "Ekle"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Скасувати"
+ "value" : "Додати"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Huỷ"
+ "value" : "Thêm"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "取消"
+ "value" : "新建"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "取消"
+ "value" : "新增"
}
}
}
},
- "create.create" : {
+ "environment.empty" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "No environment variables"
+ }
+ }
+ }
+ },
+ "environment.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Set when this program starts, in addition to the bottle's settings."
+ }
+ }
+ }
+ },
+ "environment.remove" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إنشاء"
+ "value" : "إزالة"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vytořit"
+ "value" : "Odebrat"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Opret"
+ "value" : "Fjern"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Erstellen"
+ "value" : "Entfernen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Create"
+ "value" : "Remove"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Crear"
+ "value" : "Eliminar"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Luo"
+ "value" : "Poista"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Créer"
+ "value" : "Supprimer"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Crea"
+ "value" : "Rimuovi"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "作成"
+ "value" : "削除"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "생성"
+ "value" : "제거"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Aanmaken"
+ "value" : "Verwijderen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Utwórz"
+ "value" : "Usuń"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Criar"
+ "value" : "Remove"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Criar"
+ "value" : "Remover"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Creează"
+ "value" : "Eliminați"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Создать"
+ "value" : "Удалить"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Oluştur"
+ "value" : "Kaldır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Створити"
+ "value" : "Видалити"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tạo"
+ "value" : "Gỡ"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "创建"
+ "value" : "移除"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "建立"
+ "value" : "移除"
}
}
}
},
- "create.error.message.%@.%@" : {
+ "error.createShortcut" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "“%1$@” could not be created. %2$@"
+ "value" : "Couldn't Create Shortcut"
}
}
}
},
- "create.error.prefixNotCreated.%@" : {
+ "error.exportBottle" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine did not set up the bottle. Wine output: %@"
+ "value" : "Couldn't Export Bottle"
}
}
}
},
- "create.error.title" : {
+ "error.moveApp" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Couldn't Create Bottle"
+ "value" : "Couldn't Move Brandy to Applications"
}
}
}
},
- "create.error.unsupportedLocation.%@" : {
+ "error.moveBottle" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine can't use folders whose names end with a period. Choose a different location than %@."
+ "value" : "Couldn't Move Bottle"
}
}
}
},
- "create.error.whiskyWineNotInstalled" : {
+ "error.openTool" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine is not installed. Install it from Whisky > Setup, then try again."
+ "value" : "Couldn't Open Wine Tool"
}
}
}
},
- "create.name" : {
+ "error.removeBottle" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Couldn't Remove Bottle"
+ }
+ }
+ }
+ },
+ "error.runProgram" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Couldn't Run Program"
+ }
+ }
+ }
+ },
+ "help.github" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "اسم القارورة:"
+ "value" : "Github"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Název láhve:"
+ "value" : "GitHub"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle navn:"
+ "value" : "GitHub"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle-Name:"
+ "value" : "Github"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle name:"
+ "value" : "GitHub"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nombre de la botella:"
+ "value" : "GitHub"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pullon nimi:"
+ "value" : "GitHub"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nom de la bouteille :"
+ "value" : "GitHub"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nome bottiglia:"
+ "value" : "GitHub"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle 名:"
+ "value" : "GitHub"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle 이름:"
+ "value" : "깃허브"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Fles naam:"
+ "value" : "GitHub"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nazwa butelki:"
+ "value" : "GitHub"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nome da Bottle:"
+ "value" : "GitHub"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nome da Bottle:"
+ "value" : "GitHub"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Numele sticlei:"
+ "value" : "GitHub"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Название бутылки:"
+ "value" : "GitHub"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle Adı:"
+ "value" : "GitHub"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Назва пляшки:"
+ "value" : "GitHub"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tên Bottle:"
+ "value" : "GitHub"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "容器名称:"
+ "value" : "GitHub"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "容器名稱:"
+ "value" : "GitHub"
}
}
}
},
- "create.path" : {
+ "home.launchFailed" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Steam Couldn't Open"
+ }
+ }
+ }
+ },
+ "home.openSteam" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Open Steam"
+ }
+ }
+ }
+ },
+ "home.ready.subtitle" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Open Steam to see your games."
+ }
+ }
+ }
+ },
+ "home.ready.title" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Steam Is Ready to Play"
+ }
+ }
+ }
+ },
+ "home.repair" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Repair Steam"
+ }
+ }
+ }
+ },
+ "home.running" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Steam is running."
+ }
+ }
+ }
+ },
+ "home.showSteam" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Show Steam"
+ }
+ }
+ }
+ },
+ "home.trouble" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Having trouble?"
+ }
+ }
+ }
+ },
+ "install.cli" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "مسار القارورة:"
+ "value" : "تثبيت Brandy CLI..."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cesta k lahvi:"
+ "value" : "Nainstalovat Brandy CLI..."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle sti:"
+ "value" : "Installer Brandy CLI..."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle-Pfad:"
+ "value" : "Brandy CLI installieren..."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle path:"
+ "value" : "Install Brandy CLI..."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ubicación de la botella:"
+ "value" : "Instalar Brandy CLI..."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pullon polku:"
+ "value" : "Asenna Brandy CLI..."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chemin de la bouteille :"
+ "value" : "Installer Brandy CLI..."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Percorso della bottiglia:"
+ "value" : "Installa Brandy CLI..."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle の場所:"
+ "value" : "Brandy CLI をインストール..."
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle 경로:"
+ "value" : "Brandy CLI 설치..."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Fles pad:"
+ "value" : "Installeer de Brandy CLI..."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ścieżka butelki:"
+ "value" : "Zainstaluj Brandy w formie CLI..."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Caminho da Bottle:"
+ "value" : "Instalar o Brandy CLI..."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Caminho do Bottle:"
+ "value" : "Instalar Brandy CLI..."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Locația sticlei:"
+ "value" : "Instalează CLI Brandy..."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Путь к бутылке:"
+ "value" : "Установить Brandy CLI..."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle Dosya Yolu:"
+ "value" : "Brandy CLI'i Yükle..."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Шлях до пляшки:"
+ "value" : "Встановити інтерфейс командного рядка..."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đường dẫn bottle:"
+ "value" : "Cài đặt Brandy CLI..."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "容器路径:"
+ "value" : "安装 Brandy CLI..."
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "容器路徑:"
+ "value" : "安裝Brandy CLI⋯"
}
}
}
},
- "create.title" : {
+ "kill.bottles" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إنشاء قارورة جديدة"
+ "value" : "اقتل جميع القوارير"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vytvořit novou láhev"
+ "value" : "Ukončit všechny lahve"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Opret en ny bottle"
+ "value" : "Dræb Alle Bottles"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Erstelle eine neue Bottle"
+ "value" : "Beende sofort alle Bottles"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Create a new bottle"
+ "value" : "Kill All Bottles"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Crear una botella nueva"
+ "value" : "Detener todas las Botellas"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Luo uusi pullo"
+ "value" : "Tapa kaikki pullot"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Créer une nouvelle bouteille"
+ "value" : "Fermer toutes les bouteilles"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Crea una nuova bottiglia"
+ "value" : "Termina tutte le bottiglie"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "新しい Bottle を作成"
+ "value" : "すべての Bottle を終了"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "새로운 Bottle 생성"
+ "value" : "모든 Bottle 종료하기"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Maak een nieuwe fles"
+ "value" : "Stop alle flessen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Utwórz nową butelkę"
+ "value" : "Zakończ wszystkie butelki"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Criar uma nova Bottle"
+ "value" : "Encerrar todas as Bottles"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Criar uma nova Bottle"
+ "value" : "Encerrar todas as Bottles..."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Creează o sticlă nouă"
+ "value" : "Oprește Toate Sticlele"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Создать новую бутылку"
+ "value" : "Закрыть все бутылки"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Yeni bir Bottle Oluştur"
+ "value" : "Tüm Bottle'ları Sonlandır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Створити нову пляшку"
+ "value" : "Примусово завершити всі пляшки"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tạo bottle mới"
+ "value" : "Dừng tất cả Bottle"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "创建新的容器"
+ "value" : "停止所有容器"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "建立一個新的容器"
+ "value" : "終止所有容器"
}
}
}
},
- "create.win" : {
+ "locale.auto" : {
+ "extractionState" : "manual",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إصدار Windows:"
+ "value" : "تلقائي"
}
},
"cs" : {
"stringUnit" : {
- "state" : "translated",
- "value" : "Verze Windows:"
+ "state" : "new",
+ "value" : "Automatic"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows-version:"
+ "value" : "Automatisk"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows Version:"
+ "value" : "Automatisch"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows version:"
+ "value" : "Automatic"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versión de Windows:"
+ "value" : "Automático"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows versio:"
+ "value" : "Automaattinen"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Version de Windows :"
+ "value" : "Automatique"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versione di Windows:"
+ "value" : "Automatico"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows バージョン:"
+ "value" : "自動"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows 버전:"
+ "value" : "자동"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows versie:"
+ "value" : "Automatisch"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wersja Windows:"
+ "value" : "Automatyczne"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versão do Windows:"
+ "value" : "Automático"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versão do Windows:"
+ "value" : "Automático"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Versiune Windows:"
+ "value" : "Automat"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Версия Windows:"
+ "value" : "Автоматический выбор"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows sürümü:"
+ "value" : "Otomatik"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Версія Windows:"
+ "value" : "Автоматично"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Phiên bản Windows:"
+ "value" : "Tự động"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows 版本:"
+ "value" : "自动"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Windows 版本:"
+ "value" : "自動"
}
}
}
},
- "environment.add" : {
+ "locale.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إضافة"
+ "value" : "محلي"
}
},
"cs" : {
"stringUnit" : {
- "state" : "translated",
- "value" : "Přidat"
+ "state" : "new",
+ "value" : "Locale"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tilføj"
+ "value" : "Sprog"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hinzufügen"
+ "value" : "Sprache"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Add"
+ "value" : "Locale"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Añadir"
+ "value" : "Localización"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Lisää"
+ "value" : "Kieli"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ajouter"
+ "value" : "Langue"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Aggiungi"
+ "value" : "Lingua"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "追加"
+ "value" : "言語"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "추가"
+ "value" : "언어"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Toevoegen"
+ "value" : "Taal"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Dodaj"
+ "value" : "Ustawienie regionalne"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Add"
+ "value" : "Locale"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Adicionar"
+ "value" : "Local"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Adăugați"
+ "value" : "Localizare"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Добавить"
+ "value" : "Языковой стандарт (локаль)"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ekle"
+ "value" : "Yerel"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Додати"
+ "value" : "Мова"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Thêm"
+ "value" : "Địa điểm"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "新建"
+ "value" : "本地化语言"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "新增"
+ "value" : "語言地區"
}
}
}
},
- "environment.remove" : {
+ "main.bottleMissing" : {
"localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "إزالة"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Odebrat"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fjern"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Entfernen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Remove"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Eliminar"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Poista"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Supprimer"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Rimuovi"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "削除"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "제거"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Verwijderen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Usuń"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Remove"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Remover"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Eliminați"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Удалить"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kaldır"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Видалити"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Gỡ"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "移除"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "移除"
- }
- }
- }
- },
- "help.discord" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "디스코드"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Discord"
- }
- }
- }
- },
- "help.github" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Github"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Github"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "깃허브"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "GitHub"
- }
- }
- }
- },
- "help.website" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "الموقع الالكتروني"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Webové stránky"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Hjemmeside"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Webseite"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Website"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sitio Web"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Verkkosivu"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Site Web"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sito web"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Web サイト"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "웹 사이트"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Website"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Strona internetowa"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Website"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Página web"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pagină Web"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Сайт"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Web sitesi"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Веб-сайт"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Trang web"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "网站"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "網站"
- }
- }
- }
- },
- "install.cli" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "تثبيت Whisky CLI..."
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nainstalovat Whisky CLI..."
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Installer Whisky CLI..."
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Whisky CLI installieren..."
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Install Whisky CLI..."
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Instalar Whisky CLI..."
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Asenna Whisky CLI..."
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Installer Whisky CLI..."
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Installa Whisky CLI..."
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Whisky CLI をインストール..."
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Whisky CLI 설치..."
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Installeer de Whisky CLI..."
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zainstaluj Whisky w formie CLI..."
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Instalar o Whisky CLI..."
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Instalar Whisky CLI..."
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Instalează CLI Whisky..."
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Установить Whisky CLI..."
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Whisky CLI'i Yükle..."
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Встановити інтерфейс командного рядка..."
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Cài đặt Whisky CLI..."
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "安装 Whisky CLI..."
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "安裝Whisky CLI⋯"
- }
- }
- }
- },
- "kill.bottles" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "اقتل جميع القوارير"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ukončit všechny lahve"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dræb Alle Bottles"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Beende sofort alle Bottles"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kill All Bottles"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Detener todas las Botellas"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tapa kaikki pullot"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fermer toutes les bouteilles"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Termina tutte le bottiglie"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "すべての Bottle を終了"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "모든 Bottle 종료하기"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Stop alle flessen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zakończ wszystkie butelki"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Encerrar todas as Bottles"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Encerrar todas as Bottles..."
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Oprește Toate Sticlele"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закрыть все бутылки"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tüm Bottle'ları Sonlandır"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Примусово завершити всі пляшки"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dừng tất cả Bottle"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "停止所有容器"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "終止所有容器"
- }
- }
- }
- },
- "locale.auto" : {
- "extractionState" : "manual",
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "تلقائي"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "new",
- "value" : "Automatic"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automatisk"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automatisch"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automatic"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automático"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automaattinen"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automatique"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automatico"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "自動"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "자동"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automatisch"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automatyczne"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automático"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automático"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Automat"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Автоматический выбор"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Otomatik"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Автоматично"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tự động"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "自动"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "自動"
- }
- }
- }
- },
- "locale.title" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "محلي"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "new",
- "value" : "Locale"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sprog"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sprache"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Locale"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Localización"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kieli"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Langue"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lingua"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "言語"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "언어"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Taal"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ustawienie regionalne"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Locale"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Local"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Localizare"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Языковой стандарт (локаль)"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Yerel"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Мова"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Địa điểm"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "本地化语言"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "語言地區"
- }
- }
- }
- },
- "main.createFirst" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "قم بإنشاء قارورتك الأولى للبدء"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pro začátek musíte vytvořit svou první láhev"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Opret din første bottle for at komme igang"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Erstellen Sie Ihre erste Flasche, um loszulegen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Create your first bottle to get started"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Crea tu primera botella para comenzar"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Luo ensimmäinen pullo, jotta voit aloittaa"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Créez votre première bouteille pour commencer"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Crea la tua prima bottiglia per iniziare"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "最初の Bottle を作りましょう"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "첫 번째 Bottle을 만들어 시작하기"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Maak je eerste bottle om te beginnen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Stwórz swoją pierwszą butelkę, aby rozpocząć"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Crie sua primeira Bottle para começar"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Crie a sua primeira bottle para começar"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Creaţi-vă prima sticlă pentru a începe"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Создайте первую бутылку, чтобы начать"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Başlamak için ilk Bottle'ınızı oluşturunuz"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Створіть свою першу пляшку, щоб почати"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tạo bottle đầu tiên của bạn để bắt đầu"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "从创建第一个容器开始"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "建立你的第一個容器以開始"
- }
- }
- }
- },
- "migration.error.message.%@" : {
- "localizations" : {
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Whisky will try again the next time it opens. %@"
- }
- }
- }
- },
- "migration.error.title" : {
- "localizations" : {
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Couldn't Import Data from Upstream Whisky"
- }
- }
- }
- },
- "open.bottle" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "استيراد قارورة"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importovat láhev"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importér Bottle"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Flasche importieren"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Import Bottle"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importar Botella"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tuo pullo"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importer une bouteille"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importa bottiglia"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bottle をインポート"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bottle 불러오기"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importeer Fles"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importuj butelkę"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importar Bottle"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importar Bottle"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Importă Sticlă"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Импортировать бутылку"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Bottle'ı İçeri Aktarın"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Імпортувати пляшку"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nhập Bottle"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "导入容器"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "导入容器"
- }
- }
- }
- },
- "open.logs" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "فتح مجلد السجلات"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Otevřít složku s logy"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Åbn Logmappe"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Log-Folder öffnen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Open Logs Folder"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abrir carpeta de registros"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Avaa log kansio"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ouvrir le dossier des journaux"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Apri la cartella dei log"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ログフォルダを開く"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "로그 폴더 열기"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Open Logs map"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Otwórz folder z logami"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abrir Pasta dos Logs"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Abrir Pasta dos Logs"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Deschide Folder-ul Logs"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Открыть папку логов"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Log Klasörünü Aç"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Відкрити теку журналів"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Mở Folder Log"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "打开日志文件夹"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "開啟日誌資料夾"
- }
- }
- }
- },
- "open.setup" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "إعداد..."
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nastavení..."
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Opsætning..."
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Einrichtung..."
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Setup..."
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Instalar..."
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Asennus..."
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configuration initiale..."
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configurazione..."
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "セットアップ..."
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "설치..."
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Installatie..."
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Konfiguracja..."
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Instalação..."
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configurar..."
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configurare..."
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Установить..."
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kur..."
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Налаштування..."
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Thiết lập..."
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "安装..."
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "配置⋯"
- }
- }
- }
- },
- "pin.create" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "تثبيت"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Připnout"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fastgør"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Anpinnen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Anclar"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kiinnitä"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Épingler"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fissa"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ピン留めする"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "고정"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Vastpinnen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Przypnij"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fixar"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fixează"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закрепить"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sabitle"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закріпити"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ghim"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "置顶"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "釘選"
- }
- }
- }
- },
- "pin.error.duplicate.%@" : {
- "extractionState" : "manual",
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" مثبت بالفعل!"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" je již připnut!"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" er allerede fastgjort!"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" ist bereits angepinnt!"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" is already pinned!"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "¡\"%@\" ya está anclado!"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" on jo kiinnitetty!"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" est déjà épinglé !"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" è già fissato!"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "%@ はすでにピン留めされています。"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\"은(는) 이미 고정되어 있습니다!"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" is al vastgepind!"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" jest już przypięty!"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" is already pinned!"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" já está fixado!"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" este deja fixat!"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" уже закреплен!"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" zaten sabitlenmiş!"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" вже закріплено!"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" đã được ghim!"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "\"%@\" 已被置顶!"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "「%@」已經釘選!"
- }
- }
- }
- },
- "pin.error.title" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "خطأ في تدبيس البرنامج"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Chyba v připínání programu"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fejl ved fastgørelse af program"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fehler beim Anpinnen des Programms"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Error Pinning Program"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Error al fijar el programa"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Virhe kiinnittäessä ohjelmaa"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Erreur en épinglant le programme"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Errore creazione scorciatoia"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ピン留めに失敗しました"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "프로그램 고정 오류 발생"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fout tijdens vastpinnen van het programma"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Błąd podczas przypinania programu"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Error Pinning Program"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Erro ao Fixar Programa"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nu s-a Putut Fixa Programul"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Произошла ошибка при закреплении программы"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programı Sabitleme Hatası"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Помилка під час закріплення програми"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Chương trình ghim bị lỗi"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "置顶程序出错"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "釘選程式時發生錯誤"
- }
- }
- }
- },
- "pin.help" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "تدبيس البرنامج"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Připnout program"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fastgør program"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programm anpinnen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin Program"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Anclar programa"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kiinnitä ohjelma"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Épingler le Programme"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Aggiungi scorciatoia"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "新しくピン留め"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "프로그램 고정"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programma vastpinnen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Przypnij program"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin Program"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fixar Programa"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fixează Programul"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закрепление программы"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programı Sabitle"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закріпити програму"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ghim chương trình"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "置顶程序"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "釘選程式"
- }
- }
- }
- },
- "pin.name" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "اسم الدبوس:"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Název připínáčku:"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Navn på fastgørelse:"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin Name:"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin name:"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nombre del Anclaje:"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kiinnityksen nimi:"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nom de l’épingle :"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nome della scorciatoia:"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "名前:"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "이름:"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Naam pinnen:"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nazwa przypinki:"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin name:"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nome do Fixo:"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Nume pin:"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закреплено:"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sabitleme Adı:"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закріпити ім'я:"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tên của ghim:"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "置顶名称"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "釘選名稱:"
- }
- }
- }
- },
- "pin.path" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "مسار البرنامج:"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Cesta k programu:"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programsti:"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programm Pfad:"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Program path:"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ruta del programa:"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ohjelman polku:"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Emplacement du programme :"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Percorso del programma:"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "パス:"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "프로그램 경로:"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programma pad:"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ścieżka programu:"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Program path:"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Caminho do programa:"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Locația programului:"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Путь к программе:"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Program dosya yolu:"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Шлях до програми:"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Đường dẫn chương trình:"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "程序路径"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "程式路徑:"
- }
- }
- }
- },
- "pin.title" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "تدبيس البرنامج"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Připnout program"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fastgør program"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programm anpinnen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin Program"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Anclar programa"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kiinnitä ohjelma"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Épingler le Programme"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Aggiungi scorciatoia"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "プログラムをピン留め"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "프로그램 고정"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programma vastpinnen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Przypnij program"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pin Program"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fixar Programa"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fixează Programul"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закрепить программу"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Programı Sabitle"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Закріпити програму"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ghim chương trình"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "置顶程序"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "釘選程式"
- }
- }
- }
- },
- "process.table.executable" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "قابل للتنفيذ"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Spustitelný soubor"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Eksekverbar"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ausführbare Datei"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Executable"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ejecutable"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Suoritettava"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Exécutable"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Eseguibile"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "プロセスの実行ファイル"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "실행 파일"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Uitvoerbaar bestand"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Plik wykonywalny"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Executável"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Executável"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Executabil"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Исполняемый файл"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Çalıştırılabilir"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Виконуваний файл"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tệp thi hành"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "可执行文件"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "可執行"
- }
- }
- }
- },
- "process.table.kill" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "إيقاف العملية"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zastavit proces"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Stop Processen"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Prozess beenden"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Stop Process"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Detener el proceso"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Sammuta prosessi"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Arrêter le processus"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Esci dal processo"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "選択したプロセスを停止"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "프로세스 중지"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Proces stoppen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zatrzymaj proces"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Parar Processo"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Parar Processo"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Oprește Procesul"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Остановить процесс"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "İşlemi Sonlandır"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Зупинити Процес"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dừng tiến trình"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "结束进程"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "停止進程"
- }
- }
- }
- },
- "process.table.loading" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "جلب العمليات"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Získávám seznam procesů"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Henter Processer"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lade Prozesse"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fetching Processes"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Obteniendo procesos"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Haetaan prosesseja"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Récupération des processus"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ricerca dei processi"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "プロセスを読み込み中"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "프로세스 목록을 가져오는 중"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Processen ophalen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Pobieranie procesu"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Obtendo Processos"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "A obter Processos"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Preluare Procese"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Получение списка процессов"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "İşlemler Getiriliyor"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Отримання процесів"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Đang lấy những tiến trình"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "正在获取进程"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "正在獲取進程"
- }
- }
- }
- },
- "process.table.pid" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "معرّف العملية"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ID procesu"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Process ID"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Prozess ID"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Process ID"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ID del proceso"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Prosessin ID"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ID du processus"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ID processo"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "プロセス ID"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "프로세스 ID"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Proces ID"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Identyfikator procesu"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ID do Processo"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ID do Processo"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ID Proces"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ID процесса"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "İşlem Kimliği"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ідентифікатор процесу"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Mã ID tiến trình"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "进程ID"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "進程 ID"
- }
- }
- }
- },
- "process.table.refresh" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "تحديث"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Obnovit"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Opfrisk"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Aktualisieren"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Refresh"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Actualizar"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Päivitä"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Rafraîchir"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Aggiorna"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "プロセスを再読み込み"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "새로 고침"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Vernieuwen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Odśwież"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Atualizar"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Atualizar"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Reîmprospătează"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Обновить"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Yenile"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Оновити"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Làm mới"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "刷新"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "重新整理"
- }
- }
- }
- },
- "program.add.blocklist" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "إضافة إلى القائمة السوداء"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Přidat na listinu blokovaných"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Føj til blokeringsliste"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zur Blockliste hinzufügen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Add to Blocklist"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Añadir a la lista de bloqueo"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lisää estolistaan"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ajouter à la liste de programmes bloqués"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Aggiungi alla lista bloccati"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ブロックリストに追加"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "차단 목록에 추가"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Voeg toe aan blokkeerlijst"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dodaj do czarnej listy"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Adicionar à lista de bloqueio"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Adicionar à Lista de Bloqueio"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Adaugă la Blocklist"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Добавить в Черный список"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Engelleme Listesine Ekle"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Додати до чорного списку"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Thêm vào danh sách chặn"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "添加到阻止列表"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "新增至黑名單"
- }
- }
- }
- },
- "program.add.selected.blocklist" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "إضافة المحدد إلى القائمة السوداء"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Přidat označené na listinu blokovaných"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tilføj valgte til blokeringsliste"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ausgewählte Bottle zur Sperrliste hinzufügen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Add Selected to Blocklist"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Añadir seleccionado a la lista"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lisää valitut kieltolistalle"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ajouter sélectionnés à la liste de programmes bloqués"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Aggiungi selezionati alla lista bloccati"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "選択したものをブロックリストに追加"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "선택한 항목을 블랙리스트에 추가"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Voeg geselecteerde toe aan blokkeerlijst"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Dodaj wybrane do czarnej listy"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Add Selected to Blocklist"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Adicionar Selecionados à Lista de Bloqueio"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Adăugați cele selectate la Blocklist"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Добавить выделенное в черный список"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Seçilenleri Engelleme Listesine Ekle"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Додати вибране до списку блокування"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Thêm những mục được chọn vào danh sách chặn"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "将所选内容加入黑名单"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "將選擇項目加入至黑名單"
- }
- }
- }
- },
- "program.args" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "الأوامر"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argumenty"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argumenter"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argumente"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Arguments"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argumentos"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argumentit"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Arguments"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argomenti"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "引数"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "인자"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argumenten"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Parametry"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argumentos"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argumentos"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Parametri"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Аргументы"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Argümanlar"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Аргументи"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Thông số"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "参数"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "命令列引數"
- }
- }
- }
- },
- "program.blocklist" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "القائمة السوداء"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Listina blokovaných"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Blokeringsliste"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Blockliste"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Blocklist"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lista de bloqueo"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Estolista"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Liste de programmes bloqués"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lista bloccati"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "ブロックリスト"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "차단 목록"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Blokkeerlijst"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Czarna lista"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lista de bloqueio"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lista de Bloqueio"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Blocklist"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Черный список"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Karaliste"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Чорний список"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Danh sách chặn"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "阻止列表"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "黑名單"
- }
- }
- }
- },
- "program.config" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "تهيئة"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Konfigurace"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Konfiguration"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Konfiguration"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Config"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configuración"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Konfiguraatio"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configuration"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configurazione"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "設定"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "설정"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configuratie"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Konfiguracja"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configuração"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configuração"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Configurare"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Настройки"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ayarlar"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Налаштування"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Cài đặt"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "配置"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "設定"
- }
- }
- }
- },
- "program.env" : {
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "متغيرات البيئة"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Proměnné prostředí"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Miljøvariabler"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Umgebungsvariablen"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Environment Variables"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Variables de Entorno"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ympäristön muuttujat"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Variables d'environnement"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Variabili d'ambiente"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "環境変数"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "환경 변수"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Omgevingsvariabelen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Zmienne środowiskowe"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Variáveis de Ambiente"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Variáveis de Ambiente"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Variabile de Mediu"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Переменные окружения"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ortam Değişkenleri"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Змінні середовища"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Biến môi trường"
- }
- },
- "zh-Hans" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "环境变量"
+ "value" : "Files not found"
}
- },
- "zh-Hant" : {
+ }
+ }
+ },
+ "main.bottles" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "環境變數"
+ "value" : "Bottles"
}
}
}
},
- "program.remove.blocklist" : {
+ "main.createFirst" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إزالة من القائمة السوداء"
+ "value" : "قم بإنشاء قارورتك الأولى للبدء"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Odstranit z blokovaných"
+ "value" : "Pro začátek musíte vytvořit svou první láhev"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Fjern fra blokeringsliste"
+ "value" : "Opret din første bottle for at komme igang"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Von der Blockliste entfernen"
+ "value" : "Erstellen Sie Ihre erste Flasche, um loszulegen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Remove from Blocklist"
+ "value" : "Create your first bottle to get started"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Eliminar de la lista de bloqueo"
+ "value" : "Crea tu primera botella para comenzar"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Poista estolistalta"
+ "value" : "Luo ensimmäinen pullo, jotta voit aloittaa"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Enlever de la liste de programmes bloqués"
+ "value" : "Créez votre première bouteille pour commencer"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rimuovi dalla lista bloccati"
+ "value" : "Crea la tua prima bottiglia per iniziare"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ブロックリストから削除"
+ "value" : "最初の Bottle を作りましょう"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "차단 목록에서 삭제"
+ "value" : "첫 번째 Bottle을 만들어 시작하기"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verwijder van blokkeerlijst"
+ "value" : "Maak je eerste bottle om te beginnen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Usuń z czarnej listy"
+ "value" : "Stwórz swoją pierwszą butelkę, aby rozpocząć"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Remover da lista de bloqueio"
+ "value" : "Crie sua primeira Bottle para começar"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Remover da Lista de bloqueio"
+ "value" : "Crie a sua primeira bottle para começar"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Eliminați din Blocklist"
+ "value" : "Creaţi-vă prima sticlă pentru a începe"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Удалить из Черного списка"
+ "value" : "Создайте первую бутылку, чтобы начать"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Karalisteden Kaldır"
+ "value" : "Başlamak için ilk Bottle'ınızı oluşturunuz"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Видалити з чорного списку"
+ "value" : "Створіть свою першу пляшку, щоб почати"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gỡ khỏi danh sách chặn"
+ "value" : "Tạo bottle đầu tiên của bạn để bắt đầu"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "从阻止列表中移除"
+ "value" : "从创建第一个容器开始"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "從黑名單中移除"
+ "value" : "建立你的第一個容器以開始"
}
}
}
},
- "program.remove.selected.blocklist" : {
+ "main.creatingBottle" : {
"localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "إزالة من القائمة السوداء"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Odstranit vybrané ze seznamu"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Fjern valgte fra blokeringsliste"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Ausgewählte Bottle von Sperrliste entfernen"
- }
- },
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Remove Selected from Blocklist"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Eliminar seleccionados de la lista"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Poista valitut estolistalta"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Enlever sélectionnés de la liste de programmes bloqués"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Rimuovi selezionati dalla bloccati"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "選択したものをブロックリストから削除"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "선택한 항목을 블랙리스트에서 제거"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Verwijder geselecteerde uit blokkeerlijst"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Usuń wybrane z czarnej listy"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Remove Selected from Blocklist"
+ "value" : "Creating…"
}
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Remover Selecionados da Lista de Bloqueio"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Eliminați cele selectate din Blocklist"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Убрать выделенное из черного списка"
- }
- },
- "tr" : {
+ }
+ }
+ },
+ "main.empty.title" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Seçilenleri Engelleme Listesinden Çıkar"
+ "value" : "No Bottles"
}
- },
- "uk" : {
+ }
+ }
+ },
+ "main.noSelection" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Видалити з чорного списку"
+ "value" : "No Bottle Selected"
}
- },
- "vi" : {
+ }
+ }
+ },
+ "main.noSelection.description" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gỡ những mục được chọn khỏi danh sách chặn"
+ "value" : "Choose a bottle in the sidebar."
}
- },
- "zh-Hans" : {
+ }
+ }
+ },
+ "migration.error.message.%@" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "将所选内容从黑名单中移除"
+ "value" : "Whisky will try again the next time it opens. %@"
}
- },
- "zh-Hant" : {
+ }
+ }
+ },
+ "migration.error.title" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "將選擇項目移除出黑名單"
+ "value" : "Couldn't Import Data from Upstream Whisky"
}
}
}
},
- "program.settings" : {
+ "onboarding.done.runtime" : {
"localizations" : {
- "ar" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "الإعدادات"
+ "value" : "Brandy Is Ready"
}
- },
- "cs" : {
+ }
+ }
+ },
+ "onboarding.done.steam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nastavení"
+ "value" : "Steam Is Ready"
}
- },
- "da" : {
+ }
+ }
+ },
+ "onboarding.done.steam.subtitle" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Indstillinger"
+ "value" : "The first time it opens, Steam updates itself. Then sign in to your account to see your games."
}
- },
- "de" : {
+ }
+ }
+ },
+ "onboarding.error.createBottle" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Einstellungen"
+ "value" : "Brandy couldn't create a place for your games. Try again, and if it keeps failing, restart your Mac."
}
- },
+ }
+ }
+ },
+ "onboarding.error.details" : {
+ "localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Settings"
+ "value" : "Details"
}
- },
- "es" : {
+ }
+ }
+ },
+ "onboarding.error.download" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ajustes"
+ "value" : "The download didn't finish. Check that you're connected to the internet, then try again."
}
- },
- "fi" : {
+ }
+ }
+ },
+ "onboarding.error.installSteam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Asetukset"
+ "value" : "Steam didn't install. Try again, and if it keeps failing, restart your Mac."
}
- },
- "fr" : {
+ }
+ }
+ },
+ "onboarding.error.prepareMac" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Paramètres"
+ "value" : "Your Mac couldn't install Rosetta, which Brandy needs. Make sure you're connected to the internet, then try again."
}
- },
- "it" : {
+ }
+ }
+ },
+ "onboarding.error.updateSteam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Impostazioni"
+ "value" : "Steam's update didn't finish. Check that you're connected to the internet, then try again."
}
- },
- "ja" : {
+ }
+ }
+ },
+ "onboarding.setUpSteam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "設定"
+ "value" : "Set Up Steam"
}
- },
- "ko" : {
+ }
+ }
+ },
+ "onboarding.step.createBottle" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "설정"
+ "value" : "Creating a place for your games"
}
- },
- "nl" : {
+ }
+ }
+ },
+ "onboarding.step.downloadRuntime" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instellingen"
+ "value" : "Downloading the Windows layer"
}
- },
- "pl" : {
+ }
+ }
+ },
+ "onboarding.step.downloadSteam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ustawienia"
+ "value" : "Downloading Steam from Valve"
}
- },
- "pt-BR" : {
+ }
+ }
+ },
+ "onboarding.step.installSteam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Settings"
+ "value" : "Installing Steam"
}
- },
- "pt-PT" : {
+ }
+ }
+ },
+ "onboarding.step.prepareMac" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Definições"
+ "value" : "Getting your Mac ready"
}
- },
- "ro" : {
+ }
+ }
+ },
+ "onboarding.step.updateSteam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Setări"
+ "value" : "Updating Steam"
}
- },
- "ru" : {
+ }
+ }
+ },
+ "onboarding.subtitle" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Настройки"
+ "value" : "Brandy sets up Steam for you. It takes a few minutes and about 3 GB of space."
}
- },
- "tr" : {
+ }
+ }
+ },
+ "onboarding.title" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ayarlar"
+ "value" : "Play Windows Games on Your Mac"
}
- },
- "uk" : {
+ }
+ }
+ },
+ "onboarding.withoutSteam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Налаштування"
+ "value" : "Set up without Steam"
}
- },
- "vi" : {
+ }
+ }
+ },
+ "onboarding.working.runtime" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cài đặt"
+ "value" : "Setting Up Brandy"
}
- },
- "zh-Hans" : {
+ }
+ }
+ },
+ "onboarding.working.steam" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "设置"
+ "value" : "Setting Up Steam"
}
- },
- "zh-Hant" : {
+ }
+ }
+ },
+ "onboarding.working.subtitle" : {
+ "localizations" : {
+ "en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "設定"
+ "value" : "You can leave this window open. Nothing else is needed from you."
}
}
}
},
- "program.title" : {
+ "open.bottle" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "جميع البرامج"
+ "value" : "استيراد قارورة"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Všechny programy"
+ "value" : "Importovat láhev"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Alle programmer"
+ "value" : "Importér Bottle"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Alle Programme"
+ "value" : "Flasche importieren"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "All Programs"
+ "value" : "Import Bottle"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Todos los programas"
+ "value" : "Importar Botella"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Kaikki ohjelmat"
+ "value" : "Tuo pullo"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tous les programmes"
+ "value" : "Importer une bouteille"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tutti i programmi"
+ "value" : "Importa bottiglia"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "すべてのプログラム"
+ "value" : "Bottle をインポート"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "모든 프로그램"
+ "value" : "Bottle 불러오기"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Alle programma's"
+ "value" : "Importeer Fles"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wszystkie programy"
+ "value" : "Importuj butelkę"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Todos os Programas"
+ "value" : "Importar Bottle"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Todos os programas"
+ "value" : "Importar Bottle"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Toate Programele"
+ "value" : "Importă Sticlă"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Все программы"
+ "value" : "Импортировать бутылку"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bütün Programlar"
+ "value" : "Bottle'ı İçeri Aktarın"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Всі програми"
+ "value" : "Імпортувати пляшку"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tất cả ứng dụng"
+ "value" : "Nhập Bottle"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "所有程序"
+ "value" : "导入容器"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "所有程式"
+ "value" : "导入容器"
}
}
}
},
- "rename.bottle.title" : {
+ "open.logs" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إعادة تسمية القارورة"
+ "value" : "فتح مجلد السجلات"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Přejmenovat lahev"
+ "value" : "Otevřít složku s logy"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Omdøb bottle"
+ "value" : "Åbn Logmappe"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle umbenennen"
+ "value" : "Log-Folder öffnen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rename bottle"
+ "value" : "Open Logs Folder"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renombrar botella"
+ "value" : "Abrir carpeta de registros"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Uudelleennimeä pullo"
+ "value" : "Avaa log kansio"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renommer la bouteille"
+ "value" : "Ouvrir le dossier des journaux"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rinomina bottiglia"
+ "value" : "Apri la cartella dei log"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle の名前を変更"
+ "value" : "ログフォルダを開く"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle 이름 변경"
+ "value" : "로그 폴더 열기"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle hernoemen"
+ "value" : "Open Logs map"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Zmień nazwę butelki"
+ "value" : "Otwórz folder z logami"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renomear Bottle"
+ "value" : "Abrir Pasta dos Logs"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renomear a Bottle"
+ "value" : "Abrir Pasta dos Logs"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Redenumiţi sticla"
+ "value" : "Deschide Folder-ul Logs"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Переименовать бутылку"
+ "value" : "Открыть папку логов"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle'ı Yeniden Adlandır"
+ "value" : "Log Klasörünü Aç"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Перейменувати пляшку"
+ "value" : "Відкрити теку журналів"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đổi tên của Bottle"
+ "value" : "Mở Folder Log"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "重新命名容器"
+ "value" : "打开日志文件夹"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "重新命名容器"
+ "value" : "開啟日誌資料夾"
}
}
}
},
- "rename.name" : {
+ "open.setup" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "اسم جديد:"
+ "value" : "إعداد..."
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nové jméno:"
+ "value" : "Nastavení..."
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nyt navn:"
+ "value" : "Opsætning..."
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Neuer Name:"
+ "value" : "Einrichtung..."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "New name:"
+ "value" : "Setup..."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nuevo nombre:"
+ "value" : "Instalar..."
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Uusi nimi:"
+ "value" : "Asennus..."
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nouveau nom :"
+ "value" : "Configuration initiale..."
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nuovo nome:"
+ "value" : "Configurazione..."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "新しい名前:"
+ "value" : "セットアップ..."
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "새 이름:"
+ "value" : "설치..."
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nieuwe naam:"
+ "value" : "Installatie..."
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nowa nazwa:"
+ "value" : "Konfiguracja..."
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Novo nome:"
+ "value" : "Instalação..."
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Novo nome:"
+ "value" : "Configurar..."
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nume nou:"
+ "value" : "Configurare..."
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Новое имя:"
+ "value" : "Установить..."
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Yeni ad:"
+ "value" : "Kur..."
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Нова назва:"
+ "value" : "Налаштування..."
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tên mới:"
+ "value" : "Thiết lập..."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "新名称:"
+ "value" : "安装..."
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "新名稱:"
+ "value" : "配置⋯"
}
}
}
},
- "rename.pin.title" : {
+ "pin.create" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إعادة تسمية الدبوس"
+ "value" : "تثبيت"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Přejmenovat připínáček"
+ "value" : "Připnout"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Omdøb fastgørelse"
+ "value" : "Fastgør"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pin umbenennen"
+ "value" : "Anpinnen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rename pin"
+ "value" : "Pin"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renombrar marcador"
+ "value" : "Anclar"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Uudelleennimeä kiinnike"
+ "value" : "Kiinnitä"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renommer l'épingle"
+ "value" : "Épingler"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rinomina scorciatoia"
+ "value" : "Fissa"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ピン留めの名前を変更"
+ "value" : "ピン留めする"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "이름 변경"
+ "value" : "고정"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pin hernoemen"
+ "value" : "Vastpinnen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Zmień nazwę przypięcia"
+ "value" : "Przypnij"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rename pin"
+ "value" : "Pin"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renomear pin"
+ "value" : "Fixar"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Redenumire pin"
+ "value" : "Fixează"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Переименовать ярлык"
+ "value" : "Закрепить"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sabitlenmiş Programı Adlandır"
+ "value" : "Sabitle"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Перейменувати прикріплене"
+ "value" : "Закріпити"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đổi tên ghim"
+ "value" : "Ghim"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "重命名固定快捷名称"
+ "value" : "置顶"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "重新命名 Pin"
+ "value" : "釘選"
}
}
}
},
- "rename.rename" : {
+ "pin.error.duplicate.%@" : {
+ "extractionState" : "manual",
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إعادة التسمية"
+ "value" : "\"%@\" مثبت بالفعل!"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Přejmenovat"
+ "value" : "\"%@\" je již připnut!"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Omdøb"
+ "value" : "\"%@\" er allerede fastgjort!"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Umbenennen"
+ "value" : "\"%@\" ist bereits angepinnt!"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rename"
+ "value" : "\"%@\" is already pinned!"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renombrar"
+ "value" : "¡\"%@\" ya está anclado!"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Uudelleen nimeä"
+ "value" : "\"%@\" on jo kiinnitetty!"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renommer"
+ "value" : "\"%@\" est déjà épinglé !"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rinomina"
+ "value" : "\"%@\" è già fissato!"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "変更"
+ "value" : "%@ はすでにピン留めされています。"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "이름 변경"
+ "value" : "\"%@\"은(는) 이미 고정되어 있습니다!"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hernoem"
+ "value" : "\"%@\" is al vastgepind!"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Zmień nazwę"
+ "value" : "\"%@\" jest już przypięty!"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Renomear"
+ "value" : "\"%@\" is already pinned!"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Alterar nome"
+ "value" : "\"%@\" já está fixado!"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Redenumiți"
+ "value" : "\"%@\" este deja fixat!"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Переименовать"
+ "value" : "\"%@\" уже закреплен!"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Yeniden Adlandır"
+ "value" : "\"%@\" zaten sabitlenmiş!"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Перейменувати"
+ "value" : "\"%@\" вже закріплено!"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đổi tên"
+ "value" : "\"%@\" đã được ghim!"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "重命名"
+ "value" : "\"%@\" 已被置顶!"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "重新命名"
+ "value" : "「%@」已經釘選!"
}
}
}
},
- "run.bottle" : {
+ "pin.error.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "القارورة:"
+ "value" : "خطأ في تدبيس البرنامج"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Láhev:"
+ "value" : "Chyba v připínání programu"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle:"
+ "value" : "Fejl ved fastgørelse af program"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle:"
+ "value" : "Fehler beim Anpinnen des Programms"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle:"
+ "value" : "Error Pinning Program"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Botella:"
+ "value" : "Error al fijar el programa"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pullo:"
+ "value" : "Virhe kiinnittäessä ohjelmaa"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bouteille :"
+ "value" : "Erreur en épinglant le programme"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottiglia:"
+ "value" : "Errore creazione scorciatoia"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ボトル"
+ "value" : "ピン留めに失敗しました"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle:"
+ "value" : "프로그램 고정 오류 발생"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle:"
+ "value" : "Fout tijdens vastpinnen van het programma"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Butelka:"
+ "value" : "Błąd podczas przypinania programu"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle:"
+ "value" : "Error Pinning Program"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Garrafa:"
+ "value" : "Erro ao Fixar Programa"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sticlă:"
+ "value" : "Nu s-a Putut Fixa Programul"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Бутылка:"
+ "value" : "Произошла ошибка при закреплении программы"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle:"
+ "value" : "Programı Sabitleme Hatası"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Пляшка:"
+ "value" : "Помилка під час закріплення програми"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle:"
+ "value" : "Chương trình ghim bị lỗi"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "容器:"
+ "value" : "置顶程序出错"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "容器:"
+ "value" : "釘選程式時發生錯誤"
}
}
}
},
- "run.title" : {
+ "pin.help" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "تشغيل \"%@\""
+ "value" : "تدبيس البرنامج"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Spustit \"%@\""
+ "value" : "Připnout program"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Kør \"%@\""
+ "value" : "Fastgør program"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Starte \"%@\""
+ "value" : "Programm anpinnen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Run \"%@\""
+ "value" : "Pin Program"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ejecutar \"%@\""
+ "value" : "Anclar programa"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Suorita \"%@\""
+ "value" : "Kiinnitä ohjelma"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Exécuter «%@»"
+ "value" : "Épingler le Programme"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Esegui \"%@\""
+ "value" : "Aggiungi scorciatoia"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "\"%@\" を実行"
+ "value" : "新しくピン留め"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "\"%@\" 실행"
+ "value" : "프로그램 고정"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Start \"%@\""
+ "value" : "Programma vastpinnen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Uruchom \"%@\""
+ "value" : "Przypnij program"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Executar \"%@\""
+ "value" : "Pin Program"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Executar \"%@\""
+ "value" : "Fixar Programa"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rulează \"%@\""
+ "value" : "Fixează Programul"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Запустить \"%@\""
+ "value" : "Закрепление программы"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "\"%@\" Çalıştır"
+ "value" : "Programı Sabitle"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Запустити \"%@\""
+ "value" : "Закріпити програму"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chạy \"%@\""
+ "value" : "Ghim chương trình"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "运行 \"%@\""
+ "value" : "置顶程序"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "執行「%@」"
+ "value" : "釘選程式"
}
}
}
},
- "settings.general" : {
+ "pin.name" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "عام"
+ "value" : "اسم الدبوس:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Obecné"
+ "value" : "Název připínáčku:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Generelt"
+ "value" : "Navn på fastgørelse:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Allgemein"
+ "value" : "Pin Name:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "General"
+ "value" : "Pin name:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "General"
+ "value" : "Nombre del Anclaje:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Yleinen"
+ "value" : "Kiinnityksen nimi:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Général"
+ "value" : "Nom de l’épingle :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Generali"
+ "value" : "Nome della scorciatoia:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "全般"
+ "value" : "名前:"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "일반"
+ "value" : "이름:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Algemeen"
+ "value" : "Naam pinnen:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ogólne"
+ "value" : "Nazwa przypinki:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "General"
+ "value" : "Pin name:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Geral"
+ "value" : "Nome do Fixo:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "General"
+ "value" : "Nume pin:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Основные"
+ "value" : "Закреплено:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Genel"
+ "value" : "Sabitleme Adı:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Основне"
+ "value" : "Закріпити ім'я:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chung"
+ "value" : "Tên của ghim:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "通用"
+ "value" : "置顶名称"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "一般"
+ "value" : "釘選名稱:"
}
}
}
},
- "settings.path" : {
+ "pin.path" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "مسار القارورة الإفتراضي:"
+ "value" : "مسار البرنامج:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Výchozí cesta láhve:"
+ "value" : "Cesta k programu:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Standard Bottle-sti:"
+ "value" : "Programsti:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Standard Bottle-Pfad:"
+ "value" : "Programm Pfad:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Default bottle path:"
+ "value" : "Program path:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ruta predeterminada de la botella:"
+ "value" : "Ruta del programa:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pullon oletuspolku:"
+ "value" : "Ohjelman polku:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chemin par défaut de la bouteille :"
+ "value" : "Emplacement du programme :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Percorso bottiglie predefinito:"
+ "value" : "Percorso del programma:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bottle の既定の保存場所:"
+ "value" : "パス:"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "기본 Bottle 경로:"
+ "value" : "프로그램 경로:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Standaard bottle locatie:"
+ "value" : "Programma pad:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Domyślna ścieżka butelki:"
+ "value" : "Ścieżka programu:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Caminho padrão da bottle:"
+ "value" : "Program path:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Caminho padrão da garrafa:"
+ "value" : "Caminho do programa:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cale implicită a sticlelor:"
+ "value" : "Locația programului:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Путь к бутылке по умолчанию:"
+ "value" : "Путь к программе:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Varsayılan bottle dosya yolu:"
+ "value" : "Program dosya yolu:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Стандартний шлях до пляшки:"
+ "value" : "Шлях до програми:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đường dẫn Bottle mặc định:"
+ "value" : "Đường dẫn chương trình:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "默认容器路径:"
+ "value" : "程序路径"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "預設容器路徑:"
+ "value" : "程式路徑:"
}
}
}
},
- "settings.toggle.kill.on.terminate" : {
+ "pin.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إنهاء عمليات Wine عند إغلاق Whisky"
+ "value" : "تدبيس البرنامج"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ukončit Wine procesy při uzavření Whisky"
+ "value" : "Připnout program"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Afslut Wine-processer, når Whisky lukkes"
+ "value" : "Fastgør program"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine-Prozesse beim Schließen von Whisky beenden"
+ "value" : "Programm anpinnen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Terminate Wine processes when Whisky closes"
+ "value" : "Pin Program"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Terminar procesos de Wine cuando Whisky cierre"
+ "value" : "Anclar programa"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sulje Wine-prosessit kun Whisky suljetaan"
+ "value" : "Kiinnitä ohjelma"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Terminer les processus de Wine à la fermeture de Whisky"
+ "value" : "Épingler le Programme"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Termina i processi Wine quando Whisky chiude"
+ "value" : "Aggiungi scorciatoia"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky の終了時に Wine の全プロセスを停止する"
+ "value" : "プログラムをピン留め"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky를 종료할 때 Wine 프로세스도 종료"
+ "value" : "프로그램 고정"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Beëindig de Wine processen wanneer Whisky sluit"
+ "value" : "Programma vastpinnen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Zakończ proces Wine, gdy Whisky zostanie zamknięte"
+ "value" : "Przypnij program"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Terminate Wine processes when Whisky closes"
+ "value" : "Pin Program"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Termine processos Wine quando o Whisky fechar"
+ "value" : "Fixar Programa"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Închide procesele Wine atunci când Whisky se închide"
+ "value" : "Fixează Programul"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Закрывать процессы Wine при закрытии Whisky"
+ "value" : "Закрепить программу"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky kapandığında Wine işlemlerini sonlandır"
+ "value" : "Programı Sabitle"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Завершити процеси Wine, коли закривається Whisky"
+ "value" : "Закріпити програму"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Buộc Wine phải tắt khi Whisky tắt"
+ "value" : "Ghim chương trình"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "关闭 Whisky 时结束 Wine 进程"
+ "value" : "置顶程序"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky 關閉時終止 Wine 進程"
+ "value" : "釘選程式"
}
}
}
},
- "settings.toggle.whisky.updates" : {
+ "program.add.blocklist" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "التحقق تلقائيا من تحديثات Whisky"
+ "value" : "إضافة إلى القائمة السوداء"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automaticky kontrolovat Whisky aktualizace"
+ "value" : "Přidat na listinu blokovaných"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Søg automatisk efter Whisky-opdateringer"
+ "value" : "Føj til blokeringsliste"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatisch nach Updates für Whisky suchen"
+ "value" : "Zur Blockliste hinzufügen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatically check for Whisky updates"
+ "value" : "Hide"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Buscar actualizaciones de Whiskey automáticamente"
+ "value" : "Añadir a la lista de bloqueo"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automaattisesti tarkista Whisky päivityksien varalta"
+ "value" : "Lisää estolistaan"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rechercher automatiquement les mises à jour de Whisky"
+ "value" : "Ajouter à la liste de programmes bloqués"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Controlla automaticamente aggiornamenti di Whisky"
+ "value" : "Aggiungi alla lista bloccati"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky のアップデートを自動的に確認する"
+ "value" : "ブロックリストに追加"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky 업데이트를 자동으로 확인"
+ "value" : "차단 목록에 추가"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatisch controleren op Whisky updates"
+ "value" : "Voeg toe aan blokkeerlijst"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatycznie sprawdzaj aktualizacje Whisky"
+ "value" : "Dodaj do czarnej listy"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatically check for Whisky updates"
+ "value" : "Adicionar à lista de bloqueio"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verifique automaticamente para atualizações do Whisky"
+ "value" : "Adicionar à Lista de Bloqueio"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verifică automat pentru actualizări Whisky"
+ "value" : "Adaugă la Blocklist"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Автоматически проверять на наличие обновлений Whisky"
+ "value" : "Добавить в Черный список"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky güncellemelerini otomatik olarak kontrol et"
+ "value" : "Engelleme Listesine Ekle"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Автоматична перевірка оновлень Whisky"
+ "value" : "Додати до чорного списку"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tự động kiểm tra bản cập nhật của Whisky"
+ "value" : "Thêm vào danh sách chặn"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "自动检查Whisky更新"
+ "value" : "添加到阻止列表"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "自動檢查 Whisky 更新"
+ "value" : "新增至黑名單"
}
}
}
},
- "settings.toggle.whiskywine.updates" : {
+ "program.add.selected.blocklist" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "التحقق تلقائيا من تحديثات WhiskyWine"
+ "value" : "إضافة المحدد إلى القائمة السوداء"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automaticky vyhledávat WhiskyWine aktualizace"
+ "value" : "Přidat označené na listinu blokovaných"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Søg automatisk efter WhiskyWine-opdateringer"
+ "value" : "Tilføj valgte til blokeringsliste"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatisch nach Updates für WhiskyWine suchen"
+ "value" : "Ausgewählte Bottle zur Sperrliste hinzufügen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatically check for WhiskyWine updates"
+ "value" : "Hide Selected"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Buscar actualizaciones de WhiskyWine automáticamente"
+ "value" : "Añadir seleccionado a la lista"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automaattisesti tarkista WhiskyWine päivityksien varalta"
+ "value" : "Lisää valitut kieltolistalle"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rechercher automatiquement les mises à jour de WhiskyWine"
+ "value" : "Ajouter sélectionnés à la liste de programmes bloqués"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Controlla automaticamente aggiornamenti di WhiskyWine"
+ "value" : "Aggiungi selezionati alla lista bloccati"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine のアップデートを自動的に確認する"
+ "value" : "選択したものをブロックリストに追加"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine 업데이트를 자동으로 확인"
+ "value" : "선택한 항목을 블랙리스트에 추가"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatisch controleren op WhiskyWine updates"
+ "value" : "Voeg geselecteerde toe aan blokkeerlijst"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Automatycznie sprawdzaj dostępność aktualizacji WhiskyWine"
+ "value" : "Dodaj wybrane do czarnej listy"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verificar automaticamente atualizações do WhiskyWine"
+ "value" : "Add Selected to Blocklist"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Procurar atualizações automaticamente"
+ "value" : "Adicionar Selecionados à Lista de Bloqueio"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verifică automat pentru actualizări WhiskyWine"
+ "value" : "Adăugați cele selectate la Blocklist"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Автоматически проверять на наличие обновлений WhiskyWine"
+ "value" : "Добавить выделенное в черный список"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Güncellemeleri otomatik olarak kontrol et"
+ "value" : "Seçilenleri Engelleme Listesine Ekle"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Автоматично перевіряти оновлення WhiskyWine"
+ "value" : "Додати вибране до списку блокування"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tự động kiểm tra bản cập nhật WhiskyWine"
+ "value" : "Thêm những mục được chọn vào danh sách chặn"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "自动检查 WhiskyWine 更新"
+ "value" : "将所选内容加入黑名单"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "自動檢查 WhiskyWine 更新"
+ "value" : "將選擇項目加入至黑名單"
}
}
}
},
- "settings.updates" : {
+ "program.args" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "التحديثات"
+ "value" : "الأوامر"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Aktualizace"
+ "value" : "Argumenty"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Opdateringer"
+ "value" : "Argumenter"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Aktualisierungen"
+ "value" : "Argumente"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Updates"
+ "value" : "Arguments"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Actualizaciones"
+ "value" : "Argumentos"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Päivitykset"
+ "value" : "Argumentit"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Mises à jour"
+ "value" : "Arguments"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Aggiornamenti"
+ "value" : "Argomenti"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "アップデート"
+ "value" : "引数"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "업데이트"
+ "value" : "인자"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Updates"
+ "value" : "Argumenten"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Aktualizacje"
+ "value" : "Parametry"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Updates"
+ "value" : "Argumentos"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Atualizações"
+ "value" : "Argumentos"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Actualizări"
+ "value" : "Parametri"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Обновления"
+ "value" : "Аргументы"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Güncellemeler"
+ "value" : "Argümanlar"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Оновлення"
+ "value" : "Аргументи"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Các bản cập nhật"
+ "value" : "Thông số"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "更新"
+ "value" : "参数"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "更新"
+ "value" : "命令列引數"
}
}
}
},
- "setup.done" : {
+ "program.args.prompt" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "None"
+ }
+ }
+ }
+ },
+ "program.blocklist" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "تم"
+ "value" : "القائمة السوداء"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hotovo"
+ "value" : "Listina blokovaných"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Udført"
+ "value" : "Blokeringsliste"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Fertig"
+ "value" : "Blockliste"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Done"
+ "value" : "Hidden"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Completado"
+ "value" : "Lista de bloqueo"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Valmis"
+ "value" : "Estolista"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Terminé"
+ "value" : "Liste de programmes bloqués"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Fatto"
+ "value" : "Lista bloccati"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "完了"
+ "value" : "ブロックリスト"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "완료"
+ "value" : "차단 목록"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Voltooid"
+ "value" : "Blokkeerlijst"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gotowe"
+ "value" : "Czarna lista"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Feito"
+ "value" : "Lista de bloqueio"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Concluído"
+ "value" : "Lista de Bloqueio"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Finalizat"
+ "value" : "Blocklist"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Готово"
+ "value" : "Черный список"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bitti"
+ "value" : "Karaliste"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Готово"
+ "value" : "Чорний список"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Xong"
+ "value" : "Danh sách chặn"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "完成"
+ "value" : "阻止列表"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "完成"
+ "value" : "黑名單"
+ }
+ }
+ }
+ },
+ "program.blocklist.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Hidden programs don't appear in this list."
}
}
}
},
- "setup.install.checking" : {
+ "program.config" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "جاري التحقق من تثبيت %@..."
+ "value" : "تهيئة"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Kontrola instalace %@..."
+ "value" : "Konfigurace"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tjekker %@ Installation..."
+ "value" : "Konfiguration"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Überprüfen der %@ Installation..."
+ "value" : "Konfiguration"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Checking %@ installation..."
+ "value" : "Config"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Comprobando la instalación %@..."
+ "value" : "Configuración"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tarkistetaan %@ asennusta..."
+ "value" : "Konfiguraatio"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vérification de l'installation de %@..."
+ "value" : "Configuration"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Controllo installazione %@..."
+ "value" : "Configurazione"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ のインストールを確認しています..."
+ "value" : "設定"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ 설치를 확인하는 중입니다..."
+ "value" : "설정"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "De %@ installatie controleren..."
+ "value" : "Configuratie"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sprawdzanie %@ instalacji..."
+ "value" : "Konfiguracja"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verificando instalação de %@..."
+ "value" : "Configuração"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verificando a instalação de %@..."
+ "value" : "Configuração"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Se verifică instalarea %@..."
+ "value" : "Configurare"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Проверка установки %@..."
+ "value" : "Настройки"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ kontrol ediliyor..."
+ "value" : "Ayarlar"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Перевірка інсталяції %@..."
+ "value" : "Налаштування"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đang kiểm tra cài đặt %@..."
+ "value" : "Cài đặt"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "正在检查 %@ 是否安装..."
+ "value" : "配置"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "正在檢查 %@ 安裝⋯⋯"
+ "value" : "設定"
+ }
+ }
+ }
+ },
+ "program.empty" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "No Programs"
+ }
+ }
+ }
+ },
+ "program.empty.description" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Programs you install in this bottle appear here."
}
}
}
},
- "setup.install.installed" : {
+ "program.env" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "تم تثبيت %@"
+ "value" : "متغيرات البيئة"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ nainstalováno"
+ "value" : "Proměnné prostředí"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ installeret"
+ "value" : "Miljøvariabler"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ installiert"
+ "value" : "Umgebungsvariablen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ installed"
+ "value" : "Environment Variables"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ instalado"
+ "value" : "Variables de Entorno"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ asennettu"
+ "value" : "Ympäristön muuttujat"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ installé"
+ "value" : "Variables d'environnement"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ installato"
+ "value" : "Variabili d'ambiente"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ がインストールされました"
+ "value" : "環境変数"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ 설치됨"
+ "value" : "환경 변수"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ geïnstalleerd"
+ "value" : "Omgevingsvariabelen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ zainstalowano"
+ "value" : "Zmienne środowiskowe"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ instalado"
+ "value" : "Variáveis de Ambiente"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ instalado"
+ "value" : "Variáveis de Ambiente"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ instalat"
+ "value" : "Variabile de Mediu"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ – установлено"
+ "value" : "Переменные окружения"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ yüklendi"
+ "value" : "Ortam Değişkenleri"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ встановлено"
+ "value" : "Змінні середовища"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ đã được cài đặt"
+ "value" : "Biến môi trường"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ 已安装"
+ "value" : "环境变量"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ 已安裝"
+ "value" : "環境變數"
+ }
+ }
+ }
+ },
+ "program.launch" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Launch"
+ }
+ }
+ }
+ },
+ "program.launch.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Arguments are passed to the program each time it starts."
}
}
}
},
- "setup.install.notInstalled" : {
+ "program.remove.blocklist" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ لم يتم تثبيته"
+ "value" : "إزالة من القائمة السوداء"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ není nainstalováno"
+ "value" : "Odstranit z blokovaných"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ ikke installeret"
+ "value" : "Fjern fra blokeringsliste"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ nicht installiert"
+ "value" : "Von der Blockliste entfernen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ not installed"
+ "value" : "Show"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ instalado"
+ "value" : "Eliminar de la lista de bloqueo"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ ei asennettu"
+ "value" : "Poista estolistalta"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ pas installé"
+ "value" : "Enlever de la liste de programmes bloqués"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ non installato"
+ "value" : "Rimuovi dalla lista bloccati"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ が未インストール"
+ "value" : "ブロックリストから削除"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ 설치되지 않음"
+ "value" : "차단 목록에서 삭제"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ niet geïnstalleerd"
+ "value" : "Verwijder van blokkeerlijst"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ nie zainstalowano"
+ "value" : "Usuń z czarnej listy"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ não instalado"
+ "value" : "Remover da lista de bloqueio"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ não instalado"
+ "value" : "Remover da Lista de bloqueio"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ neinstalat"
+ "value" : "Eliminați din Blocklist"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ – не установлено"
+ "value" : "Удалить из Черного списка"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ yüklü değil"
+ "value" : "Karalisteden Kaldır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ не встановлено"
+ "value" : "Видалити з чорного списку"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ chưa được cài đặt"
+ "value" : "Gỡ khỏi danh sách chặn"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ 未安装"
+ "value" : "从阻止列表中移除"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "%@ 未安裝"
+ "value" : "從黑名單中移除"
}
}
}
},
- "setup.next" : {
+ "program.settings" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "التالي"
+ "value" : "الإعدادات"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Další"
+ "value" : "Nastavení"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Næste"
+ "value" : "Indstillinger"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Weiter"
+ "value" : "Einstellungen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Next"
+ "value" : "Settings"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Continuar"
+ "value" : "Ajustes"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Seuraava"
+ "value" : "Asetukset"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Suivant"
+ "value" : "Paramètres"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Avanti"
+ "value" : "Impostazioni"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "次へ"
+ "value" : "設定"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "다음"
+ "value" : "설정"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Volgende"
+ "value" : "Instellingen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Dalej"
+ "value" : "Ustawienia"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Seguinte"
+ "value" : "Settings"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Próximo"
+ "value" : "Definições"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Următorul"
+ "value" : "Setări"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Далее"
+ "value" : "Настройки"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sonraki"
+ "value" : "Ayarlar"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Далі"
+ "value" : "Налаштування"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tiếp tục"
+ "value" : "Cài đặt"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "下一页"
+ "value" : "设置"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "下一步"
+ "value" : "設定"
}
}
}
},
- "setup.quit" : {
+ "program.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "خروج"
+ "value" : "جميع البرامج"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ukončit"
+ "value" : "Všechny programy"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Afslut"
+ "value" : "Alle programmer"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abbrechen"
+ "value" : "Alle Programme"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Quit"
+ "value" : "All Programs"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Salir"
+ "value" : "Todos los programas"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sulje"
+ "value" : "Kaikki ohjelmat"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Quitter"
+ "value" : "Tous les programmes"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chiudi"
+ "value" : "Tutti i programmi"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "終了"
+ "value" : "すべてのプログラム"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "종료"
+ "value" : "모든 프로그램"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Afsluiten"
+ "value" : "Alle programma's"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wyjdź"
+ "value" : "Wszystkie programy"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sair"
+ "value" : "Todos os Programas"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sair"
+ "value" : "Todos os programas"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ieșire"
+ "value" : "Toate Programele"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Выйти"
+ "value" : "Все программы"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Çık"
+ "value" : "Bütün Programlar"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Вийти"
+ "value" : "Всі програми"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Thoát"
+ "value" : "Tất cả ứng dụng"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "退出"
+ "value" : "所有程序"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "離開"
+ "value" : "所有程式"
}
}
}
},
- "setup.retry" : {
+ "rename.bottle.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إعادة المحاولة"
+ "value" : "إعادة تسمية القارورة"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Opakovat"
+ "value" : "Přejmenovat lahev"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Forsøg igen"
+ "value" : "Omdøb bottle"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Erneut versuchen"
+ "value" : "Bottle umbenennen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retry"
+ "value" : "Rename bottle"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Reintentar"
+ "value" : "Renombrar botella"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Yritä uudelleen"
+ "value" : "Uudelleennimeä pullo"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Réessayer"
+ "value" : "Renommer la bouteille"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Riprova"
+ "value" : "Rinomina bottiglia"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "再試行"
+ "value" : "Bottle の名前を変更"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "다시 시도"
+ "value" : "Bottle 이름 변경"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Opnieuw Proberen"
+ "value" : "Bottle hernoemen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Spróbuj ponownie"
+ "value" : "Zmień nazwę butelki"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Retry"
+ "value" : "Renomear Bottle"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Voltar a tentar"
+ "value" : "Renomear a Bottle"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Reîncearcă"
+ "value" : "Redenumiţi sticla"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Повторить"
+ "value" : "Переименовать бутылку"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tekrar Dene"
+ "value" : "Bottle'ı Yeniden Adlandır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Повторити"
+ "value" : "Перейменувати пляшку"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Thử lại"
+ "value" : "Đổi tên của Bottle"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "重试"
+ "value" : "重新命名容器"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "重試"
+ "value" : "重新命名容器"
}
}
}
},
- "setup.rosetta" : {
+ "rename.name" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "تثبيت Rosetta"
+ "value" : "اسم جديد:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalování Rosetta"
+ "value" : "Nové jméno:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installere Rosetta"
+ "value" : "Nyt navn:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installiere Rosetta"
+ "value" : "Neuer Name:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installing Rosetta"
+ "value" : "New name:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalando Rosetta"
+ "value" : "Nuevo nombre:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Asennetaan Rosettaa"
+ "value" : "Uusi nimi:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installation de Rosetta"
+ "value" : "Nouveau nom :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installazione di Rosetta"
+ "value" : "Nuovo nome:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta をインストール中"
+ "value" : "新しい名前:"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta를 설치 중입니다."
+ "value" : "새 이름:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta installeren"
+ "value" : "Nieuwe naam:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalowanie Rosetta"
+ "value" : "Nowa nazwa:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalando Rosetta"
+ "value" : "Novo nome:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalando Rosetta"
+ "value" : "Novo nome:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Se instalează Rosetta"
+ "value" : "Nume nou:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Установка Rosetta"
+ "value" : "Новое имя:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta Yükleniyor"
+ "value" : "Yeni ad:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Встановлення Rosetta"
+ "value" : "Нова назва:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đang cài đặt Rosetta"
+ "value" : "Tên mới:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "正在安装 Rosetta"
+ "value" : "新名称:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "正在安裝 Rosetta"
+ "value" : "新名稱:"
}
}
}
},
- "setup.rosetta.fail" : {
+ "rename.pin.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "فشل تثبيت Rosetta 2. الرجاء تثبيته يدوياً."
+ "value" : "إعادة تسمية الدبوس"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalace Rosetta 2 se nezdařila. Nainstalujte ji prosím ručně."
+ "value" : "Přejmenovat připínáček"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2-installation mislykkedes. Installer venligst manuelt."
+ "value" : "Omdøb fastgørelse"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2 Installation fehlgeschlagen. Bitte installieren es manuell."
+ "value" : "Pin umbenennen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2 installation failed. Please install it manually."
+ "value" : "Rename pin"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "La instalación de Rosetta 2 ha fallado. Por favor, instálala manualmente."
+ "value" : "Renombrar marcador"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2:n asennus epäonnistui. Asenna se manuaalisesti."
+ "value" : "Uudelleennimeä kiinnike"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installation de Rosetta 2 a échoué. Veuillez l'installer manuellement."
+ "value" : "Renommer l'épingle"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installazione di Rosetta 2 fallita. Si prega di installarlo manualmente."
+ "value" : "Rinomina scorciatoia"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2 のインストールに失敗しました。手動でインストールしてください。"
+ "value" : "ピン留めの名前を変更"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2 설치에 실패했습니다. 수동으로 설치해 주세요."
+ "value" : "이름 변경"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "De Rosetta 2 installatie is mislukt. Installeer het handmatig."
+ "value" : "Pin hernoemen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalacja Rosetta 2 nie powiodła się. Proszę zainstaluj ją manualnie."
+ "value" : "Zmień nazwę przypięcia"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2 installation failed. Please install it manually."
+ "value" : "Rename pin"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "A instalação do Rosetta 2 falhou. Por favor instale manualmente."
+ "value" : "Renomear pin"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nu s-a putut instala Rosetta 2. Vă rugăm să o instalați manual."
+ "value" : "Redenumire pin"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Установка Rosetta 2 завершилась с ошибкой. Пожалуйста, выполните установку вручную."
+ "value" : "Переименовать ярлык"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2 kurulumu başarısız oldu. Lütfen manuel olarak yükleyin."
+ "value" : "Sabitlenmiş Programı Adlandır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Не вдалося встановити Rosetta 2. Будь ласка, встановіть вручну."
+ "value" : "Перейменувати прикріплене"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Không thể cài đặt Rosetta 2. Hãy cài đặt nó thủ công."
+ "value" : "Đổi tên ghim"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2安装失败。请手动安装。"
+ "value" : "重命名固定快捷名称"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 2 安裝失敗。請手動安裝。"
+ "value" : "重新命名 Pin"
}
}
}
},
- "setup.rosetta.subtitle" : {
+ "rename.rename" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "تسمح Rosetta بأكواد x86، مثل Wine، بالعمل على الماك الخاص بك."
+ "value" : "إعادة التسمية"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta umožňuje x86 kódu, jakým je Wine, běžet na vašem Macu."
+ "value" : "Přejmenovat"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta tillader x86 kode, som Wine, til at køre på din Mac."
+ "value" : "Omdøb"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta erlaubt es, x86 code auf deinem Mac auszuführen, ähnlich wie Wine."
+ "value" : "Umbenennen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta allows x86 code, like Wine, to run on your Mac."
+ "value" : "Rename"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta permite que programas de arquitectura x86 (Wine por ejemplo) funcionen en tu Mac."
+ "value" : "Renombrar"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta, kuten Wine, mahdollistaa x86 koodin käyttämisen Macilläsi."
+ "value" : "Uudelleen nimeä"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta permet au code x86, comme Wine, de s'exécuter sur votre Mac."
+ "value" : "Renommer"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta permette al codice x86, come Wine, di funzionare sul tuo Mac."
+ "value" : "Rinomina"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta は、Wine のような x86 バイナリを Mac 上で実行できるようにします。"
+ "value" : "変更"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta는 Wine과 같은 x86 코드를 당신의 Mac에서 실행할 수 있게 해 줍니다."
+ "value" : "이름 변경"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta laat x86 code, zoals Wine, uitgevoerd worden op je Mac."
+ "value" : "Hernoem"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta pozwala kodowi x86, tak jak Wine, na uruchamianie na komputerze Mac."
+ "value" : "Zmień nazwę"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta permite código x86, como Wine, a rodar em seu Mac."
+ "value" : "Renomear"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta permite que código x86, como o Wine, execute no seu Mac."
+ "value" : "Alterar nome"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta permite codului x86, ca de exemplu Wine, să ruleze pe Mac-ul dvs."
+ "value" : "Redenumiți"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta позволяет программам x86, таким как Wine, работать на вашем Mac."
+ "value" : "Переименовать"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta, Wine gibi, x86 yazılımının Mac bilgisayarınızda çalışmasına izin verir."
+ "value" : "Yeniden Adlandır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta дозволяє запускати x86 код (напр. Wine) на вашому Mac."
+ "value" : "Перейменувати"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta cho phép các ứng dụng x86 như Wine chạy trên máy Mac của bạn."
+ "value" : "Đổi tên"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 让您的 Mac 可以运行 Wine 等 x86 架构的程序。"
+ "value" : "重命名"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rosetta 允許 x86 代碼(如 Wine)在 Mac 上運行。"
+ "value" : "重新命名"
}
}
}
},
- "setup.subtitle" : {
+ "run.bottle" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إدارة التبعيات المطلوبة لـWhisky."
+ "value" : "القارورة:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Spravovat požadované závislosti Whisky."
+ "value" : "Láhev:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Administrer Whisky's nødvendige afhængigheder."
+ "value" : "Bottle:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Verwalte Whiskys erforderliche Abhängigkeiten."
+ "value" : "Bottle:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Manage Whisky's required dependencies."
+ "value" : "Bottle:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Administre las dependencias requeridas de Whisky."
+ "value" : "Botella:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hallitse Whiskyn tarvittavia riippuvuuksia."
+ "value" : "Pullo:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gérer les dépendances requises de Whisky."
+ "value" : "Bouteille :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gestisci le dipendenze richieste da Whisky."
+ "value" : "Bottiglia:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky に必要な依存関係を管理します。"
+ "value" : "ボトル"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky의 필수 의존성 관리하기."
+ "value" : "Bottle:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Beheer Whisky's vereiste dependencies."
+ "value" : "Bottle:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Zarządzaj wymaganymi zależnościami Whisky."
+ "value" : "Butelka:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gerir as dependências necessárias do Whisky."
+ "value" : "Bottle:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gerencie dependências necessárias do Whisky."
+ "value" : "Garrafa:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gestionează dependențele cerute de Whisky."
+ "value" : "Sticlă:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Управление требуемыми зависимостями Whisky."
+ "value" : "Бутылка:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky'nin gerekli yazılımlarını düzenle."
+ "value" : "Bottle:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Керування необхідними залежностями Whisky."
+ "value" : "Пляшка:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Quản lý những tệp quan trọng của Whisky."
+ "value" : "Bottle:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "管理 Whisky 所需依赖"
+ "value" : "容器:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "管理Whisky需要的相依性套件"
+ "value" : "容器:"
}
}
}
},
- "setup.title" : {
+ "run.title" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إعداد التبعيات"
+ "value" : "تشغيل \"%@\""
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nastavení závislostí"
+ "value" : "Spustit \"%@\""
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Opsætning af afhængigheder"
+ "value" : "Kør \"%@\""
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Abhängigkeitseinstellungen"
+ "value" : "Starte \"%@\""
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Dependencies Setup"
+ "value" : "Run \"%@\""
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configuración de dependencias"
+ "value" : "Ejecutar \"%@\""
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Riippuvuuksien valmistelu"
+ "value" : "Suorita \"%@\""
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configuration des dépendances"
+ "value" : "Exécuter «%@»"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurazione delle dipendenze"
+ "value" : "Esegui \"%@\""
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "依存関係のセットアップ"
+ "value" : "\"%@\" を実行"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "의존성 설정"
+ "value" : "\"%@\" 실행"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Beheer dependencies"
+ "value" : "Start \"%@\""
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Konfiguracja zależności"
+ "value" : "Uruchom \"%@\""
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configuração de dependências"
+ "value" : "Executar \"%@\""
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configuração das Dependências"
+ "value" : "Executar \"%@\""
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Configurare Dependențe"
+ "value" : "Rulează \"%@\""
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Установка зависимостей"
+ "value" : "Запустить \"%@\""
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gerekli Yazılımların Kurulumu"
+ "value" : "\"%@\" Çalıştır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Встановлення залежностей"
+ "value" : "Запустити \"%@\""
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cài đặt những tệp quan trọng"
+ "value" : "Chạy \"%@\""
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "依赖项设置"
+ "value" : "运行 \"%@\""
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "相依性管理"
+ "value" : "執行「%@」"
}
}
}
},
- "setup.uninstall" : {
+ "settings.general" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إلغاء التثبيت"
+ "value" : "عام"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Odinstalování"
+ "value" : "Obecné"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Afinstaller"
+ "value" : "Generelt"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Deinstallieren"
+ "value" : "Allgemein"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Uninstall"
+ "value" : "General"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Desinstalar"
+ "value" : "General"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Poista asennus"
+ "value" : "Yleinen"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Désinstaller"
+ "value" : "Général"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Disinstalla"
+ "value" : "Generali"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "アンインストール"
+ "value" : "全般"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "제거"
+ "value" : "일반"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Deïnstalleren"
+ "value" : "Algemeen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Odinstaluj"
+ "value" : "Ogólne"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Desinstalar"
+ "value" : "General"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Remover"
+ "value" : "Geral"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Dezinstalează"
+ "value" : "General"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Удалить"
+ "value" : "Основные"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Kaldır"
+ "value" : "Genel"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Видалити"
+ "value" : "Основне"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gỡ cài đặt"
+ "value" : "Chung"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "卸载"
+ "value" : "通用"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "解除安裝"
+ "value" : "一般"
}
}
}
},
- "setup.welcome" : {
+ "settings.path" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "مرحبا بكم في Whisky"
+ "value" : "مسار القارورة الإفتراضي:"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vítejte ve Whisky"
+ "value" : "Výchozí cesta láhve:"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Velkommen til Whisky"
+ "value" : "Standard Bottle-sti:"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Willkommen zu Whisky"
+ "value" : "Standard Bottle-Pfad:"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Welcome to Whisky"
+ "value" : "Default bottle path:"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bienvenido a Whisky"
+ "value" : "Ruta predeterminada de la botella:"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tervetuloa käyttämään Whiskyä"
+ "value" : "Pullon oletuspolku:"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bienvenue sur Whisky"
+ "value" : "Chemin par défaut de la bouteille :"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Benvenuto su Whisky"
+ "value" : "Percorso bottiglie predefinito:"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky へようこそ"
+ "value" : "Bottle の既定の保存場所:"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky에 오신 것을 환영합니다"
+ "value" : "기본 Bottle 경로:"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Welkom bij Whisky"
+ "value" : "Standaard bottle locatie:"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Witaj w Whisky"
+ "value" : "Domyślna ścieżka butelki:"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bem-vindo ao Whisky"
+ "value" : "Caminho padrão da bottle:"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bem-vindo ao Whisky"
+ "value" : "Caminho padrão da garrafa:"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bine ați venit la Whisky"
+ "value" : "Cale implicită a sticlelor:"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Добро пожаловать в Whisky"
+ "value" : "Путь к бутылке по умолчанию:"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky'ye Hoşgeldiniz"
+ "value" : "Varsayılan bottle dosya yolu:"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ласкаво просимо до Whisky"
+ "value" : "Стандартний шлях до пляшки:"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chào mừng đến với Whisky"
+ "value" : "Đường dẫn Bottle mặc định:"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "欢迎使用 Whisky"
+ "value" : "默认容器路径:"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "歡迎使用 Whisky"
+ "value" : "預設容器路徑:"
+ }
+ }
+ }
+ },
+ "settings.showAdvanced" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Show advanced options"
+ }
+ }
+ }
+ },
+ "settings.showAdvanced.info" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Shows bottles, installed programs and Wine settings."
}
}
}
},
- "setup.welcome.subtitle" : {
+ "settings.toggle.kill.on.terminate" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "دعونا نحصل على الإعداد. لن يستغرق هذا دقيقة."
+ "value" : "إنهاء عمليات Wine عند إغلاق Brandy"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pojďme to nastavit. Nedube to trvat ani minutu."
+ "value" : "Ukončit Wine procesy při uzavření Brandy"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Lad os få dig sat op. Dette vil ikke tage et minut."
+ "value" : "Afslut Wine-processer, når Brandy lukkes"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Lass uns mit der Einrichtung beginnen. Dies dauert keine Minute."
+ "value" : "Wine-Prozesse beim Schließen von Brandy beenden"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Let's get you set up. This won't take a minute."
+ "value" : "Terminate Wine processes when Brandy closes"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vamos a configurar. Solo tomará un minuto."
+ "value" : "Terminar procesos de Wine cuando Brandy cierre"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Valmistellaan perusominaisuudet. Tämä vie vain hetken."
+ "value" : "Sulje Wine-prosessit kun Brandy suljetaan"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Mettons-nous en place. Cela ne prendra qu'un instant."
+ "value" : "Terminer les processus de Wine à la fermeture de Brandy"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Creiamo la tua configurazione. Ci vorrà meno di un minuto."
+ "value" : "Termina i processi Wine quando Brandy chiude"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "セットアップをしましょう。1分以内に終わります。"
+ "value" : "Brandy の終了時に Wine の全プロセスを停止する"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "설정하는 것을 도와드리겠습니다. 얼마 걸리지 않을 거예요."
+ "value" : "Brandy를 종료할 때 Wine 프로세스도 종료"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Laten we ervoor zorgen dat alles klaarstaat. Dit duurt een ogenblik."
+ "value" : "Beëindig de Wine processen wanneer Brandy sluit"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Przygotujmy się. To nie potrwa długo."
+ "value" : "Zakończ proces Wine, gdy Brandy zostanie zamknięte"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vamos preparar as coisas para você. Isso não vai demorar."
+ "value" : "Terminate Wine processes when Brandy closes"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vamos prepará-lo. Isto não irá demorar um minuto."
+ "value" : "Termine processos Wine quando o Brandy fechar"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hai să începem! Nu va lua mult."
+ "value" : "Închide procesele Wine atunci când Brandy se închide"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Производится первоначальная настройка. Это займёт не больше минуты."
+ "value" : "Закрывать процессы Wine при закрытии Brandy"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hadi seni hazırlayalım. Bu çok da uzun sürmeyecek."
+ "value" : "Brandy kapandığında Wine işlemlerini sonlandır"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Давайте все налаштуємо, це не займе багато часу."
+ "value" : "Завершити процеси Wine, коли закривається Brandy"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cùng giúp bạn thiết lập. Điều này sẽ không mất đến một phút."
+ "value" : "Buộc Wine phải tắt khi Brandy tắt"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "让我们帮您安装好,这不会用很长时间。"
+ "value" : "关闭 Brandy 时结束 Wine 进程"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "讓我們為你完成設置。此步驟不會花上一分鐘時間。"
+ "value" : "Brandy 關閉時終止 Wine 進程"
}
}
}
},
- "setup.whiskywine.download" : {
+ "settings.toggle.whisky.updates" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "تنزيل WhiskyWine"
+ "value" : "التحقق تلقائيا من تحديثات Brandy"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Stahování WhiskyWine"
+ "value" : "Automaticky kontrolovat Brandy aktualizace"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Downloader Wine"
+ "value" : "Søg automatisk efter Brandy-opdateringer"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine herunterladen"
+ "value" : "Automatisch nach Updates für Brandy suchen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Downloading WhiskyWine"
+ "value" : "Automatically check for Brandy updates"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Descargando WhiskyWine"
+ "value" : "Buscar actualizaciones de Whiskey automáticamente"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ladataan WhiskyWine"
+ "value" : "Automaattisesti tarkista Brandy päivityksien varalta"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Téléchargement de WhiskyWine"
+ "value" : "Rechercher automatiquement les mises à jour de Brandy"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Scarico Wine"
+ "value" : "Controlla automaticamente aggiornamenti di Brandy"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine をダウンロード中"
+ "value" : "Brandy のアップデートを自動的に確認する"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine를 다운로드 중입니다."
+ "value" : "Brandy 업데이트를 자동으로 확인"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine downloaden"
+ "value" : "Automatisch controleren op Brandy updates"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Pobieranie WhiskyWine"
+ "value" : "Automatycznie sprawdzaj aktualizacje Brandy"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Baixando Wine"
+ "value" : "Automatically check for Brandy updates"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Transferindo Wine"
+ "value" : "Verifique automaticamente para atualizações do Brandy"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Se descarcă WhiskyWine"
+ "value" : "Verifică automat pentru actualizări Brandy"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Скачивание WhiskyWine"
+ "value" : "Автоматически проверять на наличие обновлений Brandy"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine İndiriliyor"
+ "value" : "Brandy güncellemelerini otomatik olarak kontrol et"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Завантаження WhiskyWine"
+ "value" : "Автоматична перевірка оновлень Brandy"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đang tải xuống Wine"
+ "value" : "Tự động kiểm tra bản cập nhật của Brandy"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "正在下载 WhiskyWine"
+ "value" : "自动检查Brandy更新"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "正在下載 WhiskyWine"
- }
- }
- }
- },
- "setup.whiskywine.download.failed" : {
- "localizations" : {
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Download Failed"
+ "value" : "自動檢查 Brandy 更新"
}
}
}
},
- "setup.whiskywine.download.subtitle" : {
+ "settings.toggle.whiskywine.updates" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "ستختلف السرعة على حسب اتصالك بالإنترنت."
+ "value" : "التحقق تلقائيا من تحديثات BrandyWine"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Rychlost stahování se bude lišit v závislosti na vašem internetovém připojení."
+ "value" : "Automaticky vyhledávat BrandyWine aktualizace"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Hastigheden vil variere ud fra din internethastighed."
+ "value" : "Søg automatisk efter BrandyWine-opdateringer"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Die Geschwindigkeit variiert je nach Internetverbindung."
+ "value" : "Automatisch nach Updates für BrandyWine suchen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Speeds will vary on your internet connection."
+ "value" : "Automatically check for BrandyWine updates"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "La velocidad de descarga varía según tu conexión a Internet."
+ "value" : "Buscar actualizaciones de BrandyWine automáticamente"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nopeus vaihtelee internetyhteytesi mukaan."
+ "value" : "Automaattisesti tarkista BrandyWine päivityksien varalta"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "La vitesse de téléchargement peut varier selon votre connexion Internet."
+ "value" : "Rechercher automatiquement les mises à jour de BrandyWine"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "La velocità di download varia in base alla tua connessione internet."
+ "value" : "Controlla automaticamente aggiornamenti di BrandyWine"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "速度はインターネット環境によって変化します。"
+ "value" : "BrandyWine のアップデートを自動的に確認する"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "속도는 인터넷 연결 상태에 따라 달라질 수 있습니다."
+ "value" : "BrandyWine 업데이트를 자동으로 확인"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Snelheden zullen variëren gebaseerd op je internet connectie."
+ "value" : "Automatisch controleren op BrandyWine updates"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Prędkości będą się różnić w zależności od połączenia internetowego."
+ "value" : "Automatycznie sprawdzaj dostępność aktualizacji BrandyWine"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "As velocidades irão variar de acordo com sua internet."
+ "value" : "Verificar automaticamente atualizações do BrandyWine"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "As velocidades variam de acordo com a sua ligação de Internet."
+ "value" : "Procurar atualizações automaticamente"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Viteza va depinde de conexiunea dvs. de internet."
+ "value" : "Verifică automat pentru actualizări BrandyWine"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Скорость скачивания зависит от вашего подключения к Интернету."
+ "value" : "Автоматически проверять на наличие обновлений BrandyWine"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Yükleme hızı internet bağlantınıza göre değişecektir."
+ "value" : "Güncellemeleri otomatik olarak kontrol et"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Швидкість залежить від швидкості Вашого підключення до Інтернету."
+ "value" : "Автоматично перевіряти оновлення BrandyWine"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Tốc độ tải xuống sẽ phụ thuộc vào kết nối Internet của bạn."
+ "value" : "Tự động kiểm tra bản cập nhật BrandyWine"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "下载速度受您的网络质量影响。"
+ "value" : "自动检查 BrandyWine 更新"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "速度會因你的網路頻寬而有所不同。"
+ "value" : "自動檢查 BrandyWine 更新"
}
}
}
},
- "setup.whiskywine.eta" : {
+ "settings.updates" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- متبقي %@"
+ "value" : "التحديثات"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- zbývá %@"
+ "value" : "Aktualizace"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ tilbage"
+ "value" : "Opdateringer"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ verbleibend"
+ "value" : "Aktualisierungen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ remaining"
+ "value" : "Updates"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ restante"
+ "value" : "Actualizaciones"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ jäljellä"
+ "value" : "Päivitykset"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ restantes"
+ "value" : "Mises à jour"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ rimanenti"
+ "value" : "Aggiornamenti"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- 残り %@"
+ "value" : "アップデート"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ 남음"
+ "value" : "업데이트"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ resterend"
+ "value" : "Updates"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- pozostało %@"
+ "value" : "Aktualizacje"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ restantes"
+ "value" : "Updates"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ em falta"
+ "value" : "Atualizações"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- A rămas %@"
+ "value" : "Actualizări"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- осталось %@"
+ "value" : "Обновления"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "-%@ kaldı"
+ "value" : "Güncellemeler"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- залишилось %@"
+ "value" : "Оновлення"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- %@ còn lại"
+ "value" : "Các bản cập nhật"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- 剩余 %@"
+ "value" : "更新"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "- 剩餘%@"
+ "value" : "更新"
}
}
}
},
- "setup.whiskywine.install" : {
+ "setup.done" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "تثبيت WhiskyWine"
+ "value" : "تم"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalace WhiskyWine"
+ "value" : "Hotovo"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installere Wine"
+ "value" : "Udført"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine wird installiert"
+ "value" : "Fertig"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installing WhiskyWine"
+ "value" : "Done"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalando WhiskyWine"
+ "value" : "Completado"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Asennetaan WhiskyWine"
+ "value" : "Valmis"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installation de WhiskyWine"
+ "value" : "Terminé"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Installazione di WhiskyWine"
+ "value" : "Fatto"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine をインストール中"
+ "value" : "完了"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine 설치 중"
+ "value" : "완료"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine installeren"
+ "value" : "Voltooid"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalowanie WhiskyWine"
+ "value" : "Gotowe"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalando Wine"
+ "value" : "Feito"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Instalando Wine"
+ "value" : "Concluído"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Se instalează WhiskyWine"
+ "value" : "Finalizat"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Установка WhiskyWine"
+ "value" : "Готово"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Wine Yükleniyor"
+ "value" : "Bitti"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Встановлення WhiskyWine"
+ "value" : "Готово"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Đang cài đặt Wine"
+ "value" : "Xong"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "正在安装 WhiskyWine"
+ "value" : "完成"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "正在安裝 WhiskyWine"
- }
- }
- }
- },
- "setup.whiskywine.install.failed" : {
- "localizations" : {
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Installation Failed"
+ "value" : "完成"
}
}
}
},
- "setup.whiskywine.install.subtitle" : {
+ "setup.retry" : {
"localizations" : {
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "اوشكت على الوصول. لا تضبط الخروج بعد."
+ "value" : "إعادة المحاولة"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Už to bude. Zatím nezavírejte."
+ "value" : "Opakovat"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Er der næsten. Luk ikke programmet endnu."
+ "value" : "Forsøg igen"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Fast fertig. Noch nicht abbrechen."
+ "value" : "Erneut versuchen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Almost there. Don't tune out yet."
+ "value" : "Retry"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Un momento, ya casi está."
+ "value" : "Reintentar"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Melkein valmista. Älä karkaa vielä."
+ "value" : "Yritä uudelleen"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vous y êtes presque. Veuillez patienter s'il vous plaît."
+ "value" : "Réessayer"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Quasi fatto. Non disconnetterti adesso."
+ "value" : "Riprova"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "あと少しです。そのままお待ち下さい。"
+ "value" : "再試行"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "거의 다 왔어요. 아직 끄지 마세요."
+ "value" : "다시 시도"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bijna daar. Nog niet weg gaan."
+ "value" : "Opnieuw Proberen"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Prawie gotowe. Jeszcze nie wyłączaj."
+ "value" : "Spróbuj ponownie"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Quase lá. Por favor, aguarde."
+ "value" : "Retry"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Quase lá. Não desligue ainda."
+ "value" : "Voltar a tentar"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Aproape terminat. Nu ieși deocamdată."
+ "value" : "Reîncearcă"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Почти закончили. Не отключайтесь."
+ "value" : "Повторить"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bitmek üzere. Lütfen Kapatmayınız."
+ "value" : "Tekrar Dene"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Майже готово."
+ "value" : "Повторити"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Cài đặt sắp hoàn tất."
+ "value" : "Thử lại"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "就快完成了,请不要退出。"
+ "value" : "重试"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "快完成了。請不要退出。"
+ "value" : "重試"
}
}
}
@@ -18325,67 +14559,67 @@
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "في بعض الحالات، Whisky لن يكون قادراً على العمل بشكل صحيح من مجلد مختلف"
+ "value" : "في بعض الحالات، Brandy لن يكون قادراً على العمل بشكل صحيح من مجلد مختلف"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "V některých případech nemusí být Whisky schopna správně fungovat z jiné složky"
+ "value" : "V některých případech nemusí být Brandy schopna správně fungovat z jiné složky"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "I nogle tilfælde vil Whisky ikke være i stand til at fungere korrekt fra en anden mappe"
+ "value" : "I nogle tilfælde vil Brandy ikke være i stand til at fungere korrekt fra en anden mappe"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "In manchen Fällen wird Whisky nicht richtig funktionieren, wenn es in einem anderen Ordner gespeichert ist"
+ "value" : "In manchen Fällen wird Brandy nicht richtig funktionieren, wenn es in einem anderen Ordner gespeichert ist"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "In some cases, Whisky wont't be able to function properly from a different folder"
+ "value" : "In some cases, Brandy won't be able to function properly from a different folder"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "En algunos casos, Whisky podría no funcionar correctamente desde una carpeta diferente"
+ "value" : "En algunos casos, Brandy podría no funcionar correctamente desde una carpeta diferente"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Joissakin tapauksissa, Whiskyn ei ole mahdollista toimia kunnolla muista kansioista"
+ "value" : "Joissakin tapauksissa, Brandyn ei ole mahdollista toimia kunnolla muista kansioista"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Dans certains cas, Whisky ne pourra pas fonctionner correctement depuis un dossier différent"
+ "value" : "Dans certains cas, Brandy ne pourra pas fonctionner correctement depuis un dossier différent"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "In alcuni casi, Whisky non potrà funzionare correttamente da un cartella diversa"
+ "value" : "In alcuni casi, Brandy non potrà funzionare correttamente da un cartella diversa"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "場合によっては、異なるフォルダから Whisky を適切に使用できない場合があります"
+ "value" : "場合によっては、異なるフォルダから Brandy を適切に使用できない場合があります"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "경우에 따라 다른 폴더에서는 Whisky가 제대로 동작하지 않을 수 있습니다."
+ "value" : "경우에 따라 다른 폴더에서는 Brandy가 제대로 동작하지 않을 수 있습니다."
}
},
"nl" : {
@@ -18397,61 +14631,61 @@
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "W niektórych przypadkach Whisky nie będzie w stanie działać poprawnie w innym folderze"
+ "value" : "W niektórych przypadkach Brandy nie będzie w stanie działać poprawnie w innym folderze"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Em alguns casos, o Whisky pode não funcionar corretamente numa pasta diferente"
+ "value" : "Em alguns casos, o Brandy pode não funcionar corretamente numa pasta diferente"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Em alguns casos, o Whisky pode não funcionar corretamente numa pasta diferente"
+ "value" : "Em alguns casos, o Brandy pode não funcionar corretamente numa pasta diferente"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "În unele cazuri, Whisky nu va putea funcționa corect dintr-un folder diferit"
+ "value" : "În unele cazuri, Brandy nu va putea funcționa corect dintr-un folder diferit"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "В некоторых случаях Whisky не сможет правильно функционировать из другой папки"
+ "value" : "В некоторых случаях Brandy не сможет правильно функционировать из другой папки"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bazı durumlarda, Whisky başka bir klasörden düzgün çalışamayabilir"
+ "value" : "Bazı durumlarda, Brandy başka bir klasörden düzgün çalışamayabilir"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "У деяких випадках, Whisky може не працювати належним чином з іншої папки"
+ "value" : "У деяких випадках, Brandy може не працювати належним чином з іншої папки"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Trong một số trường hợp, Whisky sẽ không hoạt động đúng nếu không nằm trong Applications"
+ "value" : "Trong một số trường hợp, Brandy sẽ không hoạt động đúng nếu không nằm trong Applications"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "在某些情况下,Whisky 可能无法在其他的文件夹中正常工作"
+ "value" : "在某些情况下,Brandy 可能无法在其他的文件夹中正常工作"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "在某些情況下,Whisky 可能無法在不同的資料夾中正常運作"
+ "value" : "在某些情況下,Brandy 可能無法在不同的資料夾中正常運作"
}
}
}
@@ -18461,67 +14695,67 @@
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "هل ترغب في نقل Whisky إلى مجلد التطبيقات الخاص بك؟"
+ "value" : "هل ترغب في نقل Brandy إلى مجلد التطبيقات الخاص بك؟"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Chtěli byste přesunout Whisky do složky s aplikacemi?"
+ "value" : "Chtěli byste přesunout Brandy do složky s aplikacemi?"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vil du flytte Whisky til Applications mappen?"
+ "value" : "Vil du flytte Brandy til Applications mappen?"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Möchtest du Whisky in deinen Programm-Ordner legen?"
+ "value" : "Möchtest du Brandy in deinen Programm-Ordner legen?"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Would you like to move Whisky to your Applications folder?"
+ "value" : "Would you like to move Brandy to your Applications folder?"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "¿Te gustaría mover Whisky a la carpeta de Aplicaciones?"
+ "value" : "¿Te gustaría mover Brandy a la carpeta de Aplicaciones?"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Haluatko siirtää Whiskyn Applikaatio-kansioon?"
+ "value" : "Haluatko siirtää Brandyn Applikaatio-kansioon?"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Souhaitez-vous déplacer Whisky dans votre dossier Applications ?"
+ "value" : "Souhaitez-vous déplacer Brandy dans votre dossier Applications ?"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vuoi spostare Whisky nella tua cartella Applicazioni?"
+ "value" : "Vuoi spostare Brandy nella tua cartella Applicazioni?"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky を「アプリケーション」フォルダに移動しますか?"
+ "value" : "Brandy を「アプリケーション」フォルダに移動しますか?"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky를 애플리케이션 폴더로 옮기시겠습니까?"
+ "value" : "Brandy를 애플리케이션 폴더로 옮기시겠습니까?"
}
},
"nl" : {
@@ -18533,61 +14767,61 @@
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Czy chcesz przenieść Whisky do folderu Aplikacje?"
+ "value" : "Czy chcesz przenieść Brandy do folderu Aplikacje?"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Você gostaria de mover o Whisky para a pasta dos Aplicativos?"
+ "value" : "Você gostaria de mover o Brandy para a pasta dos Aplicativos?"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Gostaria de mover o Whisky para a pasta das Aplicações ?"
+ "value" : "Gostaria de mover o Brandy para a pasta das Aplicações ?"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Doriți să mutați Whisky în folder-ul dvs. Aplicații?"
+ "value" : "Doriți să mutați Brandy în folder-ul dvs. Aplicații?"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Вы хотите переместить Whisky в папку «Приложения»?"
+ "value" : "Вы хотите переместить Brandy в папку «Приложения»?"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Whisky'yi Uygulamalar klasörünüze taşımak ister misiniz?"
+ "value" : "Brandy'yi Uygulamalar klasörünüze taşımak ister misiniz?"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Хотіли б ви перемістити Whisky у вашу папку Застосунки?"
+ "value" : "Хотіли б ви перемістити Brandy у вашу папку Застосунки?"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bạn có muốn chuyển Whisky tới folder Applications?"
+ "value" : "Bạn có muốn chuyển Brandy tới folder Applications?"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "你想要将 Whisky 移动到你的应用程序文件夹吗?"
+ "value" : "你想要将 Brandy 移动到你的应用程序文件夹吗?"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "你希望將 Whisky 移動到你的應用程式資料夾嗎?"
+ "value" : "你希望將 Brandy 移動到你的應用程式資料夾嗎?"
}
}
}
@@ -18728,143 +14962,6 @@
}
}
},
- "tab.processes" : {
- "extractionState" : "stale",
- "localizations" : {
- "ar" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "العمليات قيد التشغيل"
- }
- },
- "cs" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Spuštěné procesy"
- }
- },
- "da" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Kørende processer"
- }
- },
- "de" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Laufende Prozesse"
- }
- },
- "en" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Running Processes"
- }
- },
- "es" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Procesos en ejecución"
- }
- },
- "fi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Käynnissä olevat prosessit"
- }
- },
- "fr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Processus en cours d'exécution"
- }
- },
- "it" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Processi in esecuzione"
- }
- },
- "ja" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "実行中のプロセス"
- }
- },
- "ko" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "실행중인 프로세스들"
- }
- },
- "nl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Lopende processen"
- }
- },
- "pl" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Uruchamianie procesów"
- }
- },
- "pt-BR" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Processos em Execução"
- }
- },
- "pt-PT" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Processos em Execução"
- }
- },
- "ro" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Procese Active"
- }
- },
- "ru" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Запущенные процессы"
- }
- },
- "tr" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Çalışan İşlemler"
- }
- },
- "uk" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Запущені процеси"
- }
- },
- "vi" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "Tiến trình đang chạy"
- }
- },
- "zh-Hans" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "运行中进程"
- }
- },
- "zh-Hant" : {
- "stringUnit" : {
- "state" : "translated",
- "value" : "正在執行進程"
- }
- }
- }
- },
"tab.programs" : {
"localizations" : {
"ar" : {
@@ -19006,7 +15103,7 @@
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "أنت تقوم بتشغيل WhiskyWine %@، ولكن %@ متوفر. هل ترغب في التحديث؟"
+ "value" : "أنت تقوم بتشغيل BrandyWine %@، ولكن %@ متوفر. هل ترغب في التحديث؟"
}
},
"cs" : {
@@ -19018,121 +15115,121 @@
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Du kører WhiskyWine %@, men %@ er tilgængelig. Ønsker du at opdatere?"
+ "value" : "Du kører BrandyWine %@, men %@ er tilgængelig. Ønsker du at opdatere?"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Sie verwenden WhiskyWine %@, die aktuelle Version ist %@. Möchten Sie WhiskyWine aktualisieren?"
+ "value" : "Sie verwenden BrandyWine %@, die aktuelle Version ist %@. Möchten Sie BrandyWine aktualisieren?"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "You are running WhiskyWine %@, but %@ is available. Would you like to update?"
+ "value" : "You are running BrandyWine %@, but %@ is available. Would you like to update?"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Estás ejecutando WhiskyWine %@, pero %@ está disponible. ¿Quieres actualizar?"
+ "value" : "Estás ejecutando BrandyWine %@, pero %@ está disponible. ¿Quieres actualizar?"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Käytössäsi on WhiskyWine %@, mutta %@ on saatavilla. Haluatko päivittää sen?"
+ "value" : "Käytössäsi on BrandyWine %@, mutta %@ on saatavilla. Haluatko päivittää sen?"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Vous utilisez WhiskyWine %@, mais %@ est disponible. Voulez-vous le mettre à jour ?"
+ "value" : "Vous utilisez BrandyWine %@, mais %@ est disponible. Voulez-vous le mettre à jour ?"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Stai utilizzando WhiskyWine %@, ma %@ è disponibile. Vuoi aggiornare?"
+ "value" : "Stai utilizzando BrandyWine %@, ma %@ è disponibile. Vuoi aggiornare?"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "現在 WhiskyWine %@ を使用していますが、 %@ を利用可能です。アップデートしますか?"
+ "value" : "現在 BrandyWine %@ を使用していますが、 %@ を利用可能です。アップデートしますか?"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine %@ 버전을 사용 중이지만 %@ 버전을 사용할 수 있습니다. 업데이트하시겠습니까?"
+ "value" : "BrandyWine %@ 버전을 사용 중이지만 %@ 버전을 사용할 수 있습니다. 업데이트하시겠습니까?"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "U gebruikt WhiskyWine %@, maar %@ is beschikbaar. Wilt u updaten?"
+ "value" : "U gebruikt BrandyWine %@, maar %@ is beschikbaar. Wilt u updaten?"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Używasz WhiskyWine %@, lecz nowsza wersja %@ jest dostępna. Czy chcesz zaktualizować?"
+ "value" : "Używasz BrandyWine %@, lecz nowsza wersja %@ jest dostępna. Czy chcesz zaktualizować?"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine %@ está sendo usada, mas %@ está disponível. Atualizar?"
+ "value" : "BrandyWine %@ está sendo usada, mas %@ está disponível. Atualizar?"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Está a usar o WhiskyWine %@, mas o %@ está disponível. Gostaria de atualizar?"
+ "value" : "Está a usar o BrandyWine %@, mas o %@ está disponível. Gostaria de atualizar?"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Dvs. rulați WhiskyWine %@, dar %@ este disponibil. Doriți să actualizați?"
+ "value" : "Dvs. rulați BrandyWine %@, dar %@ este disponibil. Doriți să actualizați?"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Вы используете WhiskyWine %@, но %@ доступен. Желаете обновить?"
+ "value" : "Вы используете BrandyWine %@, но %@ доступен. Желаете обновить?"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine %@ versiyonu kullanıyorsunuz ama %@ versiyonu mevcut. Güncellemek ister misiniz?"
+ "value" : "BrandyWine %@ versiyonu kullanıyorsunuz ama %@ versiyonu mevcut. Güncellemek ister misiniz?"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ви використовуєте WhiskyWine %@, але доступна версія %@. Оновити?"
+ "value" : "Ви використовуєте BrandyWine %@, але доступна версія %@. Оновити?"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Bạn đang sử dụng WhiskyWine phiên bản %@, nhưng phiên bản%@ đã được phát hành. Bạn có muốn cập nhật không?"
+ "value" : "Bạn đang sử dụng BrandyWine phiên bản %@, nhưng phiên bản%@ đã được phát hành. Bạn có muốn cập nhật không?"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "您正在运行 WhiskyWine %@,但 %@ 可用。您想要更新吗?"
+ "value" : "您正在运行 BrandyWine %@,但 %@ 可用。您想要更新吗?"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "您正在使用WhiskyWine %@, %@已可更新 請問是否要更新?"
+ "value" : "您正在使用BrandyWine %@, %@已可更新 請問是否要更新?"
}
}
}
@@ -19142,133 +15239,133 @@
"ar" : {
"stringUnit" : {
"state" : "translated",
- "value" : "إصدار جديد من WhiskyWine متوفر"
+ "value" : "إصدار جديد من BrandyWine متوفر"
}
},
"cs" : {
"stringUnit" : {
"state" : "translated",
- "value" : "K dispozici je nová verze WhiskyWine"
+ "value" : "K dispozici je nová verze BrandyWine"
}
},
"da" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Ny version af WhiskyWine tilgængelig"
+ "value" : "Ny version af BrandyWine tilgængelig"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Eine neue WhiskyWine Version ist verfügbar"
+ "value" : "Eine neue BrandyWine Version ist verfügbar"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "New Version of WhiskyWine Available"
+ "value" : "New Version of BrandyWine Available"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nueva versión de WhiskyWine disponible"
+ "value" : "Nueva versión de BrandyWine disponible"
}
},
"fi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Uusi WhiskyWine versio on saatavilla"
+ "value" : "Uusi BrandyWine versio on saatavilla"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nouvelle version de WhiskyWine disponible"
+ "value" : "Nouvelle version de BrandyWine disponible"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nuova versione di WhiskyWine disponibile"
+ "value" : "Nuova versione di BrandyWine disponibile"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine の新しいバージョンがあります"
+ "value" : "BrandyWine の新しいバージョンがあります"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
- "value" : "새로운 버전의 WhiskyWine를 사용할 수 있습니다"
+ "value" : "새로운 버전의 BrandyWine를 사용할 수 있습니다"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nieuwe WhiskyWine versie is beschikbaar"
+ "value" : "Nieuwe BrandyWine versie is beschikbaar"
}
},
"pl" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nowa wersja WhiskyWine jest dostępna"
+ "value" : "Nowa wersja BrandyWine jest dostępna"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nova versão do WhiskyWine disponível"
+ "value" : "Nova versão do BrandyWine disponível"
}
},
"pt-PT" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Nova versão do WhiskyWine disponível"
+ "value" : "Nova versão do BrandyWine disponível"
}
},
"ro" : {
"stringUnit" : {
"state" : "translated",
- "value" : "O Nouă Versiune WhiskyWine este Disponibilă"
+ "value" : "O Nouă Versiune BrandyWine este Disponibilă"
}
},
"ru" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Доступна новая версия WhiskyWine"
+ "value" : "Доступна новая версия BrandyWine"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
- "value" : "WhiskyWine'nin yeni versiyonu mevcut"
+ "value" : "BrandyWine'nin yeni versiyonu mevcut"
}
},
"uk" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Доступна нова версія WhiskyWine"
+ "value" : "Доступна нова версія BrandyWine"
}
},
"vi" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Phiên bản mới của WhiskyWine đã được phát hành"
+ "value" : "Phiên bản mới của BrandyWine đã được phát hành"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
- "value" : "有更新的 WhiskyWine 版本可用"
+ "value" : "有更新的 BrandyWine 版本可用"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
- "value" : "有新的WhiskyWine版本可供安裝"
+ "value" : "有新的BrandyWine版本可供安裝"
}
}
}
@@ -20367,6 +16464,16 @@
}
}
},
+ "winetricks.loadFailed" : {
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Couldn't Load Winetricks"
+ }
+ }
+ }
+ },
"winetricks.table.description" : {
"localizations" : {
"ar" : {
diff --git a/Whisky/Utils/ErrorAlert.swift b/Whisky/Utils/ErrorAlert.swift
new file mode 100644
index 000000000..e171cc049
--- /dev/null
+++ b/Whisky/Utils/ErrorAlert.swift
@@ -0,0 +1,36 @@
+//
+// ErrorAlert.swift
+// Whisky
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import AppKit
+
+/// Tells the person that something they asked for failed, instead of only logging it
+@MainActor
+enum ErrorAlert {
+ static func show(_ title: String, error: Error) {
+ show(title, message: error.localizedDescription)
+ }
+
+ static func show(_ title: String, message: String) {
+ let alert = NSAlert()
+ alert.messageText = title
+ alert.informativeText = message
+ alert.alertStyle = .critical
+ alert.addButton(withTitle: String(localized: "button.ok"))
+ alert.runModal()
+ }
+}
diff --git a/Whisky/Utils/ProgramShortcut.swift b/Whisky/Utils/ProgramShortcut.swift
index 9b581ce04..16897659b 100644
--- a/Whisky/Utils/ProgramShortcut.swift
+++ b/Whisky/Utils/ProgramShortcut.swift
@@ -31,15 +31,15 @@ class ProgramShortcut {
// First create shell script
let script = """
#!/bin/bash
- \(program.generateTerminalCommand())
+ \(try program.generateTerminalCommand())
"""
let scriptUrl = macos.appending(path: "launch")
try script.write(to: scriptUrl,
atomically: false,
encoding: .utf8)
- // Make shell script runable
- try FileManager.default.setAttributes([.posixPermissions: 0o777],
+ // Executable by everyone, writable only by its owner
+ try FileManager.default.setAttributes([.posixPermissions: 0o755],
ofItemAtPath: scriptUrl.path(percentEncoded: false))
// Create Info.plist (set category for Game mode)
@@ -77,7 +77,7 @@ class ProgramShortcut {
options: NSWorkspace.IconCreationOptions())
NSWorkspace.shared.activateFileViewerSelecting([app])
} catch {
- print(error)
+ await ErrorAlert.show(String(localized: "error.createShortcut"), error: error)
}
}
}
diff --git a/Whisky/Utils/WhiskyCmd.swift b/Whisky/Utils/WhiskyCmd.swift
index bf43c1da1..0870db676 100644
--- a/Whisky/Utils/WhiskyCmd.swift
+++ b/Whisky/Utils/WhiskyCmd.swift
@@ -26,7 +26,7 @@ class WhiskyCmd {
if let whiskyCmdURL = whiskyCmdURL {
// swiftlint:disable line_length
let script = """
- do shell script "ln -fs \(whiskyCmdURL.path(percentEncoded: false)) /usr/local/bin/whisky" with administrator privileges
+ do shell script "ln -fs \(whiskyCmdURL.path(percentEncoded: false)) /usr/local/bin/brandy" with administrator privileges
"""
// swiftlint:enable line_length
@@ -36,7 +36,6 @@ class WhiskyCmd {
appleScript.executeAndReturnError(&error)
if let error = error {
- print(error)
if let description = error["NSAppleScriptErrorMessage"] as? String {
await MainActor.run {
let alert = NSAlert()
diff --git a/Whisky/Utils/WhiskyWineUpdate.swift b/Whisky/Utils/WhiskyWineUpdate.swift
new file mode 100644
index 000000000..aa57dc3ed
--- /dev/null
+++ b/Whisky/Utils/WhiskyWineUpdate.swift
@@ -0,0 +1,48 @@
+//
+// WhiskyWineUpdate.swift
+// Whisky
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import AppKit
+import os
+import SemanticVersion
+import WhiskyKit
+
+enum WhiskyWineUpdate {
+ /// Checks for a newer runtime and asks whether to install it; `true` when the person agreed
+ @MainActor
+ static func offerIfAvailable() async -> Bool {
+ let remoteVersion: SemanticVersion?
+ do {
+ remoteVersion = try await WhiskyWineInstaller.availableUpdate()
+ } catch {
+ Logger.wineKit.error("Failed to check for a WhiskyWine update: \(error)")
+ return false
+ }
+ guard let remoteVersion else { return false }
+
+ let alert = NSAlert()
+ alert.messageText = String(localized: "update.whiskywine.title")
+ alert.informativeText = String(format: String(localized: "update.whiskywine.description"),
+ String(WhiskyWineInstaller.whiskyWineVersion()
+ ?? SemanticVersion(0, 0, 0)),
+ String(remoteVersion))
+ alert.alertStyle = .warning
+ alert.addButton(withTitle: String(localized: "update.whiskywine.update"))
+ alert.addButton(withTitle: String(localized: "button.removeAlert.cancel"))
+ return alert.runModal() == .alertFirstButtonReturn
+ }
+}
diff --git a/Whisky/Utils/Winetricks.swift b/Whisky/Utils/Winetricks.swift
index 714fe67e3..9b2c8aee1 100644
--- a/Whisky/Utils/Winetricks.swift
+++ b/Whisky/Utils/Winetricks.swift
@@ -45,52 +45,30 @@ class Winetricks {
static let winetricksURL: URL = WhiskyWineInstaller.libraryFolder
.appending(path: "winetricks")
- static func runCommand(command: String, bottle: Bottle) async {
+ @MainActor
+ static func runCommand(command: String, bottle: Bottle) {
guard let resourcesURL = Bundle.main.url(forResource: "cabextract", withExtension: nil)?
.deletingLastPathComponent() else { return }
- // swiftlint:disable:next line_length
- let winetricksCmd = #"PATH=\"\#(WhiskyWineInstaller.binFolder.path):\#(resourcesURL.path(percentEncoded: false)):$PATH\" WINE=wine64 WINEPREFIX=\"\#(bottle.url.path)\" \"\#(winetricksURL.path(percentEncoded: false))\" \#(command)"#
-
- let script = """
- tell application "Terminal"
- activate
- do script "\(winetricksCmd)"
- end tell
- """
-
- var error: NSDictionary?
- if let appleScript = NSAppleScript(source: script) {
- appleScript.executeAndReturnError(&error)
-
- if let error = error {
- print(error)
- if let description = error["NSAppleScriptErrorMessage"] as? String {
- await MainActor.run {
- let alert = NSAlert()
- alert.messageText = String(localized: "alert.message")
- alert.informativeText = String(localized: "alert.info")
- + " \(command): "
- + description
- alert.alertStyle = .critical
- alert.addButton(withTitle: String(localized: "button.ok"))
- alert.runModal()
- }
- }
- }
+ let path = WhiskyWineInstaller.binFolder.path(percentEncoded: false) + ":"
+ + resourcesURL.path(percentEncoded: false)
+ let winetricks = [
+ "PATH=\(path.shellQuoted):\"$PATH\"",
+ "WINE=wine",
+ "WINEPREFIX=\(bottle.url.path(percentEncoded: false).shellQuoted)",
+ winetricksURL.path(percentEncoded: false).shellQuoted,
+ command.shellQuoted
+ ]
+ do {
+ try Terminal.run(winetricks.joined(separator: " "))
+ } catch {
+ let message = "Winetricks \(command): \(error.localizedDescription)"
+ ErrorAlert.show(String(localized: "alert.message"), message: message)
}
}
- static func parseVerbs() async -> [WinetricksCategory] {
- // Grab the verbs file
+ static func parseVerbs() throws -> [WinetricksCategory] {
let verbsURL = WhiskyWineInstaller.libraryFolder.appending(path: "verbs.txt")
- let verbs: String = await { () async -> String in
- do {
- let (data, _) = try await URLSession.shared.data(from: verbsURL)
- return String(data: data, encoding: .utf8) ?? String()
- } catch {
- return String()
- }
- }()
+ let verbs = try String(contentsOf: verbsURL, encoding: .utf8)
// Read the file line by line
let lines = verbs.components(separatedBy: "\n")
diff --git a/Whisky/View Models/BottleVM.swift b/Whisky/View Models/BottleVM.swift
index c15ed5777..32aedf931 100644
--- a/Whisky/View Models/BottleVM.swift
+++ b/Whisky/View Models/BottleVM.swift
@@ -18,26 +18,8 @@
import Foundation
import os
-import SemanticVersion
import WhiskyKit
-enum BottleCreationError: LocalizedError {
- case whiskyWineNotInstalled
- case unsupportedLocation(URL)
- case prefixNotCreated(wineOutput: String)
-
- var errorDescription: String? {
- switch self {
- case .whiskyWineNotInstalled:
- return String(localized: "create.error.whiskyWineNotInstalled")
- case .unsupportedLocation(let url):
- return String(localized: "create.error.unsupportedLocation.\(url.prettyPath())")
- case .prefixNotCreated(let wineOutput):
- return String(localized: "create.error.prefixNotCreated.\(wineOutput.suffix(1000).description)")
- }
- }
-}
-
struct BottleCreationFailure: Identifiable {
let id = UUID()
let bottleName: String
@@ -58,6 +40,25 @@ final class BottleVM: ObservableObject, @unchecked Sendable {
func loadBottles() {
let bottlesInCreation = bottles.filter { $0.inFlight && !bottlesList.paths.contains($0.url) }
bottles = bottlesList.loadBottles() + bottlesInCreation
+
+ for bottle in bottles where bottle.isAvailable && bottle.settings.hasLegacyWindowsVersion {
+ switchToSupportedWindowsVersion(bottle)
+ }
+ }
+
+ /// Bottles set to Windows XP–8.1 now read as Windows 10; this makes their Wine prefix match
+ @MainActor
+ private func switchToSupportedWindowsVersion(_ bottle: Bottle) {
+ let windowsVersion = bottle.settings.windowsVersion
+ Task {
+ do {
+ try await Wine.changeWinVersion(bottle: bottle, win: windowsVersion)
+ bottle.settings.windowsVersion = windowsVersion
+ } catch {
+ let name = bottle.settings.name
+ Logger.wineKit.error("Failed to switch `\(name)` to \(windowsVersion.pretty()): \(error)")
+ }
+ }
}
func countActive() -> Int {
@@ -70,54 +71,29 @@ final class BottleVM: ObservableObject, @unchecked Sendable {
Task {
do {
- try await setUpBottle(at: newBottleDir, name: bottleName, winVersion: winVersion)
+ try await createBottle(at: newBottleDir, name: bottleName, winVersion: winVersion)
} catch {
- Logger.wineKit.error("Failed to create bottle `\(bottleName)`: \(error)")
- discardBottle(at: newBottleDir)
bottleCreationFailure = BottleCreationFailure(bottleName: bottleName, error: error)
}
}
return newBottleDir
}
+ /// Creates a bottle, shown in the list while Wine sets it up, and adds it to the bottle list
@MainActor
- private func setUpBottle(at bottleDir: URL, name: String, winVersion: WinVersion) async throws {
- guard WhiskyWineInstaller.isWhiskyWineInstalled() else {
- throw BottleCreationError.whiskyWineNotInstalled
- }
- // Wine drops trailing dots when mapping Unix paths to Windows ones, so the prefix cannot load
- guard !bottleDir.pathComponents.contains(where: { $0.hasSuffix(".") }) else {
- throw BottleCreationError.unsupportedLocation(bottleDir)
- }
-
- try FileManager.default.createDirectory(at: bottleDir, withIntermediateDirectories: true)
- let bottle = Bottle(bottleUrl: bottleDir, inFlight: true)
- bottles.append(bottle)
-
- bottle.settings.windowsVersion = winVersion
- bottle.settings.name = name
- let wineOutput = try await Wine.changeWinVersion(bottle: bottle, win: winVersion)
- guard FileManager.default.fileExists(atPath: bottleDir.appending(path: "drive_c").path(percentEncoded: false))
- else {
- throw BottleCreationError.prefixNotCreated(wineOutput: wineOutput)
- }
-
- let wineVer = try await Wine.wineVersion()
- bottle.settings.wineVersion = SemanticVersion(wineVer) ?? SemanticVersion(0, 0, 0)
- bottlesList.paths.append(bottleDir)
- loadBottles()
- }
-
- @MainActor
- private func discardBottle(at bottleDir: URL) {
- bottles.removeAll { $0.url == bottleDir }
- guard FileManager.default.fileExists(atPath: bottleDir.path(percentEncoded: false)) else { return }
+ @discardableResult
+ func createBottle(at bottleDir: URL, name: String, winVersion: WinVersion) async throws -> Bottle {
do {
- try FileManager.default.removeItem(at: bottleDir)
+ let bottle = try await Bottle.create(at: bottleDir, name: name, windowsVersion: winVersion) { bottle in
+ bottles.append(bottle)
+ }
+ bottlesList.paths.append(bottleDir)
+ loadBottles()
+ return bottles.first { $0.url == bottleDir } ?? bottle
} catch {
- Logger.wineKit.error(
- "Failed to remove incomplete bottle `\(bottleDir.path(percentEncoded: false))`: \(error)"
- )
+ Logger.wineKit.error("Failed to create bottle `\(name)`: \(error)")
+ bottles.removeAll { $0.url == bottleDir }
+ throw error
}
}
}
diff --git a/Whisky/Views/Bottle/BottleCreationView.swift b/Whisky/Views/Bottle/BottleCreationView.swift
index f4145207b..34e8e7ce8 100644
--- a/Whisky/Views/Bottle/BottleCreationView.swift
+++ b/Whisky/Views/Bottle/BottleCreationView.swift
@@ -24,8 +24,7 @@ struct BottleCreationView: View {
@State private var newBottleName: String = ""
@State private var newBottleVersion: WinVersion = .win10
- @State private var newBottleURL: URL = UserDefaults.standard.url(forKey: BottleData.defaultBottleLocationKey)
- ?? BottleData.defaultBottleDir
+ @State private var newBottleURL: URL = BottleData.preferredBottleLocation
@State private var nameValid: Bool = false
@Environment(\.dismiss) private var dismiss
diff --git a/Whisky/Views/Bottle/BottleListEntry.swift b/Whisky/Views/Bottle/BottleListEntry.swift
index 8a538e00e..cff68828f 100644
--- a/Whisky/Views/Bottle/BottleListEntry.swift
+++ b/Whisky/Views/Bottle/BottleListEntry.swift
@@ -29,8 +29,9 @@ struct BottleListEntry: View {
@State private var name: String = ""
var body: some View {
- Text(name)
- .opacity(bottle.isAvailable ? 1.0 : 0.5)
+ BottleRowLabel(name: name, subtitle: subtitle,
+ systemImage: bottle.isAvailable ? "wineglass" : "exclamationmark.triangle")
+ .foregroundStyle(bottle.isAvailable ? .primary : .secondary)
.onChange(of: refresh, initial: true) {
name = bottle.settings.name
}
@@ -82,7 +83,11 @@ struct BottleListEntry: View {
if result == .OK {
if let url = panel.url {
Task.detached(priority: .background) {
- bottle.exportAsArchive(destination: url)
+ do {
+ try bottle.exportAsArchive(destination: url)
+ } catch {
+ await ErrorAlert.show(String(localized: "error.exportBottle"), error: error)
+ }
}
}
}
@@ -99,6 +104,12 @@ struct BottleListEntry: View {
}
}
+ private var subtitle: String {
+ bottle.isAvailable
+ ? bottle.settings.windowsVersion.pretty()
+ : String(localized: "main.bottleMissing")
+ }
+
func showRemoveAlert(bottle: Bottle) {
let checkbox = NSButton(checkboxWithTitle: String(localized: "button.removeAlert.checkbox"),
target: self, action: nil)
@@ -128,6 +139,29 @@ struct BottleListEntry: View {
}
}
+/// A sidebar row: the bottle's name above a short secondary line
+struct BottleRowLabel: View {
+ let name: String
+ let subtitle: String
+ let systemImage: String
+
+ var body: some View {
+ Label {
+ VStack(alignment: .leading, spacing: 1) {
+ Text(name)
+ .lineLimit(1)
+ Text(subtitle)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ }
+ } icon: {
+ Image(systemName: systemImage)
+ }
+ .padding(.vertical, 2)
+ }
+}
+
#Preview {
BottleListEntry(
bottle: Bottle(bottleUrl: URL(filePath: "")),
diff --git a/Whisky/Views/Bottle/BottleView.swift b/Whisky/Views/Bottle/BottleView.swift
index 25e34f40f..b664b9a9d 100644
--- a/Whisky/Views/Bottle/BottleView.swift
+++ b/Whisky/Views/Bottle/BottleView.swift
@@ -23,7 +23,6 @@ import WhiskyKit
enum BottleStage {
case config
case programs
- case processes
}
struct BottleView: View {
@@ -32,90 +31,62 @@ struct BottleView: View {
@State private var programLoading: Bool = false
@State private var showWinetricksSheet: Bool = false
- private let gridLayout = [GridItem(.adaptive(minimum: 100, maximum: .infinity))]
-
var body: some View {
NavigationStack(path: $path) {
- ScrollView {
- LazyVGrid(columns: gridLayout, alignment: .center) {
- ForEach(bottle.pinnedPrograms, id: \.id) { pinnedProgram in
- PinView(
- bottle: bottle, program: pinnedProgram.program, pin: pinnedProgram.pin, path: $path
- )
- }
- PinAddView(bottle: bottle)
+ Form {
+ Section {
+ BottleHeaderView(bottle: bottle)
}
- .padding()
- Form {
+ Section("bottle.pinned") {
+ PinnedProgramsGrid(bottle: bottle, path: $path)
+ }
+ Section {
NavigationLink(value: BottleStage.programs) {
- Label("tab.programs", systemImage: "list.bullet")
+ Label("tab.programs", systemImage: "square.grid.2x2")
+ .badge(bottle.programs.count)
}
NavigationLink(value: BottleStage.config) {
- Label("tab.config", systemImage: "gearshape")
+ Label {
+ Text("tab.config")
+ Text(bottle.settings.configurationSummary)
+ } icon: {
+ Image(systemName: "gearshape")
+ }
}
-// NavigationLink(value: BottleStage.processes) {
-// Label("tab.processes", systemImage: "hockey.puck.circle")
-// }
}
- .formStyle(.grouped)
- .scrollDisabled(true)
}
- .bottomBar {
- HStack {
- Spacer()
- Button("button.cDrive") {
- bottle.openCDrive()
- }
- Button("button.terminal") {
- bottle.openTerminal()
- }
- Button("button.winetricks") {
- showWinetricksSheet.toggle()
- }
- Button("button.run") {
- let panel = NSOpenPanel()
- panel.allowsMultipleSelection = false
- panel.canChooseDirectories = false
- panel.canChooseFiles = true
- panel.allowedContentTypes = [UTType.exe,
- UTType(exportedAs: "com.microsoft.msi-installer"),
- UTType(exportedAs: "com.microsoft.bat")]
- panel.directoryURL = bottle.url.appending(path: "drive_c")
- panel.begin { result in
- programLoading = true
- Task(priority: .userInitiated) {
- if result == .OK {
- if let url = panel.urls.first {
- do {
- if url.pathExtension == "bat" {
- try await Wine.runBatchFile(url: url, bottle: bottle)
- } else {
- try await Wine.runProgram(at: url, bottle: bottle)
- }
- } catch {
- print("Failed to run external program: \(error)")
- }
- programLoading = false
- }
- } else {
- programLoading = false
- }
- updateStartMenu()
- }
- }
- }
- .disabled(programLoading)
+ .formStyle(.grouped)
+ .toolbar {
+ ToolbarItemGroup(placement: .primaryAction) {
if programLoading {
- Spacer()
- .frame(width: 10)
ProgressView()
.controlSize(.small)
}
+ Button("button.run", systemImage: "play.fill") {
+ runExternalProgram()
+ }
+ .disabled(programLoading)
+ .help("button.run")
+ Menu("bottle.more", systemImage: "ellipsis.circle") {
+ Button("button.cDrive", systemImage: "internaldrive") {
+ bottle.openCDrive()
+ }
+ Button("button.terminal", systemImage: "terminal") {
+ bottle.openTerminal()
+ }
+ Button("button.winetricks", systemImage: "wand.and.stars") {
+ showWinetricksSheet.toggle()
+ }
+ Divider()
+ Button("button.showInFinder", systemImage: "folder") {
+ NSWorkspace.shared.activateFileViewerSelecting([bottle.url])
+ }
+ }
+ .help("bottle.more")
}
- .padding()
}
- .onAppear {
- updateStartMenu()
+ .task {
+ await updateStartMenu()
}
.disabled(!bottle.isAvailable)
.navigationTitle(bottle.settings.name)
@@ -135,8 +106,6 @@ struct BottleView: View {
ProgramsView(
bottle: bottle, path: $path
)
- case .processes:
- RunningProcessesView(bottle: bottle)
}
}
.navigationDestination(for: Program.self) { program in
@@ -145,8 +114,32 @@ struct BottleView: View {
}
}
- private func updateStartMenu() {
- bottle.updateInstalledPrograms()
+ private func runExternalProgram() {
+ let panel = NSOpenPanel()
+ panel.allowsMultipleSelection = false
+ panel.canChooseDirectories = false
+ panel.canChooseFiles = true
+ panel.allowedContentTypes = [UTType.exe,
+ UTType(exportedAs: "com.microsoft.msi-installer"),
+ UTType(exportedAs: "com.microsoft.bat")]
+ panel.directoryURL = bottle.url.appending(path: "drive_c")
+ panel.begin { result in
+ guard result == .OK, let url = panel.urls.first else { return }
+ programLoading = true
+ Task(priority: .userInitiated) {
+ do {
+ try await Wine.runFile(at: url, bottle: bottle)
+ } catch {
+ ErrorAlert.show(String(localized: "error.runProgram"), error: error)
+ }
+ programLoading = false
+ await updateStartMenu()
+ }
+ }
+ }
+
+ private func updateStartMenu() async {
+ await bottle.updateInstalledPrograms()
let startMenuPrograms = bottle.getStartMenuPrograms()
for startMenuProgram in startMenuPrograms {
@@ -154,7 +147,7 @@ struct BottleView: View {
// For some godforsaken reason "foo/bar" != "foo/Bar" so...
program.url.path().caseInsensitiveCompare(startMenuProgram.url.path()) == .orderedSame {
program.pinned = true
- guard !bottle.settings.pins.contains(where: { $0.url == program.url }) else { return }
+ guard !bottle.settings.pins.contains(where: { $0.url == program.url }) else { continue }
bottle.settings.pins.append(PinnedProgram(
name: program.url.deletingPathExtension().lastPathComponent,
url: program.url
@@ -163,3 +156,27 @@ struct BottleView: View {
}
}
}
+
+private struct BottleHeaderView: View {
+ @ObservedObject var bottle: Bottle
+
+ var body: some View {
+ HStack(spacing: 14) {
+ Image(systemName: "wineglass.fill")
+ .font(.system(size: 26, weight: .medium))
+ .foregroundStyle(.white)
+ .frame(width: 52, height: 52)
+ .background(.tint, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
+ VStack(alignment: .leading, spacing: 3) {
+ Text(bottle.settings.name)
+ .font(.title2)
+ .fontWeight(.semibold)
+ .lineLimit(1)
+ Text(bottle.summary)
+ .foregroundStyle(.secondary)
+ }
+ Spacer(minLength: 0)
+ }
+ .padding(.vertical, 4)
+ }
+}
diff --git a/Whisky/Views/Bottle/ConfigView.swift b/Whisky/Views/Bottle/ConfigView.swift
index ca5bd801c..65c8e677f 100644
--- a/Whisky/Views/Bottle/ConfigView.swift
+++ b/Whisky/Views/Bottle/ConfigView.swift
@@ -18,6 +18,7 @@
import SwiftUI
import WhiskyKit
+import os
enum LoadingState {
case loading
@@ -27,102 +28,78 @@ enum LoadingState {
}
struct ConfigView: View {
+ /// Wine's DPI when a bottle has never had it changed
+ private static let defaultDPI = 96
+
@ObservedObject var bottle: Bottle
@State private var buildVersion: Int = 0
@State private var retinaMode: Bool = false
- @State private var dpiConfig: Int = 96
+ @State private var dpiConfig: Int = defaultDPI
@State private var winVersionLoadingState: LoadingState = .loading
@State private var buildVersionLoadingState: LoadingState = .loading
@State private var retinaModeLoadingState: LoadingState = .loading
@State private var dpiConfigLoadingState: LoadingState = .loading
@State private var dpiSheetPresented: Bool = false
- @AppStorage("wineSectionExpanded") private var wineSectionExpanded: Bool = true
- @AppStorage("dxvkSectionExpanded") private var dxvkSectionExpanded: Bool = true
- @AppStorage("metalSectionExpanded") private var metalSectionExpanded: Bool = true
var body: some View {
Form {
- Section("config.title.wine", isExpanded: $wineSectionExpanded) {
- SettingItemView(title: "config.winVersion", loadingState: winVersionLoadingState) {
+ Section("config.title.wine") {
+ SettingItemView(title: "config.winVersion", description: "config.winVersion.info",
+ loadingState: winVersionLoadingState) {
Picker("config.winVersion", selection: $bottle.settings.windowsVersion) {
ForEach(WinVersion.allCases.reversed(), id: \.self) {
Text($0.pretty())
}
}
}
- SettingItemView(title: "config.buildVersion", loadingState: buildVersionLoadingState) {
+ SettingItemView(title: "config.buildVersion", description: "config.buildVersion.info",
+ loadingState: buildVersionLoadingState) {
TextField("config.buildVersion", value: $buildVersion, formatter: NumberFormatter())
.multilineTextAlignment(.trailing)
- .textFieldStyle(PlainTextFieldStyle())
+ .textFieldStyle(.roundedBorder)
+ .frame(width: 90)
.onSubmit {
- buildVersionLoadingState = .modifying
- Task(priority: .userInitiated) {
- do {
- try await Wine.changeBuildVersion(bottle: bottle, version: buildVersion)
- buildVersionLoadingState = .success
- } catch {
- print("Failed to change build version")
- buildVersionLoadingState = .failed
- }
- }
+ changeBuildVersion()
}
}
- SettingItemView(title: "config.retinaMode", loadingState: retinaModeLoadingState) {
+ SettingItemView(title: "config.retinaMode", description: "config.retinaMode.info",
+ loadingState: retinaModeLoadingState) {
Toggle("config.retinaMode", isOn: $retinaMode)
- .onChange(of: retinaMode, { _, newValue in
- Task(priority: .userInitiated) {
- retinaModeLoadingState = .modifying
- do {
- try await Wine.changeRetinaMode(bottle: bottle, retinaMode: newValue)
- retinaModeLoadingState = .success
- } catch {
- print("Failed to change build version")
- retinaModeLoadingState = .failed
- }
- }
- })
- }
- Picker("config.enhancedSync", selection: $bottle.settings.enhancedSync) {
- Text("config.enhancedSync.none").tag(EnhancedSync.none)
- Text("config.enhacnedSync.esync").tag(EnhancedSync.esync)
- Text("config.enhacnedSync.msync").tag(EnhancedSync.msync)
+ .toggleStyle(.switch)
+ .onChange(of: retinaMode) { _, newValue in
+ changeRetinaMode(newValue)
+ }
}
- SettingItemView(title: "config.dpi", loadingState: dpiConfigLoadingState) {
- Button("config.inspect") {
- dpiSheetPresented = true
- }
- .sheet(isPresented: $dpiSheetPresented) {
- DPIConfigSheetView(
- dpiConfig: $dpiConfig,
- isRetinaMode: $retinaMode,
- presented: $dpiSheetPresented
- )
+ SettingItemView(title: "config.dpi", description: "config.dpi.info",
+ loadingState: dpiConfigLoadingState) {
+ HStack {
+ Text("config.dpi.value.\(dpiConfig)")
+ .foregroundStyle(.secondary)
+ .monospacedDigit()
+ Button("config.inspect") {
+ dpiSheetPresented = true
+ }
}
}
if #available(macOS 15, *) {
Toggle(isOn: $bottle.settings.avxEnabled) {
- VStack(alignment: .leading) {
- Text("config.avx")
- if bottle.settings.avxEnabled {
- HStack(alignment: .firstTextBaseline) {
- Image(systemName: "exclamationmark.triangle.fill")
- .symbolRenderingMode(.multicolor)
- .font(.subheadline)
- Text("config.avx.warning")
- .fontWeight(.light)
- .font(.subheadline)
- }
- }
+ Text("config.avx")
+ Text("config.avx.info")
+ if bottle.settings.avxEnabled {
+ Label("config.avx.warning", systemImage: "exclamationmark.triangle.fill")
+ .symbolRenderingMode(.multicolor)
}
}
}
}
- Section("config.title.dxvk", isExpanded: $dxvkSectionExpanded) {
+ Section("config.title.dxvk") {
Toggle(isOn: $bottle.settings.dxvk) {
Text("config.dxvk")
+ Text("config.dxvk.info")
}
Toggle(isOn: $bottle.settings.dxvkAsync) {
Text("config.dxvk.async")
+ Text("config.dxvk.async.info")
}
.disabled(!bottle.settings.dxvk)
Picker("config.dxvkHud", selection: $bottle.settings.dxvkHud) {
@@ -133,9 +110,10 @@ struct ConfigView: View {
}
.disabled(!bottle.settings.dxvk)
}
- Section("config.title.metal", isExpanded: $metalSectionExpanded) {
+ Section("config.title.metal") {
Toggle(isOn: $bottle.settings.metalHud) {
Text("config.metalHud")
+ Text("config.metalHud.info")
}
Toggle(isOn: $bottle.settings.metalTrace) {
Text("config.metalTrace")
@@ -151,45 +129,28 @@ struct ConfigView: View {
}
}
}
- }
- .formStyle(.grouped)
- .animation(.whiskyDefault, value: wineSectionExpanded)
- .animation(.whiskyDefault, value: dxvkSectionExpanded)
- .animation(.whiskyDefault, value: metalSectionExpanded)
- .bottomBar {
- HStack {
- Spacer()
- Button("config.controlPanel") {
- Task(priority: .userInitiated) {
- do {
- try await Wine.control(bottle: bottle)
- } catch {
- print("Failed to launch control")
- }
- }
+ Section("config.title.tools") {
+ WineToolRow(title: "config.tool.controlPanel", description: "config.tool.controlPanel.info",
+ systemImage: "slider.horizontal.3") {
+ try await Wine.control(bottle: bottle)
}
- Button("config.regedit") {
- Task(priority: .userInitiated) {
- do {
- try await Wine.regedit(bottle: bottle)
- } catch {
- print("Failed to launch regedit")
- }
- }
+ WineToolRow(title: "config.tool.regedit", description: "config.tool.regedit.info",
+ systemImage: "list.bullet.indent") {
+ try await Wine.regedit(bottle: bottle)
}
- Button("config.winecfg") {
- Task(priority: .userInitiated) {
- do {
- try await Wine.cfg(bottle: bottle)
- } catch {
- print("Failed to launch winecfg")
- }
- }
+ WineToolRow(title: "config.tool.winecfg", description: "config.tool.winecfg.info",
+ systemImage: "wrench.and.screwdriver") {
+ try await Wine.cfg(bottle: bottle)
}
}
- .padding()
}
+ .formStyle(.grouped)
.navigationTitle("tab.config")
+ .sheet(isPresented: $dpiSheetPresented) {
+ DPIConfigSheetView(dpiConfig: dpiConfig, isRetinaMode: retinaMode) { newDPI in
+ changeDPI(newDPI)
+ }
+ }
.onAppear {
winVersionLoadingState = .success
@@ -200,19 +161,20 @@ struct ConfigView: View {
retinaMode = try await Wine.retinaMode(bottle: bottle)
retinaModeLoadingState = .success
} catch {
- print(error)
+ Logger.wineKit.error("Bottle configuration: \(error)")
retinaModeLoadingState = .failed
}
}
Task(priority: .userInitiated) {
do {
- dpiConfig = try await Wine.dpiResolution(bottle: bottle) ?? 0
- dpiConfigLoadingState = .success
+ // Earlier versions could store 0, which is not a usable DPI
+ let storedDPI = try await Wine.dpiResolution(bottle: bottle).flatMap { $0 > 0 ? $0 : nil }
+ dpiConfig = storedDPI ?? Self.defaultDPI
} catch {
- print(error)
- // If DPI has not yet been edited, there will be no registry entry
- dpiConfigLoadingState = .success
+ // A bottle whose DPI was never changed has no registry entry
+ Logger.wineKit.error("Bottle configuration: \(error)")
}
+ dpiConfigLoadingState = .success
}
}
.onChange(of: bottle.settings.windowsVersion) { _, newValue in
@@ -226,24 +188,50 @@ struct ConfigView: View {
bottle.settings.windowsVersion = newValue
loadBuildName()
} catch {
- print(error)
+ Logger.wineKit.error("Bottle configuration: \(error)")
winVersionLoadingState = .failed
}
}
}
}
- .onChange(of: dpiConfig) {
- if dpiConfigLoadingState == .success {
- Task(priority: .userInitiated) {
- dpiConfigLoadingState = .modifying
- do {
- try await Wine.changeDpiResolution(bottle: bottle, dpi: dpiConfig)
- dpiConfigLoadingState = .success
- } catch {
- print(error)
- dpiConfigLoadingState = .failed
- }
- }
+ }
+
+ private func changeBuildVersion() {
+ buildVersionLoadingState = .modifying
+ Task(priority: .userInitiated) {
+ do {
+ try await Wine.changeBuildVersion(bottle: bottle, version: buildVersion)
+ buildVersionLoadingState = .success
+ } catch {
+ Logger.wineKit.error("Failed to change build version: \(error)")
+ buildVersionLoadingState = .failed
+ }
+ }
+ }
+
+ private func changeRetinaMode(_ enabled: Bool) {
+ Task(priority: .userInitiated) {
+ retinaModeLoadingState = .modifying
+ do {
+ try await Wine.changeRetinaMode(bottle: bottle, retinaMode: enabled)
+ retinaModeLoadingState = .success
+ } catch {
+ Logger.wineKit.error("Failed to change Retina mode: \(error)")
+ retinaModeLoadingState = .failed
+ }
+ }
+ }
+
+ private func changeDPI(_ dpi: Int) {
+ Task(priority: .userInitiated) {
+ dpiConfigLoadingState = .modifying
+ do {
+ try await Wine.changeDpiResolution(bottle: bottle, dpi: dpi)
+ dpiConfig = dpi
+ dpiConfigLoadingState = .success
+ } catch {
+ Logger.wineKit.error("Bottle configuration: \(error)")
+ dpiConfigLoadingState = .failed
}
}
}
@@ -259,25 +247,53 @@ struct ConfigView: View {
buildVersionLoadingState = .success
} catch {
- print(error)
+ Logger.wineKit.error("Bottle configuration: \(error)")
buildVersionLoadingState = .failed
}
}
}
}
+/// A Wine utility that opens in its own window
+private struct WineToolRow: View {
+ let title: LocalizedStringKey
+ let description: LocalizedStringKey
+ let systemImage: String
+ let open: () async throws -> Void
+
+ var body: some View {
+ LabeledContent {
+ Button("config.tool.open") {
+ Task(priority: .userInitiated) {
+ do {
+ try await open()
+ } catch {
+ ErrorAlert.show(String(localized: "error.openTool"), error: error)
+ }
+ }
+ }
+ } label: {
+ Label {
+ Text(title)
+ Text(description)
+ } icon: {
+ Image(systemName: systemImage)
+ }
+ }
+ }
+}
+
struct DPIConfigSheetView: View {
- @Binding var dpiConfig: Int
- @Binding var isRetinaMode: Bool
- @Binding var presented: Bool
- @State var stagedChanges: Float
- @FocusState var textFocused: Bool
+ let isRetinaMode: Bool
+ let save: (Int) -> Void
+ @State private var stagedChanges: Float
+ @FocusState private var textFocused: Bool
+ @Environment(\.dismiss) private var dismiss
- init(dpiConfig: Binding, isRetinaMode: Binding, presented: Binding) {
- self._dpiConfig = dpiConfig
- self._isRetinaMode = isRetinaMode
- self._presented = presented
- self.stagedChanges = Float(dpiConfig.wrappedValue)
+ init(dpiConfig: Int, isRetinaMode: Bool, save: @escaping (Int) -> Void) {
+ self.isRetinaMode = isRetinaMode
+ self.save = save
+ self._stagedChanges = State(initialValue: Float(dpiConfig))
}
var body: some View {
@@ -316,12 +332,12 @@ struct DPIConfigSheetView: View {
HStack {
Spacer()
Button("create.cancel") {
- presented = false
+ dismiss()
}
.keyboardShortcut(.cancelAction)
Button("button.ok") {
- dpiConfig = Int(stagedChanges)
- presented = false
+ save(Int(stagedChanges))
+ dismiss()
}
.keyboardShortcut(.defaultAction)
}
@@ -332,7 +348,8 @@ struct DPIConfigSheetView: View {
}
struct SettingItemView: View {
- let title: String.LocalizationValue
+ let title: LocalizedStringKey
+ let description: LocalizedStringKey
let loadingState: LoadingState
@ViewBuilder var content: () -> Content
@@ -341,9 +358,14 @@ struct SettingItemView: View {
var body: some View {
HStack {
- Text(String(localized: title))
- .multilineTextAlignment(.leading)
- .frame(maxWidth: .infinity, alignment: .leading)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(title)
+ Text(description)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ }
+ .multilineTextAlignment(.leading)
+ .frame(maxWidth: .infinity, alignment: .leading)
HStack {
switch loadingState {
diff --git a/Whisky/Views/Bottle/Pins/PinAddView.swift b/Whisky/Views/Bottle/Pins/PinAddView.swift
index 2847ce221..4a01eb29a 100644
--- a/Whisky/Views/Bottle/Pins/PinAddView.swift
+++ b/Whisky/Views/Bottle/Pins/PinAddView.swift
@@ -22,25 +22,25 @@ import WhiskyKit
struct PinAddView: View {
let bottle: Bottle
@State private var showingSheet = false
+ @State private var hovered = false
var body: some View {
- VStack {
- Button {
- showingSheet = true
- } label: {
- Image(systemName: "plus.circle")
- .resizable()
- .foregroundStyle(.secondary)
+ Button {
+ showingSheet = true
+ } label: {
+ PinTile(title: String(localized: "pin.help"), isHighlighted: hovered) {
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
+ .strokeBorder(.tertiary, style: StrokeStyle(lineWidth: 1.5, dash: [4, 3]))
+ .overlay {
+ Image(systemName: "plus")
+ .font(.title2)
+ .foregroundStyle(.secondary)
+ }
}
- .buttonStyle(.plain)
- .frame(width: 45, height: 45)
- Spacer()
- Text("pin.help")
- .multilineTextAlignment(.center)
- .lineLimit(2, reservesSpace: true)
+ .foregroundStyle(.secondary)
}
- .frame(width: 90, height: 90)
- .padding(10)
+ .buttonStyle(.plain)
+ .onHover { hovered = $0 }
.sheet(isPresented: $showingSheet) {
PinCreationView(bottle: bottle)
}
diff --git a/Whisky/Views/Bottle/Pins/PinCreationView.swift b/Whisky/Views/Bottle/Pins/PinCreationView.swift
index 2bd4c8ac0..bea4db428 100644
--- a/Whisky/Views/Bottle/Pins/PinCreationView.swift
+++ b/Whisky/Views/Bottle/Pins/PinCreationView.swift
@@ -109,8 +109,9 @@ struct PinCreationView: View {
bottle.settings.pins.append(PinnedProgram(name: newPinName, url: newPinURL))
- // Trigger a reload
- bottle.updateInstalledPrograms()
+ Task {
+ await bottle.updateInstalledPrograms()
+ }
dismiss()
}
}
diff --git a/Whisky/Views/Bottle/Pins/PinView.swift b/Whisky/Views/Bottle/Pins/PinView.swift
index 99c6ea30b..216e5f977 100644
--- a/Whisky/Views/Bottle/Pins/PinView.swift
+++ b/Whisky/Views/Bottle/Pins/PinView.swift
@@ -25,43 +25,32 @@ struct PinView: View {
@State var pin: PinnedProgram
@Binding var path: NavigationPath
- @State private var image: Image?
@State private var showRenameSheet = false
@State private var name: String = ""
@State private var opening: Bool = false
+ @State private var hovered: Bool = false
var body: some View {
- VStack {
- Group {
- if let image = image {
- image
- .resizable()
- } else {
- Image(systemName: "app.dashed")
- .resizable()
+ PinTile(title: name, isHighlighted: hovered) {
+ ProgramIconView(program: program, size: 48)
+ .scaleEffect(opening ? 1.6 : 1)
+ .opacity(opening ? 0 : 1)
+ .overlay(alignment: .bottomTrailing) {
+ Button("button.run", systemImage: "play.circle.fill") {
+ runProgram()
+ }
+ .labelStyle(.iconOnly)
+ .buttonStyle(.plain)
+ .font(.title2)
+ .symbolRenderingMode(.palette)
+ .foregroundStyle(.white, Color.accentColor)
+ .offset(x: 6, y: 6)
+ .opacity(hovered ? 1 : 0)
+ .help("button.run")
}
- }
- .frame(width: 45, height: 45)
- .scaleEffect(opening ? 2 : 1)
- .opacity(opening ? 0 : 1)
- Spacer()
- Text(name)
- .multilineTextAlignment(.center)
- .lineLimit(2, reservesSpace: true)
- }
- .frame(width: 90, height: 90)
- .padding(10)
- .overlay {
- HStack {
- Spacer()
- Image(systemName: "play.fill")
- .resizable()
- .foregroundColor(.green)
- .frame(width: 16, height: 16)
- }
- .frame(width: 45, height: 45)
- .padding(EdgeInsets(top: 0, leading: 0, bottom: 12, trailing: 0))
}
+ .onHover { hovered = $0 }
+ .help(program.displayLocation)
.contextMenu {
ProgramMenuView(program: program, path: $path)
@@ -82,14 +71,8 @@ struct PinView: View {
name = newName
}
}
- .task {
+ .onAppear {
name = pin.name
- guard let peFile = program.peFile else { return }
- let task = Task.detached {
- guard let image = peFile.bestIcon() else { return nil as Image? }
- return Image(nsImage: image)
- }
- self.image = await task.value
}
.onChange(of: name) {
if let index = bottle.settings.pins.firstIndex(where: {
diff --git a/Whisky/Views/Bottle/Pins/PinnedProgramsGrid.swift b/Whisky/Views/Bottle/Pins/PinnedProgramsGrid.swift
new file mode 100644
index 000000000..9655b8f2e
--- /dev/null
+++ b/Whisky/Views/Bottle/Pins/PinnedProgramsGrid.swift
@@ -0,0 +1,63 @@
+//
+// PinnedProgramsGrid.swift
+// Whisky
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import SwiftUI
+import WhiskyKit
+
+private let pinTileWidth: CGFloat = 96
+
+struct PinnedProgramsGrid: View {
+ @ObservedObject var bottle: Bottle
+ @Binding var path: NavigationPath
+
+ private let columns = [GridItem(.adaptive(minimum: pinTileWidth, maximum: pinTileWidth), spacing: 8)]
+
+ var body: some View {
+ LazyVGrid(columns: columns, alignment: .leading, spacing: 8) {
+ ForEach(bottle.pinnedPrograms, id: \.id) { pinnedProgram in
+ PinView(bottle: bottle, program: pinnedProgram.program, pin: pinnedProgram.pin, path: $path)
+ }
+ PinAddView(bottle: bottle)
+ }
+ }
+}
+
+/// Shared layout of a tile in the pinned programs grid
+struct PinTile: View {
+ let title: String
+ let isHighlighted: Bool
+ @ViewBuilder let icon: () -> Icon
+
+ var body: some View {
+ VStack(spacing: 6) {
+ icon()
+ .frame(width: 48, height: 48)
+ Text(title)
+ .font(.callout)
+ .multilineTextAlignment(.center)
+ .lineLimit(2, reservesSpace: true)
+ }
+ .padding(.vertical, 8)
+ .padding(.horizontal, 6)
+ .frame(width: pinTileWidth)
+ .background(.quaternary.opacity(isHighlighted ? 1 : 0),
+ in: RoundedRectangle(cornerRadius: 10, style: .continuous))
+ .contentShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
+ .animation(.whiskyDefault, value: isHighlighted)
+ }
+}
diff --git a/Whisky/Views/Bottle/RunningProcessesView.swift b/Whisky/Views/Bottle/RunningProcessesView.swift
deleted file mode 100644
index b87cf0cd0..000000000
--- a/Whisky/Views/Bottle/RunningProcessesView.swift
+++ /dev/null
@@ -1,113 +0,0 @@
-//
-// RunningProcessView.swift
-// Whisky
-//
-// This file is part of Whisky.
-//
-// Whisky is free software: you can redistribute it and/or modify it under the terms
-// of the GNU General Public License as published by the Free Software Foundation,
-// either version 3 of the License, or (at your option) any later version.
-//
-// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-// See the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with Whisky.
-// If not, see https://www.gnu.org/licenses/.
-//
-
-import SwiftUI
-import WhiskyKit
-
-struct BottleProcess: Identifiable {
- var id = UUID()
- var pid: String
- var procName: String
-}
-
-struct RunningProcessesView: View {
- @ObservedObject var bottle: Bottle
-
- @State private var processes = [BottleProcess]()
- @State private var processSortOrder = [KeyPathComparator(\BottleProcess.pid)]
- @State private var selectedProcess: BottleProcess.ID?
-
- var body: some View {
- ZStack {
- if !processes.isEmpty {
- VStack {
- Table(processes, selection: $selectedProcess, sortOrder: $processSortOrder) {
- TableColumn("process.table.pid", value: \.pid)
- TableColumn("process.table.executable", value: \.procName)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
-
- HStack {
- Spacer()
- Button("process.table.refresh") {
- Task.detached(priority: .userInitiated) {
- await fetchProcesses()
- }
- }
- Button("process.table.kill") {
- Task.detached(priority: .userInitiated) {
- await killProcess()
- }
- }
- }
- .padding()
- }
- } else {
- HStack(alignment: .center) {
- Spacer()
- VStack(alignment: .center) {
- ProgressView()
- .padding()
- Text("process.table.loading")
- }
- Spacer()
- }
- }
- }
- .onAppear {
- Task.detached(priority: .userInitiated) {
- await fetchProcesses()
- }
- }
- }
-
- func fetchProcesses() async {
- var newProcessList = [BottleProcess]()
- let output: String?
-
- do {
- output = try await Wine.runWine(["tasklist.exe"], bottle: bottle)
- } catch {
- print("Error running tasklist.exe: \(error)")
- output = ""
- }
-
- let lines = output?.split(omittingEmptySubsequences: true, whereSeparator: \.isNewline)
- for line in lines ?? [] {
- let lineParts = line.split(separator: ",", omittingEmptySubsequences: true)
- if lineParts.count > 1 {
- let pid = String(lineParts[1])
- let procName = String(lineParts[0])
- newProcessList.append(BottleProcess(pid: pid, procName: procName))
- }
- }
- processes = newProcessList
- }
-
- func killProcess() async {
- if let thisProcess = processes.first(where: { $0.id == selectedProcess }) {
- do {
- try await Wine.runWine(["taskkill.exe", "/PID", thisProcess.pid, "/F"], bottle: bottle)
- try await Task.sleep(nanoseconds: 2000)
- } catch {
- print("Error running taskkill.exe: \(error)")
- }
- await fetchProcesses()
- }
- }
-}
diff --git a/Whisky/Views/Bottle/WinetricksView.swift b/Whisky/Views/Bottle/WinetricksView.swift
index b0f92eeda..4bc0eb7d7 100644
--- a/Whisky/Views/Bottle/WinetricksView.swift
+++ b/Whisky/Views/Bottle/WinetricksView.swift
@@ -22,6 +22,7 @@ import WhiskyKit
struct WinetricksView: View {
var bottle: Bottle
@State private var winetricks: [WinetricksCategory]?
+ @State private var loadError: String?
@State private var selectedTrick: UUID?
@Environment(\.dismiss) var dismiss
@@ -61,15 +62,16 @@ struct WinetricksView: View {
let trick = winetricks.flatMap { $0.verbs }.first(where: { $0.id == selectedTrick })
if let trickName = trick?.name {
- Task.detached {
- await Winetricks.runCommand(command: trickName, bottle: bottle)
- }
+ Winetricks.runCommand(command: trickName, bottle: bottle)
}
dismiss()
}
.buttonStyle(.borderedProminent)
}
}
+ } else if let loadError {
+ ContentUnavailableView("winetricks.loadFailed", systemImage: "exclamationmark.triangle",
+ description: Text(loadError))
} else {
Spacer()
ProgressView()
@@ -79,13 +81,11 @@ struct WinetricksView: View {
}
}
.padding()
- .onAppear {
- Task.detached {
- let tricks = await Winetricks.parseVerbs()
-
- await MainActor.run {
- winetricks = tricks
- }
+ .task {
+ do {
+ winetricks = try await Task.detached { try Winetricks.parseVerbs() }.value
+ } catch {
+ loadError = error.localizedDescription
}
}
.frame(minWidth: ViewWidth.large, minHeight: 400)
diff --git a/Whisky/Views/Common/BottomBar.swift b/Whisky/Views/Common/BottomBar.swift
deleted file mode 100644
index 3828dc095..000000000
--- a/Whisky/Views/Common/BottomBar.swift
+++ /dev/null
@@ -1,75 +0,0 @@
-//
-// BottomBar.swift
-// Whisky
-//
-// This file is part of Whisky.
-//
-// Whisky is free software: you can redistribute it and/or modify it under the terms
-// of the GNU General Public License as published by the Free Software Foundation,
-// either version 3 of the License, or (at your option) any later version.
-//
-// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-// See the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with Whisky.
-// If not, see https://www.gnu.org/licenses/.
-//
-
-import SwiftUI
-
-extension View {
- func bottomBar(
- @ViewBuilder content: () -> Content
- ) -> some View where Content: View {
- modifier(BottomBarViewModifier(barContent: content()))
- }
-}
-
-private struct BottomBarViewModifier: ViewModifier where BarContent: View {
- var barContent: BarContent
-
- func body(content: Content) -> some View {
- content
- .safeAreaInset(edge: .bottom, spacing: 0) {
- VStack(spacing: 0) {
- Divider()
- barContent
- }
- .background(.regularMaterial)
- .buttonStyle(BottomBarButtonStyle())
- }
- }
-}
-
-struct BottomBarButtonStyle: PrimitiveButtonStyle {
- func makeBody(configuration: Configuration) -> some View {
- Button {
- configuration.trigger()
- } label: {
- configuration.label
- .foregroundStyle(.foreground)
- }
- }
-}
-
-#Preview {
- Form {
- Text(String("Hello World"))
- }
- .formStyle(.grouped)
- .bottomBar {
- HStack {
- Spacer()
- Button {
- } label: {
- Text(String("Button 1"))
- }
- Button {
- } label: {
- Text(String("Button 2"))
- }
- }
- .padding()
- }
-}
diff --git a/Whisky/Views/Common/ProgramIconView.swift b/Whisky/Views/Common/ProgramIconView.swift
new file mode 100644
index 000000000..82eb37dec
--- /dev/null
+++ b/Whisky/Views/Common/ProgramIconView.swift
@@ -0,0 +1,75 @@
+//
+// ProgramIconView.swift
+// Whisky
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import SwiftUI
+import WhiskyKit
+
+/// Extracting an icon parses the whole executable, so each one is read once, off the main thread
+@MainActor
+final class ProgramIconCache {
+ static let shared = ProgramIconCache()
+
+ private var icons: [URL: Image?] = [:]
+ private var loads: [URL: Task] = [:]
+
+ func icon(for program: Program) async -> Image? {
+ if let icon = icons[program.url] {
+ return icon
+ }
+ if let load = loads[program.url] {
+ return await load.value
+ }
+ guard let peFile = program.peFile else { return nil }
+
+ let load = Task.detached(priority: .utility) {
+ peFile.bestIcon().map { Image(nsImage: $0) }
+ }
+ loads[program.url] = load
+ let icon = await load.value
+ loads[program.url] = nil
+ icons[program.url] = icon
+ return icon
+ }
+}
+
+struct ProgramIconView: View {
+ let program: Program
+ let size: CGFloat
+
+ @State private var icon: Image?
+
+ var body: some View {
+ Group {
+ if let icon {
+ icon
+ .resizable()
+ .interpolation(.high)
+ } else {
+ Image(systemName: "app.dashed")
+ .resizable()
+ .fontWeight(.light)
+ .foregroundStyle(.tertiary)
+ }
+ }
+ .scaledToFit()
+ .frame(width: size, height: size)
+ .task(id: program.url) {
+ icon = await ProgramIconCache.shared.icon(for: program)
+ }
+ }
+}
diff --git a/Whisky/Views/ContentView.swift b/Whisky/Views/ContentView.swift
index cc3960a0a..7370c33cc 100644
--- a/Whisky/Views/ContentView.swift
+++ b/Whisky/Views/ContentView.swift
@@ -19,8 +19,6 @@
import SwiftUI
import UniformTypeIdentifiers
import WhiskyKit
-import SemanticVersion
-import os
struct ContentView: View {
@AppStorage("selectedBottleURL") private var selectedBottleURL: URL?
@@ -47,18 +45,18 @@ struct ContentView: View {
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
- Button {
+ Button("button.createBottle", systemImage: "plus") {
showBottleCreation.toggle()
- } label: {
- Image(systemName: "plus")
- .help("button.createBottle")
}
+ .help("button.createBottle")
}
ToolbarItem(placement: .primaryAction) {
Button {
bottleVM.loadBottles()
if let bottle = bottleVM.bottles.first(where: { $0.url == selected }) {
- bottle.updateInstalledPrograms()
+ Task {
+ await bottle.updateInstalledPrograms()
+ }
}
triggerRefresh.toggle()
withAnimation(.default) {
@@ -77,10 +75,10 @@ struct ContentView: View {
BottleCreationView(newlyCreatedBottleURL: $newlyCreatedBottleURL)
}
.sheet(isPresented: $showSetup) {
- SetupView(showSetup: $showSetup, firstTime: false)
+ OnboardingSheet(isPresented: $showSetup)
}
.sheet(isPresented: $showWhiskyWineUpdate) {
- SetupView(showSetup: $showWhiskyWineUpdate, firstTime: false, initialPath: [.whiskyWineDownload])
+ OnboardingSheet(startingWith: .updateRuntime, isPresented: $showWhiskyWineUpdate)
}
.alert(
"create.error.title",
@@ -114,10 +112,11 @@ struct ContentView: View {
}
}
- if !WhiskyWineInstaller.isWhiskyWineInstalled() {
+ if !WhiskyWineInstaller.isSupportedRuntimeInstalled() {
showSetup = true
+ } else {
+ showWhiskyWineUpdate = await WhiskyWineUpdate.offerIfAvailable()
}
- await offerWhiskyWineUpdate()
}
}
@@ -128,44 +127,18 @@ struct ContentView: View {
)
}
- private func offerWhiskyWineUpdate() async {
- let remoteVersion: SemanticVersion?
- do {
- remoteVersion = try await WhiskyWineInstaller.availableUpdate()
- } catch {
- Logger.wineKit.error("Failed to check for a WhiskyWine update: \(error)")
- return
- }
- guard let remoteVersion else { return }
-
- let alert = NSAlert()
- alert.messageText = String(localized: "update.whiskywine.title")
- alert.informativeText = String(format: String(localized: "update.whiskywine.description"),
- String(WhiskyWineInstaller.whiskyWineVersion()
- ?? SemanticVersion(0, 0, 0)),
- String(remoteVersion))
- alert.alertStyle = .warning
- alert.addButton(withTitle: String(localized: "update.whiskywine.update"))
- alert.addButton(withTitle: String(localized: "button.removeAlert.cancel"))
-
- if alert.runModal() == .alertFirstButtonReturn {
- showWhiskyWineUpdate = true
- }
- }
-
var sidebar: some View {
ScrollViewReader { proxy in
List(selection: $selected) {
- Section {
+ Section("main.bottles") {
ForEach(filteredBottles) { bottle in
Group {
if bottle.inFlight {
- HStack {
- Text(bottle.settings.name)
- Spacer()
- ProgressView().controlSize(.small)
- }
- .opacity(0.5)
+ BottleRowLabel(name: bottle.settings.name,
+ subtitle: String(localized: "main.creatingBottle"),
+ systemImage: "wineglass")
+ .badge(Text(Image(systemName: "hourglass")))
+ .foregroundStyle(.secondary)
} else {
BottleListEntry(bottle: bottle, selected: $selected, refresh: $triggerRefresh)
.selectionDisabled(!bottle.isAvailable)
@@ -198,22 +171,22 @@ struct ContentView: View {
.disabled(bottle.inFlight)
.id(bottle.url)
}
- } else {
- if (bottleVM.bottles.isEmpty || bottleVM.countActive() == 0) && bottlesLoaded {
- VStack {
+ } else if bottlesLoaded {
+ if bottleVM.bottles.isEmpty || bottleVM.countActive() == 0 {
+ ContentUnavailableView {
+ Label("main.empty.title", systemImage: "wineglass")
+ } description: {
Text("main.createFirst")
- Button {
+ } actions: {
+ Button("button.createBottle") {
showBottleCreation.toggle()
- } label: {
- HStack {
- Image(systemName: "plus")
- Text("button.createBottle")
- }
- .padding(6)
}
.buttonStyle(.borderedProminent)
- .tint(.accentColor)
+ .controlSize(.large)
}
+ } else {
+ ContentUnavailableView("main.noSelection", systemImage: "sidebar.left",
+ description: Text("main.noSelection.description"))
}
}
}
diff --git a/Whisky/Views/FileOpenView.swift b/Whisky/Views/FileOpenView.swift
index a7ca4b0e8..d79aa5666 100644
--- a/Whisky/Views/FileOpenView.swift
+++ b/Whisky/Views/FileOpenView.swift
@@ -77,16 +77,11 @@ struct FileOpenView: View {
func run() {
if let bottle = bottles.first(where: { $0.url == selection }) {
- Task.detached(priority: .userInitiated) {
+ Task(priority: .userInitiated) {
do {
- if fileURL.pathExtension == "bat" {
- try await Wine.runBatchFile(url: fileURL,
- bottle: bottle)
- } else {
- try await Wine.runProgram(at: fileURL, bottle: bottle)
- }
+ try await Wine.runFile(at: fileURL, bottle: bottle)
} catch {
- print(error)
+ ErrorAlert.show(String(localized: "error.runProgram"), error: error)
}
}
dismiss()
diff --git a/Whisky/Views/Home/HomeView.swift b/Whisky/Views/Home/HomeView.swift
new file mode 100644
index 000000000..6b91e398a
--- /dev/null
+++ b/Whisky/Views/Home/HomeView.swift
@@ -0,0 +1,139 @@
+//
+// HomeView.swift
+// Whisky
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import SwiftUI
+import WhiskyKit
+
+/// The window for people who just want to play: set up Steam, then open it
+struct HomeView: View {
+ @EnvironmentObject var bottleVM: BottleVM
+ @Binding var showSetup: Bool
+ @AppStorage("showAdvanced") private var showAdvanced = false
+
+ @StateObject private var onboarding = OnboardingModel()
+ @State private var runtimeInstalled = WhiskyWineInstaller.isSupportedRuntimeInstalled()
+ @State private var showRepair = false
+ @State private var showRuntimeUpdate = false
+
+ private var steamBottle: Bottle? {
+ runtimeInstalled ? SteamInstaller.steamBottle(in: bottleVM.bottles) : nil
+ }
+
+ var body: some View {
+ Group {
+ if let steamBottle, onboarding.phase == .welcome {
+ SteamReadyView(bottle: steamBottle) {
+ showRepair = true
+ }
+ } else {
+ OnboardingView(model: onboarding) { openSteam in
+ finishOnboarding(openSteam: openSteam)
+ }
+ }
+ }
+ .sheet(isPresented: $showRepair) {
+ OnboardingSheet(startingWith: .repairSteam, isPresented: $showRepair)
+ }
+ .sheet(isPresented: $showSetup) {
+ OnboardingSheet(isPresented: $showSetup)
+ }
+ .sheet(isPresented: $showRuntimeUpdate) {
+ OnboardingSheet(startingWith: .updateRuntime, isPresented: $showRuntimeUpdate)
+ }
+ .onChange(of: onboarding.phase) {
+ runtimeInstalled = WhiskyWineInstaller.isSupportedRuntimeInstalled()
+ }
+ .task {
+ bottleVM.loadBottles()
+ if runtimeInstalled {
+ showRuntimeUpdate = await WhiskyWineUpdate.offerIfAvailable()
+ }
+ }
+ }
+
+ private func finishOnboarding(openSteam: Bool) {
+ bottleVM.loadBottles()
+ // Someone who skipped Steam needs the full window to run their own programs
+ if !onboarding.goal.includesSteam {
+ showAdvanced = true
+ }
+ if openSteam, let bottle = onboarding.steamBottle {
+ SteamSession.session(for: bottle).open()
+ }
+ onboarding.reset()
+ }
+}
+
+struct SteamReadyView: View {
+ let repair: () -> Void
+ @ObservedObject private var session: SteamSession
+ @StateObject private var steam: Program
+
+ init(bottle: Bottle, repair: @escaping () -> Void) {
+ self.repair = repair
+ session = SteamSession.session(for: bottle)
+ _steam = StateObject(wrappedValue: Program(url: SteamInstaller.steamExecutable(in: bottle), bottle: bottle))
+ }
+
+ var body: some View {
+ VStack(spacing: 18) {
+ Spacer(minLength: 0)
+ ProgramIconView(program: steam, size: 112)
+ VStack(spacing: 6) {
+ Text("home.ready.title")
+ .font(.largeTitle)
+ .fontWeight(.semibold)
+ Text(session.isRunning ? "home.running" : "home.ready.subtitle")
+ .font(.title3)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ }
+ Button {
+ session.open()
+ } label: {
+ Label(session.isRunning ? "home.showSteam" : "home.openSteam", systemImage: "play.fill")
+ .frame(minWidth: 180)
+ }
+ .buttonStyle(.borderedProminent)
+ .controlSize(.extraLarge)
+ .keyboardShortcut(.defaultAction)
+ Spacer(minLength: 0)
+ HStack(spacing: 6) {
+ Text("home.trouble")
+ .foregroundStyle(.secondary)
+ Button("home.repair", action: repair)
+ .buttonStyle(.link)
+ .disabled(session.isRunning)
+ }
+ .font(.callout)
+ }
+ .padding(32)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
+ session.refresh()
+ }
+ .alert("home.launchFailed", isPresented: isShowingLaunchError) {
+ } message: {
+ Text(session.launchError ?? "")
+ }
+ }
+
+ private var isShowingLaunchError: Binding {
+ Binding(get: { session.launchError != nil }, set: { if !$0 { session.launchError = nil } })
+ }
+}
diff --git a/Whisky/Views/Home/SteamSession.swift b/Whisky/Views/Home/SteamSession.swift
new file mode 100644
index 000000000..f9e97c863
--- /dev/null
+++ b/Whisky/Views/Home/SteamSession.swift
@@ -0,0 +1,80 @@
+//
+// SteamSession.swift
+// Whisky
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import AppKit
+import WhiskyKit
+
+/// Opens Steam at most once per bottle. A second Steam started while the first is still updating
+/// races it and both show their own windows, so when Steam is running, opening it again brings its
+/// windows forward instead.
+@MainActor
+final class SteamSession: ObservableObject {
+ private static var sessions: [URL: SteamSession] = [:]
+
+ static func session(for bottle: Bottle) -> SteamSession {
+ if let session = sessions[bottle.url] {
+ return session
+ }
+ let session = SteamSession(bottle: bottle)
+ sessions[bottle.url] = session
+ return session
+ }
+
+ let bottle: Bottle
+ @Published private(set) var isRunning = false
+ @Published var launchError: String?
+
+ /// Alive from launch until Steam and everything it started have exited
+ private var launch: Task?
+
+ private init(bottle: Bottle) {
+ self.bottle = bottle
+ refresh()
+ }
+
+ /// Re-reads whether Steam is running, e.g. after it was quit from its own menu
+ func refresh() {
+ isRunning = launch != nil || SteamInstaller.isRunning(in: bottle)
+ }
+
+ func open() {
+ refresh()
+ guard !isRunning else {
+ showWindows()
+ return
+ }
+
+ let steam = Program(url: SteamInstaller.steamExecutable(in: bottle), bottle: bottle)
+ launch = Task {
+ do {
+ try await steam.launch()
+ } catch {
+ launchError = error.localizedDescription
+ }
+ launch = nil
+ refresh()
+ }
+ isRunning = true
+ }
+
+ private func showWindows() {
+ for processID in WineProcesses.processIDs(in: bottle) {
+ NSRunningApplication(processIdentifier: processID)?.activate()
+ }
+ }
+}
diff --git a/Whisky/Views/Onboarding/OnboardingModel.swift b/Whisky/Views/Onboarding/OnboardingModel.swift
new file mode 100644
index 000000000..efe213feb
--- /dev/null
+++ b/Whisky/Views/Onboarding/OnboardingModel.swift
@@ -0,0 +1,262 @@
+//
+// OnboardingModel.swift
+// Whisky
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import SwiftUI
+import WhiskyKit
+
+enum OnboardingGoal {
+ /// First run: everything needed to play, ending with Steam installed
+ case setUpSteam
+ /// Same steps, but Steam is reinstalled over the existing copy
+ case repairSteam
+ /// Only what Whisky needs to run programs, for people who don't want Steam
+ case installRuntime
+ /// Replace the runtime with the newer one available for download
+ case updateRuntime
+
+ var steps: [OnboardingStep] {
+ switch self {
+ case .setUpSteam, .repairSteam:
+ return OnboardingStep.allCases
+ case .installRuntime, .updateRuntime:
+ return [.prepareMac, .downloadRuntime]
+ }
+ }
+
+ var includesSteam: Bool {
+ steps.contains(.installSteam)
+ }
+}
+
+enum OnboardingStep: CaseIterable, Identifiable {
+ case prepareMac
+ case downloadRuntime
+ case createBottle
+ case downloadSteam
+ case installSteam
+ case updateSteam
+
+ var id: Self { self }
+
+ var title: LocalizedStringKey {
+ switch self {
+ case .prepareMac: "onboarding.step.prepareMac"
+ case .downloadRuntime: "onboarding.step.downloadRuntime"
+ case .createBottle: "onboarding.step.createBottle"
+ case .downloadSteam: "onboarding.step.downloadSteam"
+ case .installSteam: "onboarding.step.installSteam"
+ case .updateSteam: "onboarding.step.updateSteam"
+ }
+ }
+
+ /// What went wrong, in words that need no technical knowledge
+ var failureMessage: LocalizedStringKey {
+ switch self {
+ case .prepareMac: "onboarding.error.prepareMac"
+ case .downloadRuntime, .downloadSteam: "onboarding.error.download"
+ case .createBottle: "onboarding.error.createBottle"
+ case .installSteam: "onboarding.error.installSteam"
+ case .updateSteam: "onboarding.error.updateSteam"
+ }
+ }
+}
+
+enum OnboardingStepState: Equatable {
+ case pending
+ case running(DownloadProgress?)
+ case done
+ case failed(details: String)
+}
+
+enum OnboardingError: LocalizedError {
+ case rosettaNotInstalled
+ case missingSteamBottle
+ case missingSteamInstaller
+
+ var errorDescription: String? {
+ switch self {
+ case .rosettaNotInstalled: "softwareupdate could not install Rosetta."
+ case .missingSteamBottle: "There is no bottle to install Steam into."
+ case .missingSteamInstaller: "The Steam installer has not been downloaded."
+ }
+ }
+}
+
+@MainActor
+final class OnboardingModel: ObservableObject {
+ enum Phase {
+ case welcome
+ case working
+ case finished
+ }
+
+ /// The name of the bottle created for Steam
+ static let steamBottleName = "Steam"
+
+ @Published private(set) var phase: Phase = .welcome
+ @Published private(set) var goal: OnboardingGoal = .setUpSteam
+ @Published private(set) var states: [OnboardingStep: OnboardingStepState] = [:]
+ @Published private(set) var steamBottle: Bottle?
+
+ private var steamInstaller: URL?
+
+ init(startingWith goal: OnboardingGoal? = nil) {
+ if let goal {
+ start(goal)
+ }
+ }
+
+ /// A fixed state, for previews
+ init(phase: Phase, goal: OnboardingGoal, states: [OnboardingStep: OnboardingStepState]) {
+ self.phase = phase
+ self.goal = goal
+ self.states = states
+ }
+
+ var isRunning: Bool {
+ states.values.contains { if case .running = $0 { true } else { false } }
+ }
+
+ var failedStep: OnboardingStep? {
+ goal.steps.first { if case .failed = state(of: $0) { true } else { false } }
+ }
+
+ func state(of step: OnboardingStep) -> OnboardingStepState {
+ states[step] ?? .pending
+ }
+
+ func start(_ goal: OnboardingGoal) {
+ self.goal = goal
+ states = [:]
+ phase = .working
+ Task { await runRemainingSteps() }
+ }
+
+ /// Back to the welcome screen, e.g. once the person has opened Steam from the finished screen
+ func reset() {
+ states = [:]
+ phase = .welcome
+ }
+
+ func retry() {
+ guard let failedStep else { return }
+ states[failedStep] = .pending
+ Task { await runRemainingSteps() }
+ }
+
+ private func runRemainingSteps() async {
+ for step in goal.steps where state(of: step) != .done {
+ states[step] = .running(nil)
+ do {
+ try await perform(step)
+ states[step] = .done
+ } catch {
+ states[step] = .failed(details: error.localizedDescription)
+ return
+ }
+ }
+ phase = .finished
+ }
+
+ private func perform(_ step: OnboardingStep) async throws {
+ switch step {
+ case .prepareMac:
+ try await prepareMac()
+ case .downloadRuntime:
+ try await installRuntime()
+ case .createBottle:
+ try await findOrCreateSteamBottle()
+ case .downloadSteam:
+ try await downloadSteam()
+ case .installSteam:
+ try await installSteam()
+ case .updateSteam:
+ try await updateSteam()
+ }
+ }
+
+ private func prepareMac() async throws {
+ guard !Rosetta2.isRosettaInstalled else { return }
+ guard try await Rosetta2.installRosetta() else {
+ throw OnboardingError.rosettaNotInstalled
+ }
+ }
+
+ private func installRuntime() async throws {
+ guard goal == .updateRuntime || !WhiskyWineInstaller.isSupportedRuntimeInstalled() else { return }
+
+ let archive = try await WhiskyWineInstaller.download(progress: reporter(for: .downloadRuntime))
+ states[.downloadRuntime] = .running(nil)
+ try await Task.detached(priority: .userInitiated) {
+ try WhiskyWineInstaller.install(from: archive)
+ }.value
+ }
+
+ private func findOrCreateSteamBottle() async throws {
+ BottleVM.shared.loadBottles()
+ if let existing = SteamInstaller.steamBottle(in: BottleVM.shared.bottles)
+ ?? BottleVM.shared.bottles.first(where: { $0.isAvailable && $0.settings.name == Self.steamBottleName }) {
+ steamBottle = existing
+ return
+ }
+
+ let location = BottleData.preferredBottleLocation.appending(path: UUID().uuidString)
+ steamBottle = try await BottleVM.shared.createBottle(at: location, name: Self.steamBottleName,
+ winVersion: .win10)
+ }
+
+ private func downloadSteam() async throws {
+ guard let steamBottle else { throw OnboardingError.missingSteamBottle }
+ guard goal == .repairSteam || !SteamInstaller.isInstalled(in: steamBottle) else { return }
+
+ steamInstaller = try await SteamInstaller.downloadInstaller(progress: reporter(for: .downloadSteam))
+ }
+
+ private func installSteam() async throws {
+ guard let steamBottle else { throw OnboardingError.missingSteamBottle }
+ if let steamInstaller {
+ try await SteamInstaller.install(in: steamBottle, using: steamInstaller)
+ self.steamInstaller = nil
+ } else if !SteamInstaller.isInstalled(in: steamBottle) {
+ throw OnboardingError.missingSteamInstaller
+ }
+
+ let steam = SteamInstaller.steamExecutable(in: steamBottle)
+ if !steamBottle.settings.pins.contains(where: { $0.url == steam }) {
+ steamBottle.settings.pins.append(PinnedProgram(name: Self.steamBottleName, url: steam))
+ }
+ }
+
+ /// Steam updates itself on first launch; doing it here shows progress and lets the web helper shim go in
+ private func updateSteam() async throws {
+ guard let steamBottle else { throw OnboardingError.missingSteamBottle }
+ let report = reporter(for: .updateSteam)
+ try await SteamInstaller.completeFirstUpdate(in: steamBottle) { downloadedBytes in
+ report(DownloadProgress(receivedBytes: downloadedBytes, expectedBytes: nil))
+ }
+ }
+
+ private func reporter(for step: OnboardingStep) -> @Sendable (DownloadProgress) -> Void {
+ { progress in
+ Task { @MainActor [weak self] in
+ guard self?.state(of: step) != .done else { return }
+ self?.states[step] = .running(progress)
+ }
+ }
+ }
+}
diff --git a/Whisky/Views/Onboarding/OnboardingView.swift b/Whisky/Views/Onboarding/OnboardingView.swift
new file mode 100644
index 000000000..1662c7d12
--- /dev/null
+++ b/Whisky/Views/Onboarding/OnboardingView.swift
@@ -0,0 +1,269 @@
+//
+// OnboardingView.swift
+// Whisky
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import SwiftUI
+import WhiskyKit
+
+struct OnboardingView: View {
+ @ObservedObject var model: OnboardingModel
+ /// Called when the person is done: after Open Steam, Done, or Cancel on the welcome screen
+ var finish: (_ openSteam: Bool) -> Void
+ var canCancel = false
+
+ var body: some View {
+ VStack(spacing: 0) {
+ switch model.phase {
+ case .welcome:
+ welcome
+ case .working:
+ OnboardingProgressView(model: model)
+ case .finished:
+ finished
+ }
+ }
+ .padding(32)
+ .frame(minWidth: 460, maxWidth: .infinity, minHeight: 420, maxHeight: .infinity)
+ .animation(.whiskyDefault, value: model.phase)
+ }
+
+ private var welcome: some View {
+ VStack(spacing: 18) {
+ Spacer(minLength: 0)
+ Image(nsImage: NSApp.applicationIconImage)
+ .resizable()
+ .frame(width: 96, height: 96)
+ VStack(spacing: 6) {
+ Text("onboarding.title")
+ .font(.largeTitle)
+ .fontWeight(.semibold)
+ Text("onboarding.subtitle")
+ .font(.title3)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: 380)
+ }
+ Button {
+ model.start(.setUpSteam)
+ } label: {
+ Text("onboarding.setUpSteam")
+ .frame(minWidth: 180)
+ }
+ .buttonStyle(.borderedProminent)
+ .controlSize(.extraLarge)
+ .keyboardShortcut(.defaultAction)
+ .padding(.top, 6)
+ Spacer(minLength: 0)
+ HStack(spacing: 16) {
+ Button("onboarding.withoutSteam") {
+ model.start(.installRuntime)
+ }
+ if canCancel {
+ Button("create.cancel") {
+ finish(false)
+ }
+ .keyboardShortcut(.cancelAction)
+ }
+ }
+ .buttonStyle(.link)
+ .font(.callout)
+ }
+ }
+
+ private var finished: some View {
+ VStack(spacing: 18) {
+ Spacer(minLength: 0)
+ Image(systemName: "checkmark.circle.fill")
+ .font(.system(size: 64))
+ .foregroundStyle(.green)
+ Text(model.goal.includesSteam ? "onboarding.done.steam" : "onboarding.done.runtime")
+ .font(.largeTitle)
+ .fontWeight(.semibold)
+ if model.goal.includesSteam {
+ Text("onboarding.done.steam.subtitle")
+ .font(.title3)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: 380)
+ Button {
+ finish(true)
+ } label: {
+ Label("home.openSteam", systemImage: "play.fill")
+ .frame(minWidth: 180)
+ }
+ .buttonStyle(.borderedProminent)
+ .controlSize(.extraLarge)
+ .keyboardShortcut(.defaultAction)
+ } else {
+ Button("setup.done") {
+ finish(false)
+ }
+ .buttonStyle(.borderedProminent)
+ .controlSize(.large)
+ .keyboardShortcut(.defaultAction)
+ }
+ Spacer(minLength: 0)
+ }
+ }
+}
+
+private struct OnboardingProgressView: View {
+ @ObservedObject var model: OnboardingModel
+ @State private var showDetails = false
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ VStack(alignment: .leading, spacing: 4) {
+ Text(model.goal.includesSteam ? "onboarding.working.steam" : "onboarding.working.runtime")
+ .font(.title)
+ .fontWeight(.semibold)
+ Text("onboarding.working.subtitle")
+ .foregroundStyle(.secondary)
+ }
+ VStack(alignment: .leading, spacing: 14) {
+ ForEach(model.goal.steps) { step in
+ OnboardingStepRow(step: step, state: model.state(of: step))
+ }
+ }
+ Spacer(minLength: 0)
+ if let failedStep = model.failedStep, case .failed(let details) = model.state(of: failedStep) {
+ failure(message: failedStep.failureMessage, details: details)
+ }
+ }
+ .frame(maxWidth: 440)
+ }
+
+ private func failure(message: LocalizedStringKey, details: String) -> some View {
+ VStack(alignment: .leading, spacing: 10) {
+ Text(message)
+ .fixedSize(horizontal: false, vertical: true)
+ DisclosureGroup("onboarding.error.details", isExpanded: $showDetails) {
+ Text(details)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .textSelection(.enabled)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .font(.callout)
+ HStack {
+ Spacer()
+ Button("setup.retry") {
+ showDetails = false
+ model.retry()
+ }
+ .buttonStyle(.borderedProminent)
+ .keyboardShortcut(.defaultAction)
+ }
+ }
+ .padding(14)
+ .background(.red.opacity(0.08), in: RoundedRectangle(cornerRadius: 10, style: .continuous))
+ }
+}
+
+private struct OnboardingStepRow: View {
+ let step: OnboardingStep
+ let state: OnboardingStepState
+
+ var body: some View {
+ HStack(alignment: .top, spacing: 12) {
+ icon
+ .frame(width: 20, height: 20)
+ VStack(alignment: .leading, spacing: 6) {
+ Text(step.title)
+ .foregroundStyle(state == .pending ? .secondary : .primary)
+ if case .running(let progress?) = state {
+ ProgressView(value: progress.fraction ?? 0)
+ Text(Self.describe(progress))
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .monospacedDigit()
+ }
+ }
+ }
+ }
+
+ @ViewBuilder
+ private var icon: some View {
+ switch state {
+ case .pending:
+ Image(systemName: "circle")
+ .foregroundStyle(.tertiary)
+ case .running:
+ ProgressView()
+ .controlSize(.small)
+ case .done:
+ Image(systemName: "checkmark.circle.fill")
+ .foregroundStyle(.green)
+ case .failed:
+ Image(systemName: "exclamationmark.circle.fill")
+ .foregroundStyle(.red)
+ }
+ }
+
+ private static func describe(_ progress: DownloadProgress) -> String {
+ let received = ByteCountFormatter.string(fromByteCount: progress.receivedBytes, countStyle: .file)
+ guard let expected = progress.expectedBytes else { return received }
+ let total = ByteCountFormatter.string(fromByteCount: expected, countStyle: .file)
+ return String(format: String(localized: "setup.whiskywine.progress"), received, total)
+ }
+}
+
+/// Onboarding in a sheet, for repairs and for the advanced window
+struct OnboardingSheet: View {
+ @StateObject private var model: OnboardingModel
+ @Binding var isPresented: Bool
+
+ init(startingWith goal: OnboardingGoal? = nil, isPresented: Binding) {
+ _model = StateObject(wrappedValue: OnboardingModel(startingWith: goal))
+ _isPresented = isPresented
+ }
+
+ var body: some View {
+ OnboardingView(model: model, finish: { openSteam in
+ isPresented = false
+ if openSteam, let bottle = model.steamBottle {
+ SteamSession.session(for: bottle).open()
+ }
+ }, canCancel: true)
+ .frame(width: 540, height: 500)
+ .interactiveDismissDisabled(model.isRunning)
+ }
+}
+
+#Preview("Welcome") {
+ OnboardingView(model: OnboardingModel(phase: .welcome, goal: .setUpSteam, states: [:])) { _ in }
+}
+
+#Preview("Downloading") {
+ OnboardingView(model: OnboardingModel(phase: .working, goal: .setUpSteam, states: [
+ .prepareMac: .done,
+ .downloadRuntime: .running(DownloadProgress(receivedBytes: 118_000_000, expectedBytes: 284_000_000))
+ ])) { _ in }
+}
+
+#Preview("Failed") {
+ OnboardingView(model: OnboardingModel(phase: .working, goal: .setUpSteam, states: [
+ .prepareMac: .done,
+ .downloadRuntime: .done,
+ .createBottle: .done,
+ .downloadSteam: .failed(details: "The Internet connection appears to be offline.")
+ ])) { _ in }
+}
+
+#Preview("Finished") {
+ OnboardingView(model: OnboardingModel(phase: .finished, goal: .setUpSteam, states: [:])) { _ in }
+}
diff --git a/Whisky/Views/Programs/EnvironmentArgView.swift b/Whisky/Views/Programs/EnvironmentArgView.swift
index ee69b035e..d65591e5e 100644
--- a/Whisky/Views/Programs/EnvironmentArgView.swift
+++ b/Whisky/Views/Programs/EnvironmentArgView.swift
@@ -44,32 +44,22 @@ class Key: Identifiable {
struct EnvironmentArgView: View {
@ObservedObject var program: Program
- @Binding var isExpanded: Bool
@FocusState var focus: Focusable?
@State private var environmentKeys: [Key] = []
@State private var movedToIllegalKey = false
var body: some View {
- Section(isExpanded: $isExpanded) {
- List(environmentKeys, id: \.id) { key in
+ Section {
+ if environmentKeys.isEmpty {
+ Text("environment.empty")
+ .foregroundStyle(.secondary)
+ }
+ ForEach(environmentKeys, id: \.id) { key in
KeyItem(focus: _focus,
environmentKeys: $environmentKeys,
key: key)
}
- .alternatingRowBackgrounds(.enabled)
- .onAppear {
- let keys = program.settings.environment.map { (key: String, value: String) in
- return Key(key: key, value: value)
- }
- environmentKeys = keys.sorted(by: { $0.key < $1.key })
- }
- .onDisappear {
- program.settings.environment.removeAll()
- for key in environmentKeys where !key.key.isEmpty {
- program.settings.environment[key.key] = key.value
- }
- }
} header: {
HStack {
Text("program.env").frame(maxWidth: .infinity, alignment: .leading)
@@ -77,9 +67,22 @@ struct EnvironmentArgView: View {
Button("environment.add", systemImage: "plus") {
createNewKey()
}
- .buttonStyle(.plain)
+ .buttonStyle(.borderless)
.labelStyle(.titleAndIcon)
- .opacity(isExpanded ? 1 : 0)
+ }
+ } footer: {
+ Text("environment.info")
+ }
+ .onAppear {
+ let keys = program.settings.environment.map { (key: String, value: String) in
+ return Key(key: key, value: value)
+ }
+ environmentKeys = keys.sorted(by: { $0.key < $1.key })
+ }
+ .onDisappear {
+ program.settings.environment.removeAll()
+ for key in environmentKeys where !key.key.isEmpty {
+ program.settings.environment[key.key] = key.value
}
}
.onChange(of: focus) { oldValue, newValue in
diff --git a/Whisky/Views/Programs/ProgramView.swift b/Whisky/Views/Programs/ProgramView.swift
index 801085a04..e74ef2974 100644
--- a/Whisky/Views/Programs/ProgramView.swift
+++ b/Whisky/Views/Programs/ProgramView.swift
@@ -22,96 +22,90 @@ import UniformTypeIdentifiers
struct ProgramView: View {
@ObservedObject var program: Program
- @State var programLoading: Bool = false
- @State var cachedIconImage: Image?
- @AppStorage("configSectionExapnded") private var configSectionExpanded: Bool = true
- @AppStorage("envArgsSectionExpanded") private var envArgsSectionExpanded: Bool = true
+ @State private var programLoading: Bool = false
var body: some View {
Form {
- Section("program.config", isExpanded: $configSectionExpanded) {
+ Section {
+ HStack(spacing: 14) {
+ ProgramIconView(program: program, size: 48)
+ VStack(alignment: .leading, spacing: 3) {
+ Text(program.displayName)
+ .font(.title2)
+ .fontWeight(.semibold)
+ .lineLimit(1)
+ Text(program.displayLocation)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .textSelection(.enabled)
+ }
+ Spacer(minLength: 0)
+ }
+ .padding(.vertical, 4)
+ }
+ Section {
+ LabeledContent("program.args") {
+ TextField("program.args", text: $program.settings.arguments,
+ prompt: Text("program.args.prompt"))
+ .labelsHidden()
+ .font(.system(.body, design: .monospaced))
+ .multilineTextAlignment(.trailing)
+ }
Picker("locale.title", selection: $program.settings.locale) {
ForEach(Locales.allCases, id: \.self) { locale in
Text(locale.pretty()).id(locale)
}
}
- VStack {
- HStack {
- Text("program.args")
- Spacer()
- }
- TextField("program.args", text: $program.settings.arguments)
- .textFieldStyle(.roundedBorder)
- .font(.system(.body, design: .monospaced))
- .labelsHidden()
- }
+ } header: {
+ Text("program.launch")
+ } footer: {
+ Text("program.launch.info")
}
- EnvironmentArgView(program: program, isExpanded: $envArgsSectionExpanded)
+ EnvironmentArgView(program: program)
}
- .bottomBar {
- HStack {
- Spacer()
- Button("button.showInFinder") {
- NSWorkspace.shared.activateFileViewerSelecting([program.url])
- }
- Button("button.createShortcut") {
- let panel = NSSavePanel()
- let applicationDir = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask)[0]
- let name = program.name.replacingOccurrences(of: ".exe", with: "")
- panel.directoryURL = applicationDir
- panel.canCreateDirectories = true
- panel.allowedContentTypes = [UTType.applicationBundle]
- panel.allowsOtherFileTypes = false
- panel.isExtensionHidden = true
- panel.nameFieldStringValue = name + ".app"
- panel.begin { result in
- if result == .OK {
- if let url = panel.url {
- let name = url.deletingPathExtension().lastPathComponent
- Task(priority: .userInitiated) {
- await ProgramShortcut.createShortcut(program, app: url, name: name)
- }
- }
- }
- }
+ .formStyle(.grouped)
+ .toolbar {
+ ToolbarItemGroup(placement: .primaryAction) {
+ if programLoading {
+ ProgressView()
+ .controlSize(.small)
}
- Button("button.run") {
+ Button("button.run", systemImage: "play.fill") {
programLoading = true
program.run()
}
.disabled(programLoading)
- if programLoading {
- Spacer()
- .frame(width: 10)
- ProgressView()
- .controlSize(.small)
+ .help("button.run")
+ Menu("bottle.more", systemImage: "ellipsis.circle") {
+ Button("button.showInFinder", systemImage: "folder") {
+ NSWorkspace.shared.activateFileViewerSelecting([program.url])
+ }
+ Button("button.createShortcut", systemImage: "square.and.arrow.down") {
+ createShortcut()
+ }
}
+ .help("bottle.more")
}
- .padding()
}
- .toolbar {
- if let image = cachedIconImage {
- ToolbarItem(id: "ProgramViewIcon", placement: .navigation) {
- image
- .resizable()
- .frame(width: 25, height: 25)
- .padding(.trailing, 5)
- }
- } else {
- ToolbarItem(id: "ProgramViewIcon", placement: .navigation) {
- Image(systemName: "app.dashed")
- .resizable()
- .frame(width: 25, height: 25)
- .padding(.trailing, 5)
- }
+ .navigationTitle(program.displayName)
+ }
+
+ private func createShortcut() {
+ let panel = NSSavePanel()
+ let applicationDir = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask)[0]
+ panel.directoryURL = applicationDir
+ panel.canCreateDirectories = true
+ panel.allowedContentTypes = [UTType.applicationBundle]
+ panel.allowsOtherFileTypes = false
+ panel.isExtensionHidden = true
+ panel.nameFieldStringValue = program.displayName + ".app"
+ panel.begin { result in
+ guard result == .OK, let url = panel.url else { return }
+ let name = url.deletingPathExtension().lastPathComponent
+ Task(priority: .userInitiated) {
+ await ProgramShortcut.createShortcut(program, app: url, name: name)
}
}
- .navigationTitle(program.name)
- .formStyle(.grouped)
- .animation(.whiskyDefault, value: configSectionExpanded)
- .animation(.whiskyDefault, value: envArgsSectionExpanded)
- .task {
- if let fetchedImage = program.peFile?.bestIcon() { self.cachedIconImage = Image(nsImage: fetchedImage) }
- }
}
}
diff --git a/Whisky/Views/Programs/ProgramsView.swift b/Whisky/Views/Programs/ProgramsView.swift
index eebbf0764..c31ed1d6e 100644
--- a/Whisky/Views/Programs/ProgramsView.swift
+++ b/Whisky/Views/Programs/ProgramsView.swift
@@ -23,177 +23,157 @@ struct ProgramsView: View {
@ObservedObject var bottle: Bottle
@State private var blocklist: [URL] = []
@State private var selectedPrograms = Set()
- @State private var selectedBlockitems = Set()
@Binding var path: NavigationPath
@State private var sortedPrograms: [Program] = []
- @State private var resortPrograms = false
@State private var searchText = ""
- @AppStorage("areProgramsExpanded") private var areProgramsExpanded = true
- @AppStorage("isBlocklistExpanded") private var isBlocklistExpanded = false
-
private var searchResults: [Program] {
guard !searchText.isEmpty else { return sortedPrograms }
- return sortedPrograms.filter({ $0.name.localizedCaseInsensitiveContains(searchText) })
+ return sortedPrograms.filter { $0.name.localizedCaseInsensitiveContains(searchText) }
}
- private var searchedBlocklists: [URL] {
+ private var searchedBlocklist: [URL] {
guard !searchText.isEmpty else { return blocklist }
- return blocklist.filter({ $0.absoluteString.localizedCaseInsensitiveContains(searchText) })
- }
-
- private var selectedSearchedPrograms: [Program] {
- searchResults.filter({ selectedPrograms.contains($0) })
+ return blocklist.filter { $0.lastPathComponent.localizedCaseInsensitiveContains(searchText) }
}
var body: some View {
- Form {
- Section("program.title", isExpanded: $areProgramsExpanded) {
- List(searchResults, id: \.self, selection: $selectedPrograms) { program in
- ProgramItemView(
- bottle: bottle, program: program, path: $path
- )
- .contextMenu {
- let selectedPrograms = selectedSearchedPrograms
- if selectedPrograms.contains(program) && selectedPrograms.count > 1 {
- Button("program.add.selected.blocklist", systemImage: "hand.raised") {
- bottle.settings.blocklist.append(contentsOf: selectedPrograms.map { $0.url })
- blocklist = bottle.settings.blocklist
- }
- .labelStyle(.titleAndIcon)
- } else {
- ProgramMenuView(program: program, path: $path)
-
- Section {
- Button("program.add.blocklist", systemImage: "hand.raised") {
- bottle.settings.blocklist.append(program.url)
- blocklist = bottle.settings.blocklist
- }
- .labelStyle(.titleAndIcon)
- }
- }
- }
+ List(selection: $selectedPrograms) {
+ let pinned = searchResults.filter(\.pinned)
+ if !pinned.isEmpty {
+ Section("bottle.pinned") {
+ programRows(pinned)
}
}
- .animation(.whiskyDefault, value: sortedPrograms)
-
- Section("program.blocklist", isExpanded: $isBlocklistExpanded) {
- List(searchedBlocklists, id: \.self, selection: $selectedBlockitems) { blockedUrl in
- BlocklistItemView(
- blockedUrl: blockedUrl, bottle: bottle
- )
- .contextMenu {
- if selectedBlockitems.contains(blockedUrl) {
- Button("program.remove.selected.blocklist", systemImage: "hand.raised") {
- bottle.settings.blocklist.removeAll(where: { selectedBlockitems.contains($0) })
- blocklist = bottle.settings.blocklist
- }
- .labelStyle(.titleAndIcon)
- .symbolVariant(.slash)
- } else {
- Button("program.remove.blocklist", systemImage: "hand.raised") {
- bottle.settings.blocklist.removeAll(where: { $0 == blockedUrl })
- blocklist = bottle.settings.blocklist
- }
- .labelStyle(.titleAndIcon)
- .symbolVariant(.slash)
- }
+ Section("program.title") {
+ programRows(searchResults.filter { !$0.pinned })
+ }
+ if !searchedBlocklist.isEmpty {
+ Section {
+ ForEach(searchedBlocklist, id: \.self) { blockedUrl in
+ BlocklistItemView(blockedUrl: blockedUrl, bottle: bottle)
}
+ } header: {
+ Text("program.blocklist")
+ } footer: {
+ Text("program.blocklist.info")
+ }
+ }
+ }
+ .listStyle(.inset)
+ .alternatingRowBackgrounds(.disabled)
+ .overlay {
+ if searchResults.isEmpty && searchedBlocklist.isEmpty {
+ if searchText.isEmpty {
+ ContentUnavailableView("program.empty", systemImage: "square.grid.2x2",
+ description: Text("program.empty.description"))
+ } else {
+ ContentUnavailableView.search(text: searchText)
}
}
}
- .formStyle(.grouped)
.animation(.whiskyDefault, value: sortedPrograms)
.animation(.whiskyDefault, value: bottle.settings.blocklist)
- .animation(.whiskyDefault, value: searchText)
- .animation(.whiskyDefault, value: areProgramsExpanded)
- .animation(.whiskyDefault, value: isBlocklistExpanded)
.navigationTitle("tab.programs")
.searchable(text: $searchText)
.onAppear {
loadData()
}
- .onChange(of: resortPrograms) {
- loadPrograms()
- }
.onChange(of: bottle.settings) {
loadData()
}
+ .onChange(of: bottle.programs) {
+ loadData()
+ }
}
- private func loadData() {
- loadPrograms()
- blocklist = bottle.settings.blocklist.filter({
- return FileManager.default.fileExists(atPath: $0.path(percentEncoded: false))
- })
+ private func programRows(_ programs: [Program]) -> some View {
+ ForEach(programs, id: \.self) { program in
+ ProgramItemView(program: program, path: $path)
+ .contextMenu {
+ programMenu(for: program)
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func programMenu(for program: Program) -> some View {
+ let selected = searchResults.filter { selectedPrograms.contains($0) }
+ if selected.contains(program) && selected.count > 1 {
+ Button("program.add.selected.blocklist", systemImage: "eye.slash") {
+ bottle.settings.blocklist.append(contentsOf: selected.map(\.url))
+ }
+ .labelStyle(.titleAndIcon)
+ } else {
+ ProgramMenuView(program: program, path: $path)
+
+ Section {
+ Button("program.add.blocklist", systemImage: "eye.slash") {
+ bottle.settings.blocklist.append(program.url)
+ }
+ .labelStyle(.titleAndIcon)
+ }
+ }
}
- private func loadPrograms() {
- let programs = bottle.programs.filter({
- return FileManager.default.fileExists(atPath: $0.url.path(percentEncoded: false))
- })
- sortedPrograms = [
- programs.pinned.sorted { $0.name < $1.name },
- programs.unpinned.sorted { $0.name < $1.name }
- ].flatMap { $0 }
+ private func loadData() {
+ let programs = bottle.programs.filter {
+ FileManager.default.fileExists(atPath: $0.url.path(percentEncoded: false))
+ }
+ sortedPrograms = programs.sorted {
+ $0.displayName.localizedStandardCompare($1.displayName) == .orderedAscending
+ }
+ blocklist = bottle.settings.blocklist.filter {
+ FileManager.default.fileExists(atPath: $0.path(percentEncoded: false))
+ }
}
}
struct ProgramItemView: View {
- @ObservedObject var bottle: Bottle
@ObservedObject var program: Program
@Binding var path: NavigationPath
- @State private var showButtons = false
- @State private var pinHovered = false
+ @State private var hovered = false
var body: some View {
- HStack {
- Button {
- program.pinned.toggle()
- } label: {
- Image(systemName: "pin")
- .onHover { hover in
- pinHovered = hover
- }
- .symbolVariant(program.pinned ? pinHovered ? .slash.fill : .fill : .none)
- .contentTransition(.symbolEffect(.replace))
+ HStack(spacing: 10) {
+ ProgramIconView(program: program, size: 28)
+ VStack(alignment: .leading, spacing: 1) {
+ Text(program.displayName)
+ .lineLimit(1)
+ Text(program.displayLocation)
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.middle)
}
- .buttonStyle(.plain)
- .foregroundColor(program.pinned ? .accentColor : .secondary)
- .opacity(program.pinned ? 1 : showButtons ? 1 : 0)
- Text(program.name)
- .frame(maxWidth: .infinity, alignment: .leading)
- if showButtons {
- if let peFile = program.peFile,
- let archString = peFile.architecture.toString() {
- Text(archString)
- .foregroundStyle(.secondary)
- .padding(.horizontal, 5)
- .overlay(
- RoundedRectangle(cornerRadius: 4)
- .stroke(.secondary)
- )
- }
-
- Button("program.config", systemImage: "gearshape") {
+ .frame(maxWidth: .infinity, alignment: .leading)
+ if let architecture = program.peFile?.architecture.toString() {
+ Text(architecture)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .padding(.horizontal, 5)
+ .padding(.vertical, 1)
+ .background(.quaternary, in: Capsule())
+ }
+ HStack(spacing: 2) {
+ Button("program.config", systemImage: "slider.horizontal.3") {
path.append(program)
}
- .labelStyle(.iconOnly)
- .buttonStyle(.plain)
- .foregroundStyle(.secondary)
.help("program.config")
- Button("button.run", systemImage: "play") {
+ Button("button.run", systemImage: "play.fill") {
program.run()
}
- .labelStyle(.iconOnly)
- .buttonStyle(.plain)
- .foregroundStyle(.secondary)
.help("button.run")
}
+ .labelStyle(.iconOnly)
+ .buttonStyle(.borderless)
+ .opacity(hovered ? 1 : 0)
}
- .padding(4)
- .onHover { hover in
- showButtons = hover
+ .padding(.vertical, 3)
+ .contentShape(Rectangle())
+ .onHover { hovered = $0 }
+ .onTapGesture(count: 2) {
+ path.append(program)
}
}
}
@@ -201,26 +181,26 @@ struct ProgramItemView: View {
struct BlocklistItemView: View {
let blockedUrl: URL
@ObservedObject var bottle: Bottle
- @State private var showButtons: Bool = false
var body: some View {
- HStack {
- Text(blockedUrl.prettyPath(bottle))
- Spacer()
- if showButtons {
- Button("program.remove.blocklist", systemImage: "xmark") {
- bottle.settings.blocklist.removeAll { $0 == blockedUrl }
- }
- .labelStyle(.iconOnly)
- .symbolVariant(.fill.circle)
- .buttonStyle(.plain)
- .foregroundColor(.secondary)
- .help("program.remove.blocklist")
+ HStack(spacing: 10) {
+ Image(systemName: "eye.slash")
+ .foregroundStyle(.secondary)
+ .frame(width: 28)
+ VStack(alignment: .leading, spacing: 1) {
+ Text(blockedUrl.deletingPathExtension().lastPathComponent)
+ Text(blockedUrl.deletingLastPathComponent().prettyPath(bottle))
+ .font(.subheadline)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.middle)
}
+ .frame(maxWidth: .infinity, alignment: .leading)
+ Button("program.remove.blocklist") {
+ bottle.settings.blocklist.removeAll { $0 == blockedUrl }
+ }
+ .buttonStyle(.borderless)
}
- .padding(4)
- .onHover { hover in
- showButtons = hover
- }
+ .padding(.vertical, 3)
}
}
diff --git a/Whisky/Views/Settings/SettingsView.swift b/Whisky/Views/Settings/SettingsView.swift
index 285babf7f..7bb3e10ab 100644
--- a/Whisky/Views/Settings/SettingsView.swift
+++ b/Whisky/Views/Settings/SettingsView.swift
@@ -20,14 +20,21 @@ import SwiftUI
import WhiskyKit
struct SettingsView: View {
+ static let killOnTerminateKey = "killOnTerminate"
+
@AppStorage("SUEnableAutomaticChecks") var whiskyUpdate = true
- @AppStorage("killOnTerminate") var killOnTerminate = true
+ @AppStorage(killOnTerminateKey) var killOnTerminate = true
@AppStorage("checkWhiskyWineUpdates") var checkWhiskyWineUpdates = true
@AppStorage(BottleData.defaultBottleLocationKey) var defaultBottleLocation = BottleData.defaultBottleDir
+ @AppStorage("showAdvanced") var showAdvanced = false
var body: some View {
Form {
Section("settings.general") {
+ Toggle(isOn: $showAdvanced) {
+ Text("settings.showAdvanced")
+ Text("settings.showAdvanced.info")
+ }
Toggle("settings.toggle.kill.on.terminate", isOn: $killOnTerminate)
ActionView(
text: "settings.path",
diff --git a/Whisky/Views/Setup/RosettaView.swift b/Whisky/Views/Setup/RosettaView.swift
deleted file mode 100644
index 059bc6f2b..000000000
--- a/Whisky/Views/Setup/RosettaView.swift
+++ /dev/null
@@ -1,119 +0,0 @@
-//
-// RosettaView.swift
-// Whisky
-//
-// This file is part of Whisky.
-//
-// Whisky is free software: you can redistribute it and/or modify it under the terms
-// of the GNU General Public License as published by the Free Software Foundation,
-// either version 3 of the License, or (at your option) any later version.
-//
-// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-// See the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with Whisky.
-// If not, see https://www.gnu.org/licenses/.
-//
-
-import SwiftUI
-import WhiskyKit
-
-struct RosettaView: View {
- @State var installing: Bool = true
- @State var successful: Bool = true
- @Binding var path: [SetupStage]
- @Binding var showSetup: Bool
-
- var body: some View {
- VStack {
- Text("setup.rosetta")
- .font(.title)
- .fontWeight(.bold)
- Text("setup.rosetta.subtitle")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- Spacer()
- Group {
- if installing {
- ProgressView()
- .scaleEffect(2)
- } else {
- if successful {
- Image(systemName: "checkmark.circle")
- .resizable()
- .foregroundStyle(.green)
- .frame(width: 80, height: 80)
- } else {
- VStack {
- Image(systemName: "xmark.circle")
- .resizable()
- .foregroundStyle(.red)
- .frame(width: 80, height: 80)
- .padding(.bottom, 20)
- Text("setup.rosetta.fail")
- .font(.subheadline)
- }
- }
- }
- }
- Spacer()
- HStack {
- if !successful {
- Button("setup.quit") {
- exit(0)
- }
- .keyboardShortcut(.cancelAction)
- Spacer()
- Button("setup.retry") {
- installing = true
- successful = true
-
- Task.detached {
- await checkOrInstall()
- }
- }
- .keyboardShortcut(.defaultAction)
- }
- }
- }
- .frame(width: 400, height: 200)
- .onAppear {
- Task.detached {
- await checkOrInstall()
- }
- }
- }
-
- func checkOrInstall() async {
- if Rosetta2.isRosettaInstalled {
- installing = false
- sleep(2)
- proceed()
- } else {
- do {
- successful = try await Rosetta2.installRosetta()
- installing = false
- try await Task.sleep(for: .seconds(2))
- proceed()
- } catch {
- successful = false
- installing = false
- }
- }
- }
-
- @MainActor
- func proceed() {
- if !WhiskyWineInstaller.isWhiskyWineInstalled() {
- path.append(.whiskyWineDownload)
- return
- }
-
- showSetup = false
- }
-}
-
-#Preview {
- RosettaView(path: .constant([]), showSetup: .constant(true))
-}
diff --git a/Whisky/Views/Setup/SetupFailureView.swift b/Whisky/Views/Setup/SetupFailureView.swift
deleted file mode 100644
index 642cc72de..000000000
--- a/Whisky/Views/Setup/SetupFailureView.swift
+++ /dev/null
@@ -1,42 +0,0 @@
-//
-// SetupFailureView.swift
-// Whisky
-//
-// This file is part of Whisky.
-//
-// Whisky is free software: you can redistribute it and/or modify it under the terms
-// of the GNU General Public License as published by the Free Software Foundation,
-// either version 3 of the License, or (at your option) any later version.
-//
-// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-// See the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with Whisky.
-// If not, see https://www.gnu.org/licenses/.
-//
-
-import SwiftUI
-
-struct SetupFailureView: View {
- let title: LocalizedStringKey
- let message: String
- let retry: () -> Void
-
- var body: some View {
- VStack(spacing: 8) {
- Label(title, systemImage: "xmark.octagon.fill")
- .foregroundStyle(.red)
- .font(.headline)
- Text(message)
- .font(.subheadline)
- .foregroundStyle(.secondary)
- .multilineTextAlignment(.center)
- .lineLimit(3)
- .textSelection(.enabled)
- Button("setup.retry", action: retry)
- .keyboardShortcut(.defaultAction)
- }
- .padding(.horizontal)
- }
-}
diff --git a/Whisky/Views/Setup/SetupView.swift b/Whisky/Views/Setup/SetupView.swift
deleted file mode 100644
index 60bc87188..000000000
--- a/Whisky/Views/Setup/SetupView.swift
+++ /dev/null
@@ -1,59 +0,0 @@
-//
-// SetupView.swift
-// Whisky
-//
-// This file is part of Whisky.
-//
-// Whisky is free software: you can redistribute it and/or modify it under the terms
-// of the GNU General Public License as published by the Free Software Foundation,
-// either version 3 of the License, or (at your option) any later version.
-//
-// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-// See the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with Whisky.
-// If not, see https://www.gnu.org/licenses/.
-//
-
-import SwiftUI
-
-enum SetupStage {
- case rosetta
- case whiskyWineDownload
- case whiskyWineInstall
-}
-
-struct SetupView: View {
- @State private var path: [SetupStage]
- @State private var tarLocation: URL = URL(fileURLWithPath: "")
- @Binding var showSetup: Bool
- var firstTime: Bool
-
- init(showSetup: Binding, firstTime: Bool = true, initialPath: [SetupStage] = []) {
- _showSetup = showSetup
- self.firstTime = firstTime
- _path = State(initialValue: initialPath)
- }
-
- var body: some View {
- VStack {
- NavigationStack(path: $path) {
- WelcomeView(path: $path, showSetup: $showSetup, firstTime: firstTime)
- .navigationBarBackButtonHidden(true)
- .navigationDestination(for: SetupStage.self) { stage in
- switch stage {
- case .rosetta:
- RosettaView(path: $path, showSetup: $showSetup)
- case .whiskyWineDownload:
- WhiskyWineDownloadView(tarLocation: $tarLocation, path: $path)
- case .whiskyWineInstall:
- WhiskyWineInstallView(tarLocation: $tarLocation, path: $path, showSetup: $showSetup)
- }
- }
- }
- }
- .padding()
- .interactiveDismissDisabled()
- }
-}
diff --git a/Whisky/Views/Setup/WelcomeView.swift b/Whisky/Views/Setup/WelcomeView.swift
deleted file mode 100644
index f6cd64f22..000000000
--- a/Whisky/Views/Setup/WelcomeView.swift
+++ /dev/null
@@ -1,154 +0,0 @@
-//
-// WelcomeView.swift
-// Whisky
-//
-// This file is part of Whisky.
-//
-// Whisky is free software: you can redistribute it and/or modify it under the terms
-// of the GNU General Public License as published by the Free Software Foundation,
-// either version 3 of the License, or (at your option) any later version.
-//
-// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-// See the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with Whisky.
-// If not, see https://www.gnu.org/licenses/.
-//
-
-import SwiftUI
-import WhiskyKit
-
-struct WelcomeView: View {
- @State var rosettaInstalled: Bool?
- @State var whiskyWineInstalled: Bool?
- @State var shouldCheckInstallStatus: Bool = false
- @Binding var path: [SetupStage]
- @Binding var showSetup: Bool
- var firstTime: Bool
-
- var body: some View {
- VStack {
- VStack {
- if firstTime {
- Text("setup.welcome")
- .font(.title)
- .fontWeight(.bold)
- Text("setup.welcome.subtitle")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- } else {
- Text("setup.title")
- .font(.title)
- .fontWeight(.bold)
- Text("setup.subtitle")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- }
- }
- .padding(.horizontal)
- Spacer()
- Form {
- InstallStatusView(isInstalled: $rosettaInstalled,
- shouldCheckInstallStatus: $shouldCheckInstallStatus,
- name: "Rosetta")
- InstallStatusView(isInstalled: $whiskyWineInstalled,
- shouldCheckInstallStatus: $shouldCheckInstallStatus,
- showUninstall: true,
- name: "WhiskyWine")
- }
- .formStyle(.grouped)
- .scrollDisabled(true)
- .onAppear {
- checkInstallStatus()
- }
- .onChange(of: shouldCheckInstallStatus) {
- checkInstallStatus()
- }
- Spacer()
- HStack {
- if let rosettaInstalled = rosettaInstalled,
- let whiskyWineInstalled = whiskyWineInstalled {
- if !rosettaInstalled || !whiskyWineInstalled {
- Button("setup.quit") {
- exit(0)
- }
- .keyboardShortcut(.cancelAction)
- }
- Spacer()
- Button(rosettaInstalled && whiskyWineInstalled ? "setup.done" : "setup.next") {
- if !rosettaInstalled {
- path.append(.rosetta)
- return
- }
-
- if !whiskyWineInstalled {
- path.append(.whiskyWineDownload)
- return
- }
-
- showSetup = false
- }
- .keyboardShortcut(.defaultAction)
- }
- }
- }
- .frame(width: 400, height: 200)
- }
-
- func checkInstallStatus() {
- rosettaInstalled = Rosetta2.isRosettaInstalled
- whiskyWineInstalled = WhiskyWineInstaller.isWhiskyWineInstalled()
- }
-}
-
-struct InstallStatusView: View {
- @Binding var isInstalled: Bool?
- @Binding var shouldCheckInstallStatus: Bool
- @State var showUninstall: Bool = false
- @State var name: String
- @State var text: String = String(localized: "setup.install.checking")
-
- var body: some View {
- HStack {
- Group {
- if let installed = isInstalled {
- Circle()
- .foregroundColor(installed ? .green : .red)
- } else {
- ProgressView()
- .controlSize(.small)
- }
- }
- .frame(width: 10)
- Text(String.init(format: text, name))
- Spacer()
- if let installed = isInstalled {
- if installed && showUninstall {
- Button("setup.uninstall") {
- uninstall()
- }
- }
- }
- }
- .onChange(of: isInstalled) {
- if let installed = isInstalled {
- if installed {
- text = String(localized: "setup.install.installed")
- } else {
- text = String(localized: "setup.install.notInstalled")
- }
- } else {
- text = String(localized: "setup.install.checking")
- }
- }
- }
-
- func uninstall() {
- if name == "WhiskyWine" {
- WhiskyWineInstaller.uninstall()
- }
-
- shouldCheckInstallStatus.toggle()
- }
-}
diff --git a/Whisky/Views/Setup/WhiskyWineDownloadView.swift b/Whisky/Views/Setup/WhiskyWineDownloadView.swift
deleted file mode 100644
index be380e375..000000000
--- a/Whisky/Views/Setup/WhiskyWineDownloadView.swift
+++ /dev/null
@@ -1,161 +0,0 @@
-//
-// WhiskyWineDownloadView.swift
-// Whisky
-//
-// This file is part of Whisky.
-//
-// Whisky is free software: you can redistribute it and/or modify it under the terms
-// of the GNU General Public License as published by the Free Software Foundation,
-// either version 3 of the License, or (at your option) any later version.
-//
-// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-// See the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with Whisky.
-// If not, see https://www.gnu.org/licenses/.
-//
-
-import SwiftUI
-import WhiskyKit
-
-struct WhiskyWineDownloadView: View {
- @State private var fractionProgress: Double = 0
- @State private var completedBytes: Int64 = 0
- @State private var totalBytes: Int64 = 0
- @State private var downloadSpeed: Double = 0
- @State private var downloadTask: URLSessionDownloadTask?
- @State private var observation: NSKeyValueObservation?
- @State private var startTime: Date?
- @State private var failureMessage: String?
- @Binding var tarLocation: URL
- @Binding var path: [SetupStage]
- var body: some View {
- VStack {
- VStack {
- Text("setup.whiskywine.download")
- .font(.title)
- .fontWeight(.bold)
- Text("setup.whiskywine.download.subtitle")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- Spacer()
- if let failureMessage {
- SetupFailureView(title: "setup.whiskywine.download.failed", message: failureMessage) {
- startDownload()
- }
- } else {
- progress
- }
- Spacer()
- }
- Spacer()
- }
- .frame(width: 400, height: 200)
- .onAppear {
- startDownload()
- }
- .onDisappear {
- observation?.invalidate()
- downloadTask?.cancel()
- }
- }
-
- private var progress: some View {
- VStack {
- ProgressView(value: fractionProgress, total: 1)
- HStack {
- HStack {
- Text(String(format: String(localized: "setup.whiskywine.progress"),
- formatBytes(bytes: completedBytes),
- formatBytes(bytes: totalBytes)))
- + Text(String(" "))
- + (shouldShowEstimate() ?
- Text(String(format: String(localized: "setup.whiskywine.eta"),
- formatRemainingTime(remainingBytes: totalBytes - completedBytes)))
- : Text(String()))
- Spacer()
- }
- .font(.subheadline)
- .monospacedDigit()
- }
- }
- .padding(.horizontal)
- }
-
- private func startDownload() {
- failureMessage = nil
- fractionProgress = 0
- completedBytes = 0
- totalBytes = 0
- downloadSpeed = 0
-
- let task = URLSession(configuration: .ephemeral)
- .downloadTask(with: WhiskyWineInstaller.librariesArchiveURL) { location, response, error in
- let result = Result {
- try WhiskyWineInstaller.acceptDownload(at: location, response: response, error: error)
- }
- Task { @MainActor in
- finishDownload(result)
- }
- }
- observation = task.observe(\.countOfBytesReceived) { task, _ in
- let received = task.countOfBytesReceived
- let expected = task.countOfBytesExpectedToReceive
- Task { @MainActor in
- updateProgress(received: received, expected: expected)
- }
- }
- downloadTask = task
- startTime = Date()
- task.resume()
- }
-
- private func updateProgress(received: Int64, expected: Int64) {
- let elapsedTime = Date().timeIntervalSince(startTime ?? Date())
- if received > 0 && elapsedTime > 0 {
- downloadSpeed = Double(received) / elapsedTime
- }
- completedBytes = received
- totalBytes = max(expected, 0)
- fractionProgress = totalBytes > 0 ? Double(completedBytes) / Double(totalBytes) : 0
- }
-
- private func finishDownload(_ result: Result) {
- observation?.invalidate()
- switch result {
- case .success(let archive):
- tarLocation = archive
- path.append(.whiskyWineInstall)
- case .failure(let error as URLError) where error.code == .cancelled:
- break
- case .failure(let error):
- failureMessage = error.localizedDescription
- }
- }
-
- func formatBytes(bytes: Int64) -> String {
- let formatter = ByteCountFormatter()
- formatter.countStyle = .file
- formatter.zeroPadsFractionDigits = true
- return formatter.string(fromByteCount: bytes)
- }
-
- func shouldShowEstimate() -> Bool {
- let elapsedTime = Date().timeIntervalSince(startTime ?? Date())
- return Int(elapsedTime.rounded()) > 5 && completedBytes != 0 && totalBytes > 0 && downloadSpeed > 0
- }
-
- func formatRemainingTime(remainingBytes: Int64) -> String {
- let remainingTimeInSeconds = Double(remainingBytes) / downloadSpeed
-
- let formatter = DateComponentsFormatter()
- formatter.allowedUnits = [.hour, .minute, .second]
- formatter.unitsStyle = .full
- if shouldShowEstimate() {
- return formatter.string(from: TimeInterval(remainingTimeInSeconds)) ?? ""
- } else {
- return ""
- }
- }
-}
diff --git a/Whisky/Views/Setup/WhiskyWineInstallView.swift b/Whisky/Views/Setup/WhiskyWineInstallView.swift
deleted file mode 100644
index 2b7d05593..000000000
--- a/Whisky/Views/Setup/WhiskyWineInstallView.swift
+++ /dev/null
@@ -1,78 +0,0 @@
-//
-// WhiskyWineInstallView.swift
-// Whisky
-//
-// This file is part of Whisky.
-//
-// Whisky is free software: you can redistribute it and/or modify it under the terms
-// of the GNU General Public License as published by the Free Software Foundation,
-// either version 3 of the License, or (at your option) any later version.
-//
-// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-// See the GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License along with Whisky.
-// If not, see https://www.gnu.org/licenses/.
-//
-
-import SwiftUI
-import WhiskyKit
-
-struct WhiskyWineInstallView: View {
- @State private var installing: Bool = true
- @State private var failureMessage: String?
- @Binding var tarLocation: URL
- @Binding var path: [SetupStage]
- @Binding var showSetup: Bool
-
- var body: some View {
- VStack {
- VStack {
- Text("setup.whiskywine.install")
- .font(.title)
- .fontWeight(.bold)
- Text("setup.whiskywine.install.subtitle")
- .font(.subheadline)
- .foregroundStyle(.secondary)
- Spacer()
- if let failureMessage {
- SetupFailureView(title: "setup.whiskywine.install.failed", message: failureMessage) {
- path.removeLast()
- }
- } else if installing {
- ProgressView()
- .progressViewStyle(.circular)
- .frame(width: 80)
- } else {
- Image(systemName: "checkmark.circle")
- .resizable()
- .frame(width: 80, height: 80)
- .foregroundStyle(.green)
- }
- Spacer()
- }
- Spacer()
- }
- .frame(width: 400, height: 200)
- .task {
- await install()
- }
- }
-
- private func install() async {
- let archive = tarLocation
- do {
- try await Task.detached {
- try WhiskyWineInstaller.install(from: archive)
- }.value
- installing = false
- try await Task.sleep(for: .seconds(2))
- showSetup = false
- } catch is CancellationError {
- return
- } catch {
- failureMessage = error.localizedDescription
- }
- }
-}
diff --git a/Whisky/Views/WhiskyApp.swift b/Whisky/Views/WhiskyApp.swift
index ef9dc302d..d54319088 100644
--- a/Whisky/Views/WhiskyApp.swift
+++ b/Whisky/Views/WhiskyApp.swift
@@ -24,6 +24,7 @@ import os
@main
struct WhiskyApp: App {
@State var showSetup: Bool = false
+ @AppStorage("showAdvanced") private var showAdvanced = false
@State private var migrationError: Error?
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@Environment(\.openURL) var openURL
@@ -46,24 +47,30 @@ struct WhiskyApp: App {
var body: some Scene {
WindowGroup {
- ContentView(showSetup: $showSetup)
- .frame(minWidth: ViewWidth.large, minHeight: 316)
- .environmentObject(BottleVM.shared)
- .alert("migration.error.title", isPresented: isShowingMigrationError) {
- } message: {
- Text("migration.error.message.\(migrationError?.localizedDescription ?? "")")
+ Group {
+ if showAdvanced {
+ ContentView(showSetup: $showSetup)
+ } else {
+ HomeView(showSetup: $showSetup)
}
- .onAppear {
- NSWindow.allowsAutomaticWindowTabbing = false
+ }
+ .frame(minWidth: ViewWidth.large, minHeight: 480)
+ .environmentObject(BottleVM.shared)
+ .alert("migration.error.title", isPresented: isShowingMigrationError) {
+ } message: {
+ Text("migration.error.message.\(migrationError?.localizedDescription ?? "")")
+ }
+ .onAppear {
+ NSWindow.allowsAutomaticWindowTabbing = false
- Task.detached {
- do {
- try WineLogs.prune()
- } catch {
- Logger.wineKit.error("Failed to prune logs: \(error)")
- }
+ Task.detached {
+ do {
+ try WineLogs.prune()
+ } catch {
+ Logger.wineKit.error("Failed to prune logs: \(error)")
}
}
+ }
}
// Don't ask me how this works, it just does
.handlesExternalEvents(matching: ["{same path of URL?}"])
@@ -116,18 +123,8 @@ struct WhiskyApp: App {
}
}
CommandGroup(replacing: .help) {
- Button("help.website") {
- if let url = URL(string: "https://getwhisky.app/") {
- openURL(url)
- }
- }
Button("help.github") {
- if let url = URL(string: "https://github.com/Whisky-App/Whisky") {
- openURL(url)
- }
- }
- Button("help.discord") {
- if let url = URL(string: "https://discord.gg/CsqAfs9CnM") {
+ if let url = URL(string: "https://github.com/iStorry/Whisky") {
openURL(url)
}
}
@@ -140,10 +137,23 @@ struct WhiskyApp: App {
static func killBottles() {
for bottle in BottleVM.shared.bottles {
+ Task {
+ do {
+ try await Wine.stopAllProcesses(in: bottle)
+ } catch {
+ Logger.wineKit.error("Failed to stop `\(bottle.settings.name)`: \(error)")
+ }
+ }
+ }
+ }
+
+ /// Stops every bottle before returning, since the app is about to exit
+ static func killBottlesBeforeExit() {
+ for bottle in BottleVM.shared.bottles where bottle.isAvailable {
do {
- try Wine.killBottle(bottle: bottle)
+ try Wine.stopAllProcessesBeforeExit(in: bottle)
} catch {
- print("Failed to kill bottle: \(error)")
+ Logger.wineKit.error("Failed to stop `\(bottle.settings.name)`: \(error)")
}
}
}
diff --git a/WhiskyCmd/Main.swift b/WhiskyCmd/Main.swift
index 53dce310c..a9b21a374 100644
--- a/WhiskyCmd/Main.swift
+++ b/WhiskyCmd/Main.swift
@@ -19,35 +19,40 @@
import Foundation
import WhiskyKit
import SwiftyTextTable
-import Progress
-import SemanticVersion
import ArgumentParser
@main
-struct Whisky: ParsableCommand {
+struct Whisky: AsyncParsableCommand {
static let configuration = CommandConfiguration(
- abstract: "A CLI interface for Whisky.",
+ commandName: "brandy",
+ abstract: "A command-line interface for Brandy.",
subcommands: [List.self,
Create.self,
Add.self,
-// Export.self,
Delete.self,
Remove.self,
Run.self,
- Shellenv.self
- /*Install.self,
- Uninstall.self*/])
+ Shellenv.self])
- static func main() {
+ static func main() async {
do {
try UpstreamDataMigration.migrateIfNeeded()
} catch {
FileHandle.standardError.write(Data("Failed to migrate data from upstream Whisky: \(error)\n".utf8))
}
- main(nil)
+ await main(nil)
}
}
+/// The bottle in Brandy's list with this name
+private func bottle(named name: String) throws -> Bottle {
+ var bottlesList = BottleData()
+ guard let bottle = bottlesList.loadBottles().first(where: { $0.settings.name == name }) else {
+ throw ValidationError("No bottle called \"\(name)\" found.")
+ }
+ return bottle
+}
+
extension Whisky {
struct List: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "List existing bottles.")
@@ -71,120 +76,92 @@ extension Whisky {
}
}
- struct Create: ParsableCommand {
+ struct Create: AsyncParsableCommand {
static let configuration = CommandConfiguration(abstract: "Create a new bottle.")
@Argument var name: String
+ @Option(help: "The Windows version: win10 or win11.") var windows = WinVersion.win10.rawValue
- mutating func run() throws {
- let bottleURL = BottleData.defaultBottleDir.appending(path: UUID().uuidString)
-
- do {
- try FileManager.default.createDirectory(atPath: bottleURL.path(percentEncoded: false),
- withIntermediateDirectories: true)
- let bottle = Bottle(bottleUrl: bottleURL, inFlight: true)
- // Should allow customisation
- bottle.settings.windowsVersion = .win10
- bottle.settings.name = name
-// try await Wine.changeWinVersion(bottle: bottle, win: winVersion)
-// let wineVer = try await Wine.wineVersion()
- bottle.settings.wineVersion = SemanticVersion(0, 0, 0)
-
- var bottlesList = BottleData()
- bottlesList.paths.append(bottleURL)
- print("Created new bottle \"\(name)\".")
- } catch {
- throw ValidationError("\(error)")
+ mutating func run() async throws {
+ guard let windowsVersion = WinVersion(rawValue: windows) else {
+ throw ValidationError("Unknown Windows version \"\(windows)\". Use win10 or win11.")
}
+
+ let bottleURL = BottleData.preferredBottleLocation.appending(path: UUID().uuidString)
+ _ = try await Bottle.create(at: bottleURL, name: name, windowsVersion: windowsVersion)
+ var bottlesList = BottleData()
+ bottlesList.paths.append(bottleURL)
+ print("Created bottle \"\(name)\".")
}
}
struct Add: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Add an existing bottle.")
- @Argument var path: String
+ @Argument(help: "The bottle's folder.") var path: String
mutating func run() throws {
- // Should be sanitised
- let bottleURL = URL(filePath: path)
- let settings = try BottleSettings.decode(from: bottleURL)
+ let bottleURL = URL(filePath: path).standardizedFileURL
+ let metadata = bottleURL.appending(path: "Metadata").appendingPathExtension("plist")
+ guard FileManager.default.fileExists(atPath: metadata.path(percentEncoded: false)) else {
+ throw ValidationError("\(path) is not a bottle: it has no Metadata.plist.")
+ }
+ let settings = try BottleSettings.decode(from: metadata)
+
var bottlesList = BottleData()
+ guard !bottlesList.paths.contains(bottleURL) else {
+ throw ValidationError("Bottle \"\(settings.name)\" is already in Brandy.")
+ }
bottlesList.paths.append(bottleURL)
print("Bottle \"\(settings.name)\" added.")
}
}
- struct Export: ParsableCommand {
- static let configuration = CommandConfiguration(abstract: "Export an existing bottle.")
-
- mutating func run() throws {
-// print("Create a bottle")
- }
- }
-
struct Delete: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Delete an existing bottle from disk.")
@Argument var name: String
mutating func run() throws {
- var bottlesList = BottleData()
- let bottles = bottlesList.loadBottles()
+ let bottleToDelete = try bottle(named: name)
+ try FileManager.default.removeItem(at: bottleToDelete.url)
- // Should ask for confirmation
- let bottleToRemove = bottles.first(where: { $0.settings.name == name })
- if let bottleToRemove = bottleToRemove {
- bottlesList.paths.removeAll(where: { $0 == bottleToRemove.url })
- do {
- try FileManager.default.removeItem(at: bottleToRemove.url)
- print("Deleted \"\(name)\".")
- } catch {
- print(error)
- }
- } else {
- throw ValidationError("No bottle called \"\(name)\" found.")
- }
+ var bottlesList = BottleData()
+ bottlesList.paths.removeAll { $0 == bottleToDelete.url }
+ print("Deleted \"\(name)\".")
}
}
struct Remove: ParsableCommand {
- static let configuration = CommandConfiguration(abstract: "Remove an existing bottle from Whisky.",
+ static let configuration = CommandConfiguration(abstract: "Remove an existing bottle from Brandy.",
discussion: "This will not remove the bottle from disk.")
@Argument var name: String
mutating func run() throws {
+ let bottleToRemove = try bottle(named: name)
var bottlesList = BottleData()
- let bottles = bottlesList.loadBottles()
-
- let bottleToRemove = bottles.first(where: { $0.settings.name == name })
- if let bottleToRemove = bottleToRemove {
- bottlesList.paths.removeAll(where: { $0 == bottleToRemove.url })
- print("Removed \"\(name)\".")
- } else {
- throw ValidationError("No bottle called \"\(name)\" found.")
- }
+ bottlesList.paths.removeAll { $0 == bottleToRemove.url }
+ print("Removed \"\(name)\".")
}
}
- struct Run: ParsableCommand {
- static let configuration = CommandConfiguration(abstract: "Run a program with Whisky.")
+ struct Run: AsyncParsableCommand {
+ static let configuration = CommandConfiguration(
+ abstract: "Run a program with Brandy.",
+ discussion: "Waits until the program, and anything it started, has exited."
+ )
@Argument var bottleName: String
- @Argument var path: String
- @Argument var args: [String] = []
-
- mutating func run() throws {
- var bottlesList = BottleData()
- let bottles = bottlesList.loadBottles()
+ @Argument(help: "The program, as a macOS path.") var path: String
+ @Argument(parsing: .captureForPassthrough, help: "Arguments for the program.") var args: [String] = []
- guard let bottle = bottles.first(where: { $0.settings.name == bottleName }) else {
- throw ValidationError("A bottle with that name doesn't exist.")
+ mutating func run() async throws {
+ let program = Program(url: URL(filePath: path).standardizedFileURL, bottle: try bottle(named: bottleName))
+ guard FileManager.default.fileExists(atPath: program.url.path(percentEncoded: false)) else {
+ throw ValidationError("\(path) does not exist.")
}
-
- let url = URL(fileURLWithPath: path)
- let program = Program(url: url, bottle: bottle)
- program.runInTerminal()
+ try await program.launch(extraArguments: args)
}
}
@@ -194,34 +171,7 @@ extension Whisky {
@Argument var bottleName: String
mutating func run() throws {
- var bottlesList = BottleData()
- let bottles = bottlesList.loadBottles()
-
- guard let bottle = bottles.first(where: { $0.settings.name == bottleName }) else {
- throw ValidationError("A bottle with that name doesn't exist.")
- }
-
- let envCmd = Wine.generateTerminalEnvironmentCommand(bottle: bottle)
- print(envCmd)
-
- }
- }
-
- struct Install: ParsableCommand {
- static let configuration = CommandConfiguration(abstract: "Install WhiskyWine.")
-
- mutating func run() throws {
-
- }
- }
-
- struct Uninstall: ParsableCommand {
- static let configuration = CommandConfiguration(abstract: "Uninstall WhiskyWine.")
-
- @Flag(name: [.long, .short], help: "Uninstall WhiskyWine") var whiskyWine = false
-
- mutating func run() throws {
-
+ print(Wine.generateTerminalEnvironmentCommand(bottle: try bottle(named: bottleName)))
}
}
}
diff --git a/WhiskyKit/Sources/WhiskyKit/Download/Download.swift b/WhiskyKit/Sources/WhiskyKit/Download/Download.swift
new file mode 100644
index 000000000..ee41421a6
--- /dev/null
+++ b/WhiskyKit/Sources/WhiskyKit/Download/Download.swift
@@ -0,0 +1,125 @@
+//
+// Download.swift
+// WhiskyKit
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import Foundation
+import os
+
+public enum DownloadError: LocalizedError {
+ case missingResponse
+ case unexpectedStatus(Int)
+
+ public var errorDescription: String? {
+ switch self {
+ case .missingResponse:
+ return "The server did not respond."
+ case .unexpectedStatus(let status):
+ return "The server responded with HTTP \(status)."
+ }
+ }
+}
+
+public struct DownloadProgress: Sendable, Equatable {
+ public let receivedBytes: Int64
+ /// `nil` when the server does not say how large the file is
+ public let expectedBytes: Int64?
+
+ public init(receivedBytes: Int64, expectedBytes: Int64?) {
+ self.receivedBytes = receivedBytes
+ self.expectedBytes = expectedBytes
+ }
+
+ public var fraction: Double? {
+ guard let expectedBytes, expectedBytes > 0 else { return nil }
+ return min(Double(receivedBytes) / Double(expectedBytes), 1)
+ }
+}
+
+public enum Download {
+ /// Downloads `url` into a new temporary folder, keeping its file name, and returns the file's location
+ public static func file(
+ from url: URL, progress: @escaping @Sendable (DownloadProgress) -> Void
+ ) async throws -> URL {
+ let delegate = DownloadDelegate(progress: progress)
+ let session = URLSession(configuration: .ephemeral, delegate: delegate, delegateQueue: nil)
+ defer { session.finishTasksAndInvalidate() }
+
+ let task = session.downloadTask(with: url)
+ return try await withTaskCancellationHandler {
+ try await withCheckedThrowingContinuation { continuation in
+ delegate.start(continuation)
+ task.resume()
+ }
+ } onCancel: {
+ task.cancel()
+ }
+ }
+
+ /// Validates a finished download and moves it out of URLSession's temporary location,
+ /// which is deleted as soon as the delegate callback returns
+ static func accept(_ location: URL, from source: URL, response: URLResponse?) throws -> URL {
+ guard let response = response as? HTTPURLResponse else {
+ throw DownloadError.missingResponse
+ }
+ guard response.statusCode == 200 else {
+ throw DownloadError.unexpectedStatus(response.statusCode)
+ }
+
+ let folder = FileManager.default.temporaryDirectory.appending(path: UUID().uuidString)
+ try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
+ let destination = folder.appending(path: source.lastPathComponent)
+ try FileManager.default.moveItem(at: location, to: destination)
+ return destination
+ }
+}
+
+private final class DownloadDelegate: NSObject, URLSessionDownloadDelegate, Sendable {
+ private let progress: @Sendable (DownloadProgress) -> Void
+ private let continuation = OSAllocatedUnfairLock?>(initialState: nil)
+
+ init(progress: @escaping @Sendable (DownloadProgress) -> Void) {
+ self.progress = progress
+ }
+
+ func start(_ continuation: CheckedContinuation) {
+ self.continuation.withLock { $0 = continuation }
+ }
+
+ private func finish(_ result: Result) {
+ let continuation = continuation.withLock { stored in
+ defer { stored = nil }
+ return stored
+ }
+ continuation?.resume(with: result)
+ }
+
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64,
+ totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
+ let expected = totalBytesExpectedToWrite > 0 ? totalBytesExpectedToWrite : nil
+ progress(DownloadProgress(receivedBytes: totalBytesWritten, expectedBytes: expected))
+ }
+
+ func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
+ let source = downloadTask.originalRequest?.url ?? location
+ finish(Result { try Download.accept(location, from: source, response: downloadTask.response) })
+ }
+
+ func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+ guard let error else { return }
+ finish(.failure(error))
+ }
+}
diff --git a/WhiskyKit/Sources/WhiskyKit/Extensions/Bundle+Extensions.swift b/WhiskyKit/Sources/WhiskyKit/Extensions/Bundle+Extensions.swift
index 96dd732a3..67d17165e 100644
--- a/WhiskyKit/Sources/WhiskyKit/Extensions/Bundle+Extensions.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Extensions/Bundle+Extensions.swift
@@ -20,7 +20,7 @@ import Foundation
public extension Bundle {
static var whiskyBundleIdentifier: String {
- return Bundle.main.bundleIdentifier ?? "com.istorry.Whisky"
+ return Bundle.main.bundleIdentifier ?? "com.istorry.Brandy"
}
/// The identifier upstream Whisky stored its data under, before this fork
diff --git a/WhiskyKit/Sources/WhiskyKit/Extensions/FileManager+Extensions.swift b/WhiskyKit/Sources/WhiskyKit/Extensions/FileManager+Extensions.swift
index a6f059235..d3a625f2a 100644
--- a/WhiskyKit/Sources/WhiskyKit/Extensions/FileManager+Extensions.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Extensions/FileManager+Extensions.swift
@@ -26,7 +26,7 @@ extension FileManager {
at: sourceDirectory, includingPropertiesForKeys: [.isRegularFileKey])
while let fileURL = enumerator?.nextObject() as? URL {
- guard fileURL.pathExtension == "dll" else { return }
+ guard fileURL.pathExtension == "dll" else { continue }
let originalURL = destinationDirectory.appending(path: fileURL.lastPathComponent)
try FileManager.default.replaceFile(at: originalURL, with: fileURL, makeOriginalCopy: makeOriginalCopy)
}
diff --git a/WhiskyKit/Sources/WhiskyKit/Extensions/Program+Extensions.swift b/WhiskyKit/Sources/WhiskyKit/Extensions/Program+Extensions.swift
index 8d589187f..0c899e011 100644
--- a/WhiskyKit/Sources/WhiskyKit/Extensions/Program+Extensions.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Extensions/Program+Extensions.swift
@@ -30,14 +30,9 @@ extension Program {
}
func runInWine() {
- let arguments = settings.arguments.split { $0.isWhitespace }.map(String.init)
- let environment = generateEnvironment()
-
Task.detached(priority: .userInitiated) {
do {
- try await Wine.runProgram(
- at: self.url, args: arguments, bottle: self.bottle, environment: environment
- )
+ try await self.launch()
} catch {
await MainActor.run {
self.showRunError(message: error.localizedDescription)
@@ -46,31 +41,31 @@ extension Program {
}
}
- public func generateTerminalCommand() -> String {
+ /// Runs the program with its settings, plus `extraArguments`, and returns once it, and anything it
+ /// started, has exited
+ public func launch(extraArguments: [String] = []) async throws {
+ let arguments = try SteamInstaller.prepareLaunch(of: url, in: bottle)
+ + settings.arguments.split { $0.isWhitespace }.map(String.init)
+ + extraArguments
+ try await Wine.runProgram(at: url, args: arguments, bottle: bottle, environment: generateEnvironment())
+ }
+
+ /// A shell command that runs the program with its settings, for Terminal and for shortcut apps
+ public func generateTerminalCommand() throws -> String {
+ let arguments = try SteamInstaller.prepareLaunch(of: url, in: bottle).map(\.shellQuoted)
+ + [settings.arguments]
return Wine.generateRunCommand(
- at: self.url, bottle: bottle, args: settings.arguments, environment: generateEnvironment()
+ at: url, bottle: bottle, args: arguments.joined(separator: " "), environment: generateEnvironment()
)
}
public func runInTerminal() {
- let wineCmd = generateTerminalCommand().replacingOccurrences(of: "\\", with: "\\\\")
-
- let script = """
- tell application "Terminal"
- activate
- do script "\(wineCmd)"
- end tell
- """
-
- Task.detached(priority: .userInitiated) {
- var error: NSDictionary?
- guard let appleScript = NSAppleScript(source: script) else { return }
- appleScript.executeAndReturnError(&error)
-
- if let error = error {
- Logger.wineKit.error("Failed to run terminal script \(error)")
- guard let description = error["NSAppleScriptErrorMessage"] as? String else { return }
- await self.showRunError(message: String(describing: description))
+ Task { @MainActor in
+ do {
+ try Terminal.run(generateTerminalCommand())
+ } catch {
+ Logger.wineKit.error("Failed to run `\(self.name)` in Terminal: \(error)")
+ showRunError(message: error.localizedDescription)
}
}
}
diff --git a/WhiskyKit/Sources/WhiskyKit/Extensions/URL+Extensions.swift b/WhiskyKit/Sources/WhiskyKit/Extensions/URL+Extensions.swift
index 1bf530e2d..37fb9eea2 100644
--- a/WhiskyKit/Sources/WhiskyKit/Extensions/URL+Extensions.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Extensions/URL+Extensions.swift
@@ -19,26 +19,23 @@
import Foundation
extension String {
- public var esc: String {
- let esc = ["\\", "\"", "'", " ", "(", ")", "[", "]", "{", "}", "&", "|",
- ";", "<", ">", "`", "$", "!", "*", "?", "#", "~", "="]
- var str = self
- for char in esc {
- str = str.replacingOccurrences(of: char, with: "\\" + char)
- }
- return str
+ /// Quoted for a POSIX shell: single quotes, with any single quote written as '\''
+ public var shellQuoted: String {
+ "'" + replacingOccurrences(of: "'", with: #"'\''"#) + "'"
+ }
+
+ /// Quoted as an AppleScript string literal
+ public var appleScriptQuoted: String {
+ "\"" + replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\""
}
}
extension URL {
- public var esc: String {
- path.esc
- }
public func prettyPath() -> String {
var prettyPath = path(percentEncoded: false)
prettyPath = prettyPath
- .replacingOccurrences(of: Bundle.main.bundleIdentifier ?? Bundle.whiskyBundleIdentifier, with: "Whisky")
+ .replacingOccurrences(of: Bundle.main.bundleIdentifier ?? Bundle.whiskyBundleIdentifier, with: "Brandy")
.replacingOccurrences(of: "/Users/\(NSUserName())", with: "~")
return prettyPath
}
diff --git a/WhiskyKit/Sources/WhiskyKit/Logging/LogFile.swift b/WhiskyKit/Sources/WhiskyKit/Logging/LogFile.swift
index 1dd8bf7cc..7c7583701 100644
--- a/WhiskyKit/Sources/WhiskyKit/Logging/LogFile.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Logging/LogFile.swift
@@ -91,7 +91,7 @@ public extension LogFile {
var header = String()
let macOSVersion = ProcessInfo.processInfo.operatingSystemVersion
- header += "Whisky Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "")\n"
+ header += "Brandy Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "")\n"
header += "Date: \(ISO8601DateFormatter().string(from: Date.now))\n"
header += "macOS Version: \(macOSVersion.majorVersion).\(macOSVersion.minorVersion).\(macOSVersion.patchVersion)\n\n"
write(header)
@@ -120,8 +120,7 @@ public extension LogFile {
if let version = WhiskyWineInstaller.whiskyWineVersion() {
header += "WhiskyWine Version: \(version.major).\(version.minor).\(version.patch)\n"
}
- header += "Windows Version: \(bottle.settings.windowsVersion)\n"
- header += "Enhanced Sync: \(bottle.settings.enhancedSync)\n\n"
+ header += "Windows Version: \(bottle.settings.windowsVersion)\n\n"
header += "Metal HUD: \(bottle.settings.metalHud)\n"
header += "Metal Trace: \(bottle.settings.metalTrace)\n\n"
diff --git a/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift b/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift
index 32156e0de..0fe9a2048 100644
--- a/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift
+++ b/WhiskyKit/Sources/WhiskyKit/PE/PortableExecutable.swift
@@ -18,6 +18,7 @@
import Foundation
import AppKit
+import os
public struct PEError: Error {
public let message: String
@@ -144,7 +145,7 @@ public struct PEFile: Hashable, Equatable, Sendable {
}
/// The best icon for this executable
- /// - Returns: An `NSImage` if there is a renderable icon in the resource directory table
+ /// - Returns: The largest renderable icon in the resource directory table, or `nil` if there is none
public func bestIcon() -> NSImage? {
guard let handle = try? FileHandle(forReadingFrom: url) else {
return nil
@@ -169,7 +170,8 @@ public struct PEFile: Hashable, Equatable, Sendable {
}
}
} catch {
- print("Failed to get icon")
+ // Skip this entry; the executable's other icons may still render
+ Logger.wineKit.debug("Unreadable icon in `\(url.lastPathComponent)`: \(error)")
}
} else if bitmapInfo.colorFormat != .unknown {
return bitmapInfo.renderBitmap(handle: handle, offset: UInt64(offset + bitmapInfo.size))
@@ -179,10 +181,6 @@ public struct PEFile: Hashable, Equatable, Sendable {
}
.filter { $0.isValid }
- if !icons.isEmpty {
- return icons.max(by: { $0.size.height < $1.size.height })
- } else {
- return NSImage()
- }
+ return icons.max(by: { $0.size.height < $1.size.height })
}
}
diff --git a/WhiskyKit/Sources/WhiskyKit/ShellLink.swift b/WhiskyKit/Sources/WhiskyKit/ShellLink.swift
index 8c7299640..a9dccd772 100644
--- a/WhiskyKit/Sources/WhiskyKit/ShellLink.swift
+++ b/WhiskyKit/Sources/WhiskyKit/ShellLink.swift
@@ -103,7 +103,8 @@ public struct LinkInfo: Hashable {
try handle.seek(toOffset: offset)
if let pathData = try handle.readToEnd() {
if let nullRange = pathData.firstIndex(of: 0) {
- let encoding: String.Encoding = unicode ? .utf16 : .windowsCP1254
+ // Non-Unicode shortcuts store the path in the ANSI code page, Windows-1252 on Western systems
+ let encoding: String.Encoding = unicode ? .utf16 : .windowsCP1252
if var string = String(data: pathData[.. URL {
+ bottle.url.appending(path: steamFolderPath)
+ }
+
+ private static func webHelperFolder(in bottle: Bottle) -> URL {
+ steamFolder(in: bottle).appending(path: "bin").appending(path: "cef").appending(path: "cef.win64")
+ }
+
+ public static func steamExecutable(in bottle: Bottle) -> URL {
+ steamFolder(in: bottle).appending(path: executableName)
+ }
+
+ /// True once Steam's first self-update (about 330 MB) has installed the client and its web helper
+ public static func isFirstUpdateComplete(in bottle: Bottle) -> Bool {
+ let manifest = steamFolder(in: bottle)
+ .appending(path: "package")
+ .appending(path: "steam_client_win64.installed")
+ let webHelper = webHelperFolder(in: bottle).appending(path: webHelperName)
+ return FileManager.default.fileExists(atPath: manifest.path(percentEncoded: false))
+ && FileManager.default.fileExists(atPath: webHelper.path(percentEncoded: false))
+ }
+
+ /// Whether Steam's client is running in the bottle, however it was started
+ public static func isRunning(in bottle: Bottle) -> Bool {
+ !WineProcesses.processIDs(in: bottle, named: executableName).isEmpty
+ }
+
+ /// Whether `program` is the Steam client of its bottle
+ public static func isSteam(_ program: URL, in bottle: Bottle) -> Bool {
+ program.standardizedFileURL.path(percentEncoded: false)
+ .caseInsensitiveCompare(steamExecutable(in: bottle).standardizedFileURL.path(percentEncoded: false))
+ == .orderedSame
+ }
+
+ public static func isInstalled(in bottle: Bottle) -> Bool {
+ FileManager.default.fileExists(atPath: steamExecutable(in: bottle).path(percentEncoded: false))
+ }
+
+ /// The bottle Steam is installed in, if any
+ public static func steamBottle(in bottles: [Bottle]) -> Bottle? {
+ bottles.first { $0.isAvailable && isInstalled(in: $0) }
+ }
+
+ public static func downloadInstaller(
+ progress: @escaping @Sendable (DownloadProgress) -> Void
+ ) async throws -> URL {
+ try await Download.file(from: installerURL, progress: progress)
+ }
+
+ /// Runs the installer silently and waits for it to finish
+ public static func install(in bottle: Bottle, using installer: URL) async throws {
+ defer {
+ do {
+ try FileManager.default.removeItem(at: installer)
+ } catch {
+ Logger.wineKit.error("Failed to remove the Steam installer: \(error)")
+ }
+ }
+
+ let output = try await Wine.runWine([installer.path(percentEncoded: false), "/S"], bottle: bottle)
+ guard isInstalled(in: bottle) else {
+ throw SteamInstallerError.notInstalled(installerOutput: output)
+ }
+ }
+
+ /// Runs Steam until its first self-update finishes, then stops it and installs the web helper shim.
+ /// Steam reports no progress, so the size of its download folder is checked every few seconds.
+ public static func completeFirstUpdate(
+ in bottle: Bottle, progress: @escaping @Sendable (_ downloadedBytes: Int64) -> Void
+ ) async throws {
+ if !isFirstUpdateComplete(in: bottle) {
+ let steam = Task {
+ try await Wine.runWine([steamExecutable(in: bottle).path(percentEncoded: false)] + launchArguments,
+ bottle: bottle)
+ }
+ do {
+ try await waitForFirstUpdate(in: bottle, progress: progress)
+ try await Wine.stopAllProcesses(in: bottle)
+ _ = await steam.result
+ } catch {
+ try await Wine.stopAllProcesses(in: bottle)
+ throw error
+ }
+ }
+ try installWebHelperShim(in: bottle)
+ }
+
+ private static func waitForFirstUpdate(
+ in bottle: Bottle, progress: @Sendable (Int64) -> Void
+ ) async throws {
+ let downloads = steamFolder(in: bottle).appending(path: "package")
+ var downloaded: Int64 = -1
+ var lastGrowth = ContinuousClock.now
+ while !isFirstUpdateComplete(in: bottle) {
+ try await Task.sleep(for: .seconds(2))
+ let size = folderSize(downloads)
+ if size != downloaded {
+ downloaded = size
+ lastGrowth = .now
+ progress(size)
+ } else if ContinuousClock.now - lastGrowth > firstUpdateStallTimeout {
+ throw SteamInstallerError.firstUpdateStalled
+ }
+ }
+ }
+
+ private static func folderSize(_ folder: URL) -> Int64 {
+ let keys: Set = [.fileSizeKey, .isRegularFileKey]
+ let enumerator = FileManager.default.enumerator(at: folder, includingPropertiesForKeys: Array(keys))
+ var total: Int64 = 0
+ while let file = enumerator?.nextObject() as? URL {
+ // Steam renames files while it unpacks them; one that vanished is simply not counted
+ guard let values = try? file.resourceValues(forKeys: keys) else { continue }
+ if values.isRegularFile == true {
+ total += Int64(values.fileSize ?? 0)
+ }
+ }
+ return total
+ }
+
+ /// Puts the shim in place of Steam's web helper, keeping Steam's as steamwebhelper_real.exe.
+ /// Run before every launch: Steam updates put its own helper back.
+ public static func installWebHelperShim(in bottle: Bottle, shim: URL = webHelperShim) throws {
+ let fileManager = FileManager.default
+ guard fileManager.fileExists(atPath: shim.path(percentEncoded: false)) else {
+ throw SteamInstallerError.webHelperShimMissing
+ }
+ let folder = webHelperFolder(in: bottle)
+ let helper = folder.appending(path: webHelperName)
+ let real = folder.appending(path: realWebHelperName)
+ guard fileManager.fileExists(atPath: helper.path(percentEncoded: false)) else {
+ throw SteamInstallerError.webHelperMissing
+ }
+
+ if try isShim(helper) {
+ guard fileManager.fileExists(atPath: real.path(percentEncoded: false)) else {
+ throw SteamInstallerError.webHelperMissing
+ }
+ guard !fileManager.contentsEqual(atPath: helper.path(percentEncoded: false),
+ andPath: shim.path(percentEncoded: false)) else { return }
+ try fileManager.removeItem(at: helper)
+ } else {
+ if fileManager.fileExists(atPath: real.path(percentEncoded: false)) {
+ try fileManager.removeItem(at: real)
+ }
+ try fileManager.moveItem(at: helper, to: real)
+ }
+ try fileManager.copyItem(at: shim, to: helper)
+ }
+
+ private static func isShim(_ file: URL) throws -> Bool {
+ try Data(contentsOf: file, options: .alwaysMapped).range(of: shimMarker) != nil
+ }
+
+ /// Arguments to add when starting `program`: for Steam, after making sure its interface can draw
+ public static func prepareLaunch(of program: URL, in bottle: Bottle) throws -> [String] {
+ guard isSteam(program, in: bottle) else { return [] }
+ if isFirstUpdateComplete(in: bottle) {
+ try installWebHelperShim(in: bottle)
+ }
+ return launchArguments
+ }
+}
diff --git a/WhiskyKit/Sources/WhiskyKit/Utils/Terminal.swift b/WhiskyKit/Sources/WhiskyKit/Utils/Terminal.swift
new file mode 100644
index 000000000..6b711dc9f
--- /dev/null
+++ b/WhiskyKit/Sources/WhiskyKit/Utils/Terminal.swift
@@ -0,0 +1,56 @@
+//
+// Terminal.swift
+// WhiskyKit
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import Foundation
+
+public enum TerminalError: LocalizedError {
+ case scriptFailed(String)
+
+ public var errorDescription: String? {
+ switch self {
+ case .scriptFailed(let message):
+ return message
+ }
+ }
+}
+
+/// Opens Terminal windows running shell commands
+public enum Terminal {
+ /// The AppleScript that opens Terminal and runs `command`
+ static func script(running command: String) -> String {
+ """
+ tell application "Terminal"
+ activate
+ do script \(command.appleScriptQuoted)
+ end tell
+ """
+ }
+
+ /// Opens a Terminal window running `command`, a shell command whose own arguments are already quoted
+ @MainActor
+ public static func run(_ command: String) throws {
+ guard let script = NSAppleScript(source: script(running: command)) else {
+ throw TerminalError.scriptFailed("The Terminal script could not be created.")
+ }
+ var error: NSDictionary?
+ script.executeAndReturnError(&error)
+ if let error {
+ throw TerminalError.scriptFailed(error[NSAppleScript.errorMessage] as? String ?? "\(error)")
+ }
+ }
+}
diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle+Creation.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle+Creation.swift
new file mode 100644
index 000000000..7c68fb289
--- /dev/null
+++ b/WhiskyKit/Sources/WhiskyKit/Whisky/Bottle+Creation.swift
@@ -0,0 +1,86 @@
+//
+// Bottle+Creation.swift
+// WhiskyKit
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import Foundation
+import os
+import SemanticVersion
+
+public enum BottleCreationError: LocalizedError {
+ case whiskyWineNotInstalled
+ case unsupportedLocation(URL)
+ case prefixNotCreated(wineOutput: String)
+
+ public var errorDescription: String? {
+ switch self {
+ case .whiskyWineNotInstalled:
+ return "BrandyWine is not installed. Install it from Brandy > Setup, then try again."
+ case .unsupportedLocation(let url):
+ return "Wine can't use folders whose names end with a period. Choose a location other than "
+ + url.prettyPath() + "."
+ case .prefixNotCreated(let wineOutput):
+ return "Wine did not set up the bottle. Wine output: \(wineOutput.suffix(1000))"
+ }
+ }
+}
+
+extension Bottle {
+ /// Creates a bottle at `url` and returns once Wine has set it up. Nothing is left behind if that fails.
+ /// `whenStarted` receives the bottle as soon as its folder exists, e.g. to show it while Wine works.
+ @MainActor
+ public static func create(
+ at url: URL, name: String, windowsVersion: WinVersion, whenStarted: (Bottle) -> Void = { _ in }
+ ) async throws -> Bottle {
+ guard WhiskyWineInstaller.isSupportedRuntimeInstalled() else {
+ throw BottleCreationError.whiskyWineNotInstalled
+ }
+ // Wine drops trailing dots when mapping Unix paths to Windows ones, so the prefix cannot load
+ guard !url.pathComponents.contains(where: { $0.hasSuffix(".") }) else {
+ throw BottleCreationError.unsupportedLocation(url)
+ }
+
+ try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
+ do {
+ let bottle = Bottle(bottleUrl: url, inFlight: true)
+ whenStarted(bottle)
+ bottle.settings.windowsVersion = windowsVersion
+ bottle.settings.name = name
+
+ let wineOutput = try await Wine.changeWinVersion(bottle: bottle, win: windowsVersion)
+ let driveC = url.appending(path: "drive_c").path(percentEncoded: false)
+ guard FileManager.default.fileExists(atPath: driveC) else {
+ throw BottleCreationError.prefixNotCreated(wineOutput: wineOutput)
+ }
+
+ let wineVersion = try await Wine.wineVersion()
+ bottle.settings.wineVersion = SemanticVersion(wineVersion) ?? SemanticVersion(0, 0, 0)
+ bottle.inFlight = false
+ return bottle
+ } catch {
+ removeIncompleteBottle(at: url)
+ throw error
+ }
+ }
+
+ private static func removeIncompleteBottle(at url: URL) {
+ do {
+ try FileManager.default.removeItem(at: url)
+ } catch {
+ Logger.wineKit.error("Failed to remove incomplete bottle `\(url.path(percentEncoded: false))`: \(error)")
+ }
+ }
+}
diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/BottleData.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/BottleData.swift
index ce47d62c0..686df1eac 100644
--- a/WhiskyKit/Sources/WhiskyKit/Whisky/BottleData.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Whisky/BottleData.swift
@@ -32,6 +32,11 @@ public struct BottleData: Codable {
/// `UserDefaults` key for the folder new bottles are created in
public static let defaultBottleLocationKey = "defaultBottleLocation"
+ /// The folder the user chose for new bottles, or the default one
+ public static var preferredBottleLocation: URL {
+ UserDefaults.standard.url(forKey: defaultBottleLocationKey) ?? defaultBottleDir
+ }
+
static let currentVersion = SemanticVersion(1, 0, 0)
static func containerDir(for bundleIdentifier: String) -> URL {
diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/BottleSettings.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/BottleSettings.swift
index 8d8866c74..d3485a849 100644
--- a/WhiskyKit/Sources/WhiskyKit/Whisky/BottleSettings.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Whisky/BottleSettings.swift
@@ -60,23 +60,11 @@ public struct BottleInfo: Codable, Equatable {
}
public enum WinVersion: String, CaseIterable, Codable, Sendable {
- case winXP = "winxp64"
- case win7 = "win7"
- case win8 = "win8"
- case win81 = "win81"
- case win10 = "win10"
- case win11 = "win11"
+ case win10
+ case win11
public func pretty() -> String {
switch self {
- case .winXP:
- return "Windows XP"
- case .win7:
- return "Windows 7"
- case .win8:
- return "Windows 8"
- case .win81:
- return "Windows 8.1"
case .win10:
return "Windows 10"
case .win11:
@@ -85,16 +73,17 @@ public enum WinVersion: String, CaseIterable, Codable, Sendable {
}
}
-public enum EnhancedSync: Codable, Equatable {
- case none, esync, msync
-}
-
public struct BottleWineConfig: Codable, Equatable {
- static let defaultWineVersion = SemanticVersion(7, 7, 0)
+ static let defaultWineVersion = SemanticVersion(11, 0, 0)
var wineVersion: SemanticVersion = Self.defaultWineVersion
var windowsVersion: WinVersion = .win10
- var enhancedSync: EnhancedSync = .msync
var avxEnabled: Bool = false
+ /// Set when the stored Windows version is one Whisky no longer offers, such as Windows 7
+ var migratedFromLegacyWindowsVersion = false
+
+ private enum CodingKeys: String, CodingKey {
+ case wineVersion, windowsVersion, avxEnabled
+ }
public init() {}
@@ -102,9 +91,14 @@ public struct BottleWineConfig: Codable, Equatable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.wineVersion = try container.decodeIfPresent(SemanticVersion.self, forKey: .wineVersion) ?? Self.defaultWineVersion
- self.windowsVersion = try container.decodeIfPresent(WinVersion.self, forKey: .windowsVersion) ?? .win10
- self.enhancedSync = try container.decodeIfPresent(EnhancedSync.self, forKey: .enhancedSync) ?? .msync
self.avxEnabled = try container.decodeIfPresent(Bool.self, forKey: .avxEnabled) ?? false
+
+ // A strict decode would fail the whole metadata file and reset the bottle's name and pins
+ let storedWindowsVersion = try container.decodeIfPresent(String.self, forKey: .windowsVersion)
+ self.windowsVersion = storedWindowsVersion.flatMap(WinVersion.init(rawValue:)) ?? .win10
+ if let storedWindowsVersion, WinVersion(rawValue: storedWindowsVersion) == nil {
+ self.migratedFromLegacyWindowsVersion = true
+ }
}
// swiftlint:enable line_length
}
@@ -185,7 +179,16 @@ public struct BottleSettings: Codable, Equatable {
/// The version of windows used by this bottle
public var windowsVersion: WinVersion {
get { return wineConfig.windowsVersion }
- set { wineConfig.windowsVersion = newValue }
+ set {
+ wineConfig.windowsVersion = newValue
+ wineConfig.migratedFromLegacyWindowsVersion = false
+ }
+ }
+
+ /// The bottle was set to a Windows version Whisky no longer offers and now reads as Windows 10,
+ /// but its Wine prefix has not been switched yet
+ public var hasLegacyWindowsVersion: Bool {
+ wineConfig.migratedFromLegacyWindowsVersion
}
public var avxEnabled: Bool {
@@ -205,11 +208,6 @@ public struct BottleSettings: Codable, Equatable {
set { info.blocklist = newValue }
}
- public var enhancedSync: EnhancedSync {
- get { return wineConfig.enhancedSync }
- set { wineConfig.enhancedSync = newValue }
- }
-
public var metalHud: Bool {
get { return metalConfig.metalHud }
set { metalConfig.metalHud = newValue }
@@ -277,7 +275,6 @@ public struct BottleSettings: Codable, Equatable {
try data.write(to: metadataUrl)
}
- // swiftlint:disable:next cyclomatic_complexity
public func environmentVariables(wineEnv: inout [String: String]) {
if dxvk {
wineEnv.updateValue("dxgi,d3d9,d3d10core,d3d11=n,b", forKey: "WINEDLLOVERRIDES")
@@ -297,19 +294,6 @@ public struct BottleSettings: Codable, Equatable {
wineEnv.updateValue("1", forKey: "DXVK_ASYNC")
}
- switch enhancedSync {
- case .none:
- break
- case .esync:
- wineEnv.updateValue("1", forKey: "WINEESYNC")
- case .msync:
- wineEnv.updateValue("1", forKey: "WINEMSYNC")
- // D3DM detects ESYNC and changes behaviour accordingly
- // so we have to lie to it so that it doesn't break
- // under MSYNC. Values hardcoded in lid3dshared.dylib
- wineEnv.updateValue("1", forKey: "WINEESYNC")
- }
-
if metalHud {
wineEnv.updateValue("1", forKey: "MTL_HUD_ENABLED")
}
diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift
index 4b5445553..b49d056cf 100644
--- a/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Whisky/Program.swift
@@ -63,10 +63,6 @@ public final class Program: ObservableObject, Equatable, Hashable, Identifiable,
self.settingsURL = settingsUrl
do {
- if !FileManager.default.fileExists(atPath: settingsFolder.path(percentEncoded: false)) {
- try FileManager.default.createDirectory(at: settingsFolder, withIntermediateDirectories: true)
- }
-
self.settings = try ProgramSettings.decode(from: settingsUrl)
} catch {
Logger.wineKit.error("Failed to load settings for `\(name)`: \(error)")
@@ -90,7 +86,14 @@ public final class Program: ObservableObject, Equatable, Hashable, Identifiable,
/// Save the settings to file
private func saveSettings() {
+ // A program can outlive its bottle in the UI; saving must not recreate a deleted bottle's folder
+ guard FileManager.default.fileExists(atPath: bottle.url.path(percentEncoded: false)) else { return }
+
do {
+ let settingsFolder = settingsURL.deletingLastPathComponent()
+ if !FileManager.default.fileExists(atPath: settingsFolder.path(percentEncoded: false)) {
+ try FileManager.default.createDirectory(at: settingsFolder, withIntermediateDirectories: false)
+ }
try settings.encode(to: settingsURL)
} catch {
Logger.wineKit.error("Failed to save settings for `\(self.name)`: \(error)")
diff --git a/WhiskyKit/Sources/WhiskyKit/Whisky/ProgramSettings.swift b/WhiskyKit/Sources/WhiskyKit/Whisky/ProgramSettings.swift
index 257629aa8..be50adf51 100644
--- a/WhiskyKit/Sources/WhiskyKit/Whisky/ProgramSettings.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Whisky/ProgramSettings.swift
@@ -73,9 +73,7 @@ public struct ProgramSettings: Codable {
static func decode(from settingsURL: URL) throws -> ProgramSettings {
guard FileManager.default.fileExists(atPath: settingsURL.path(percentEncoded: false)) else {
- let settings = ProgramSettings()
- try settings.encode(to: settingsURL)
- return settings
+ return ProgramSettings()
}
let data = try Data(contentsOf: settingsURL)
diff --git a/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift b/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift
index ceaf6bb99..7c4446135 100644
--- a/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift
+++ b/WhiskyKit/Sources/WhiskyKit/WhiskyWine/WhiskyWineInstaller.swift
@@ -21,16 +21,10 @@ import os
import SemanticVersion
public enum WhiskyWineInstallerError: LocalizedError {
- case missingResponse
- case unexpectedStatus(Int)
case missingVersionInfo
public var errorDescription: String? {
switch self {
- case .missingResponse:
- return "The server did not respond."
- case .unexpectedStatus(let status):
- return "The server responded with HTTP \(status)."
case .missingVersionInfo:
return "The downloaded archive does not contain a WhiskyWine version file."
}
@@ -38,7 +32,7 @@ public enum WhiskyWineInstallerError: LocalizedError {
}
public class WhiskyWineInstaller {
- /// The Whisky application folder
+ /// The app's folder in Application Support
public static let applicationFolder = applicationFolder(for: Bundle.whiskyBundleIdentifier)
/// The folder of all the library files
@@ -47,16 +41,22 @@ public class WhiskyWineInstaller {
/// URL to the installed `wine` `bin` directory
public static let binFolder: URL = libraryFolder.appending(path: "Wine").appending(path: "bin")
- /// Assets of the `whiskywine` release, which `scripts/publish-whiskywine.sh` replaces on every runtime update
+ /// The runtime major version this app runs; Wine's layout changes between majors
+ public static let supportedMajorVersion = 3
+ /// The first runtime with everything this app relies on: 3.1 added the Steam web helper shim
+ private static let minimumVersion = SemanticVersion(3, 1, 0)
+
+ /// Assets of the release for the supported major version, which `scripts/publish-whiskywine.sh` publishes
private static let distributionURL: URL = {
- guard let url = URL(string: "https://github.com/iStorry/Whisky/releases/download/whiskywine/") else {
+ let release = "https://github.com/iStorry/Whisky/releases/download/whiskywine-\(supportedMajorVersion)/"
+ guard let url = URL(string: release) else {
preconditionFailure("Invalid WhiskyWine distribution URL")
}
return url
}()
/// The archive holding the `Libraries` folder
- public static let librariesArchiveURL = distributionURL.appending(path: "Libraries.tar.gz")
+ private static let librariesArchiveURL = distributionURL.appending(path: "Libraries.tar.gz")
private static let remoteVersionURL = distributionURL.appending(path: "WhiskyWineVersion.plist")
@@ -73,24 +73,15 @@ public class WhiskyWineInstaller {
return whiskyWineVersion() != nil
}
- /// Validates a finished `URLSessionDownloadTask` and moves its file out of the session's temporary
- /// location, which is deleted as soon as the completion handler returns
- public static func acceptDownload(at location: URL?, response: URLResponse?, error: Error?) throws -> URL {
- if let error {
- throw error
- }
- guard let location, let response = response as? HTTPURLResponse else {
- throw WhiskyWineInstallerError.missingResponse
- }
- guard response.statusCode == 200 else {
- throw WhiskyWineInstallerError.unexpectedStatus(response.statusCode)
- }
+ /// Installed, of the major version this app runs, and new enough to have everything it relies on
+ public static func isSupportedRuntimeInstalled() -> Bool {
+ guard let version = whiskyWineVersion() else { return false }
+ return version.major == supportedMajorVersion && version >= minimumVersion
+ }
- let destination = FileManager.default.temporaryDirectory
- .appending(path: UUID().uuidString)
- .appendingPathExtension("tar.gz")
- try FileManager.default.moveItem(at: location, to: destination)
- return destination
+ /// Downloads the runtime archive for `install(from:)`
+ public static func download(progress: @escaping @Sendable (DownloadProgress) -> Void) async throws -> URL {
+ try await Download.file(from: librariesArchiveURL, progress: progress)
}
/// Installs the archive, replacing the current `Libraries` folder only once the new one is extracted and valid
@@ -130,24 +121,16 @@ public class WhiskyWineInstaller {
}
}
- public static func uninstall() {
- do {
- try FileManager.default.removeItem(at: libraryFolder)
- } catch {
- print("Failed to uninstall WhiskyWine: \(error)")
- }
- }
-
/// The newer WhiskyWine version available for download, or `nil` when the installed one is current
public static func availableUpdate() async throws -> SemanticVersion? {
guard let localVersion = whiskyWineVersion() else { return nil }
let (data, response) = try await URLSession(configuration: .ephemeral).data(from: remoteVersionURL)
guard let response = response as? HTTPURLResponse else {
- throw WhiskyWineInstallerError.missingResponse
+ throw DownloadError.missingResponse
}
guard response.statusCode == 200 else {
- throw WhiskyWineInstallerError.unexpectedStatus(response.statusCode)
+ throw DownloadError.unexpectedStatus(response.statusCode)
}
let remoteVersion = try PropertyListDecoder().decode(WhiskyWineVersion.self, from: data).version
diff --git a/WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift b/WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift
index 5bace8086..9aa45acb4 100644
--- a/WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift
+++ b/WhiskyKit/Sources/WhiskyKit/Wine/Wine.swift
@@ -22,8 +22,8 @@ import os.log
public class Wine {
/// URL to the installed `DXVK` folder
private static let dxvkFolder: URL = WhiskyWineInstaller.libraryFolder.appending(path: "DXVK")
- /// Path to the `wine64` binary
- public static let wineBinary: URL = WhiskyWineInstaller.binFolder.appending(path: "wine64")
+ /// Path to the `wine` launcher, which runs both 32-bit and 64-bit programs
+ public static let wineBinary: URL = WhiskyWineInstaller.binFolder.appending(path: "wine")
/// Parth to the `wineserver` binary
private static let wineserverBinary: URL = WhiskyWineInstaller.binFolder.appending(path: "wineserver")
@@ -114,40 +114,29 @@ public class Wine {
public static func generateRunCommand(
at url: URL, bottle: Bottle, args: String, environment: [String: String]
) -> String {
- var wineCmd = "\(wineBinary.esc) start /unix \(url.esc) \(args)"
- let env = constructWineEnvironment(for: bottle, environment: environment)
- for environment in env {
- wineCmd = "\(environment.key)=\"\(environment.value)\" " + wineCmd
- }
-
- return wineCmd
+ let command = "\(wineBinary.path(percentEncoded: false).shellQuoted) start /unix "
+ + "\(url.path(percentEncoded: false).shellQuoted) \(args)"
+ let variables = constructWineEnvironment(for: bottle, environment: environment)
+ .sorted { $0.key < $1.key }
+ .map { "\($0.key)=\($0.value.shellQuoted)" }
+ return (variables + [command]).joined(separator: " ")
}
public static func generateTerminalEnvironmentCommand(bottle: Bottle) -> String {
var cmd = """
- export PATH=\"\(WhiskyWineInstaller.binFolder.path):$PATH\"
- export WINE=\"wine64\"
- alias wine=\"wine64\"
- alias winecfg=\"wine64 winecfg\"
- alias msiexec=\"wine64 msiexec\"
- alias regedit=\"wine64 regedit\"
- alias regsvr32=\"wine64 regsvr32\"
- alias wineboot=\"wine64 wineboot\"
- alias wineconsole=\"wine64 wineconsole\"
- alias winedbg=\"wine64 winedbg\"
- alias winefile=\"wine64 winefile\"
- alias winepath=\"wine64 winepath\"
+ export PATH=\(WhiskyWineInstaller.binFolder.path(percentEncoded: false).shellQuoted):"$PATH"
+ export WINE=wine
"""
- let env = constructWineEnvironment(for: bottle, environment: constructWineEnvironment(for: bottle))
- for environment in env {
- cmd += "\nexport \(environment.key)=\"\(environment.value)\""
+ for (key, value) in constructWineEnvironment(for: bottle).sorted(by: { $0.key < $1.key }) {
+ cmd += "\nexport \(key)=\(value.shellQuoted)"
}
return cmd
}
/// Run a `wineserver` command with the given arguments and return the output result
+ @discardableResult
private static func runWineserver(_ args: [String], bottle: Bottle) async throws -> String {
var result: [ProcessOutput] = []
@@ -208,12 +197,32 @@ public class Wine {
return try await runWine(["cmd", "/c", url.path(percentEncoded: false)], bottle: bottle)
}
- public static func killBottle(bottle: Bottle) throws {
- Task.detached(priority: .userInitiated) {
- try await runWineserver(["-k"], bottle: bottle)
+ /// Runs a program, installer or batch file the person picked
+ public static func runFile(at url: URL, bottle: Bottle) async throws {
+ if url.pathExtension.lowercased() == "bat" {
+ try await runBatchFile(url: url, bottle: bottle)
+ } else {
+ try await runProgram(at: url, bottle: bottle)
}
}
+ /// Stops every Windows program in the bottle and waits for its wineserver to exit
+ public static func stopAllProcesses(in bottle: Bottle) async throws {
+ try await runWineserver(["-k"], bottle: bottle)
+ try await runWineserver(["-w"], bottle: bottle)
+ }
+
+ /// Stops every Windows program in the bottle before returning, for when the app is quitting
+ /// and asynchronous work would never run
+ public static func stopAllProcessesBeforeExit(in bottle: Bottle) throws {
+ let process = Process()
+ process.executableURL = wineserverBinary
+ process.arguments = ["-k"]
+ process.environment = constructWineServerEnvironment(for: bottle)
+ try process.run()
+ process.waitUntilExit()
+ }
+
public static func enableDXVK(bottle: Bottle) throws {
try FileManager.default.replaceDLLs(
in: bottle.url.appending(path: "drive_c").appending(path: "windows").appending(path: "system32"),
diff --git a/WhiskyKit/Sources/WhiskyKit/Wine/WineProcesses.swift b/WhiskyKit/Sources/WhiskyKit/Wine/WineProcesses.swift
new file mode 100644
index 000000000..292da856c
--- /dev/null
+++ b/WhiskyKit/Sources/WhiskyKit/Wine/WineProcesses.swift
@@ -0,0 +1,104 @@
+//
+// WineProcesses.swift
+// WhiskyKit
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import Darwin
+import Foundation
+
+/// The Windows programs Wine is running, found from the arguments and environment macOS keeps for each process
+public enum WineProcesses {
+ struct ProcessArguments: Equatable {
+ let executablePath: String
+ let arguments: [String]
+ let environment: [String]
+ }
+
+ /// Processes running in `bottle`, optionally only those running the Windows program `executableName`
+ public static func processIDs(in bottle: Bottle, named executableName: String? = nil) -> [pid_t] {
+ let prefixVariable = "WINEPREFIX=\(bottle.url.path)"
+ return allProcessIDs().filter { processID in
+ guard let process = arguments(of: processID),
+ process.environment.contains(prefixVariable) else { return false }
+ guard let executableName else { return true }
+ return process.arguments.first.map { windowsFileName(of: $0) == executableName.lowercased() } ?? false
+ }
+ }
+
+ /// The lowercased file name of a Windows or Unix path, e.g. `steam.exe` for `C:\Program Files\Steam\Steam.exe`
+ static func windowsFileName(of path: String) -> String {
+ let name = path.split(whereSeparator: { $0 == "\\" || $0 == "/" }).last.map(String.init) ?? path
+ return name.lowercased()
+ }
+
+ private static func allProcessIDs() -> [pid_t] {
+ let capacity = proc_listallpids(nil, 0)
+ guard capacity > 0 else { return [] }
+ // Processes can start between the two calls, so leave room for more
+ var processIDs = [pid_t](repeating: 0, count: Int(capacity) * 2)
+ let count = processIDs.withUnsafeMutableBytes { buffer in
+ proc_listallpids(buffer.baseAddress, Int32(buffer.count))
+ }
+ return Array(processIDs.prefix(Int(max(count, 0))))
+ }
+
+ private static func arguments(of processID: pid_t) -> ProcessArguments? {
+ var request: [Int32] = [CTL_KERN, KERN_PROCARGS2, processID]
+ var size = 0
+ guard sysctl(&request, 3, nil, &size, nil, 0) == 0, size > 0 else { return nil }
+ var bytes = [UInt8](repeating: 0, count: size)
+ guard sysctl(&request, 3, &bytes, &size, nil, 0) == 0 else { return nil }
+ return parse(Array(bytes.prefix(size)))
+ }
+
+ /// Parses `KERN_PROCARGS2`: argc, the executable path, padding, argc arguments, then the environment.
+ /// Wine rewrites a process's arguments to its Windows command line and fills what is left with NULs,
+ /// so empty strings can sit between the arguments and the environment.
+ static func parse(_ bytes: [UInt8]) -> ProcessArguments? {
+ let countSize = MemoryLayout.size
+ guard bytes.count > countSize else { return nil }
+ let argumentCount = bytes.withUnsafeBytes { Int($0.loadUnaligned(as: Int32.self)) }
+ var index = countSize
+
+ func nextString() -> String {
+ let start = index
+ while index < bytes.count && bytes[index] != 0 {
+ index += 1
+ }
+ let string = String(bytes: bytes[start.. HTTPURLResponse {
+ try #require(HTTPURLResponse(url: source, statusCode: status, httpVersion: nil, headerFields: nil))
+ }
+
+ @Test func rejectsANonSuccessResponse() throws {
+ let folder = try TemporaryFolder()
+ let location = folder.url.appending(path: "download")
+ try Data("Not Found".utf8).write(to: location)
+
+ #expect {
+ try Download.accept(location, from: source, response: response(status: 404))
+ } throws: { error in
+ guard case DownloadError.unexpectedStatus(404) = error else { return false }
+ return true
+ }
+ }
+
+ @Test func rejectsAMissingResponse() throws {
+ let folder = try TemporaryFolder()
+ let location = folder.url.appending(path: "download")
+ try Data().write(to: location)
+
+ #expect {
+ try Download.accept(location, from: source, response: nil)
+ } throws: { error in
+ guard case DownloadError.missingResponse = error else { return false }
+ return true
+ }
+ }
+
+ @Test func movesTheFileOutUnderItsOriginalName() throws {
+ let folder = try TemporaryFolder()
+ let location = folder.url.appending(path: "download")
+ try Data("installer".utf8).write(to: location)
+
+ let accepted = try Download.accept(location, from: source, response: response(status: 200))
+ defer {
+ do {
+ try FileManager.default.removeItem(at: accepted.deletingLastPathComponent())
+ } catch {
+ Issue.record(error)
+ }
+ }
+
+ #expect(accepted.lastPathComponent == "SteamSetup.exe")
+ #expect(!FileManager.default.fileExists(atPath: location.path(percentEncoded: false)))
+ #expect(try String(contentsOf: accepted, encoding: .utf8) == "installer")
+ }
+
+ @Test func reportsAFractionOnlyWhenTheSizeIsKnown() {
+ #expect(DownloadProgress(receivedBytes: 50, expectedBytes: 200).fraction == 0.25)
+ #expect(DownloadProgress(receivedBytes: 50, expectedBytes: nil).fraction == nil)
+ }
+}
diff --git a/WhiskyKit/Tests/WhiskyKitTests/ProcessStreamTests.swift b/WhiskyKit/Tests/WhiskyKitTests/ProcessStreamTests.swift
index 840bf3986..c0d671757 100644
--- a/WhiskyKit/Tests/WhiskyKitTests/ProcessStreamTests.swift
+++ b/WhiskyKit/Tests/WhiskyKitTests/ProcessStreamTests.swift
@@ -61,22 +61,18 @@ struct ProcessStreamTests {
#expect(logged.contains("out\n") && logged.contains("err\n"))
}
- private func cpuTime() -> TimeInterval {
- var usage = rusage()
- getrusage(RUSAGE_SELF, &usage)
- let user = TimeInterval(usage.ru_utime.tv_sec) + TimeInterval(usage.ru_utime.tv_usec) / 1_000_000
- let system = TimeInterval(usage.ru_stime.tv_sec) + TimeInterval(usage.ru_stime.tv_usec) / 1_000_000
- return user + system
- }
-
- @Test func goesIdleAfterEndOfFile() async throws {
- _ = try await collect("echo done")
+ @Test func stopsReadingAtEndOfFile() async throws {
+ let process = Process()
+ process.executableURL = URL(filePath: "/bin/sh")
+ process.arguments = ["-c", "echo done"]
- let before = cpuTime()
- try await Task.sleep(for: .milliseconds(500))
- let spent = cpuTime() - before
+ for await _ in try process.runStream(name: "sh", logFile: nil) {}
- #expect(spent < 0.1, "Readers kept polling the closed pipes for \(spent)s of CPU")
+ // A handler left on a closed pipe is called again immediately, forever, spinning a core
+ let output = try #require(process.standardOutput as? Pipe)
+ let errors = try #require(process.standardError as? Pipe)
+ #expect(output.fileHandleForReading.readabilityHandler == nil)
+ #expect(errors.fileHandleForReading.readabilityHandler == nil)
}
@Test func waitsForChildrenThatKeepThePipesOpen() async throws {
diff --git a/WhiskyKit/Tests/WhiskyKitTests/ProgramTests.swift b/WhiskyKit/Tests/WhiskyKitTests/ProgramTests.swift
new file mode 100644
index 000000000..091c35e34
--- /dev/null
+++ b/WhiskyKit/Tests/WhiskyKitTests/ProgramTests.swift
@@ -0,0 +1,60 @@
+//
+// ProgramTests.swift
+// WhiskyKitTests
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import Foundation
+import Testing
+@testable import WhiskyKit
+
+struct ProgramTests {
+ private func makeBottle(in folder: TemporaryFolder) throws -> Bottle {
+ let bottleURL = folder.url.appending(path: "Bottle")
+ try FileManager.default.createDirectory(at: bottleURL, withIntermediateDirectories: true)
+ return Bottle(bottleUrl: bottleURL, isAvailable: true)
+ }
+
+ @Test func loadingAProgramWritesNothing() throws {
+ let folder = try TemporaryFolder()
+ let bottle = try makeBottle(in: folder)
+
+ _ = Program(url: bottle.url.appending(path: "drive_c/Game.exe"), bottle: bottle)
+
+ #expect(!FileManager.default.fileExists(atPath: bottle.url.appending(path: "Program Settings").path))
+ }
+
+ @Test func savesSettingsInsideAnExistingBottle() throws {
+ let folder = try TemporaryFolder()
+ let bottle = try makeBottle(in: folder)
+ let program = Program(url: bottle.url.appending(path: "drive_c/Game.exe"), bottle: bottle)
+
+ program.settings.arguments = "-windowed"
+
+ let reloaded = Program(url: program.url, bottle: bottle)
+ #expect(reloaded.settings.arguments == "-windowed")
+ }
+
+ @Test func doesNotRecreateADeletedBottle() throws {
+ let folder = try TemporaryFolder()
+ let bottle = try makeBottle(in: folder)
+ let program = Program(url: bottle.url.appending(path: "drive_c/Game.exe"), bottle: bottle)
+ try FileManager.default.removeItem(at: bottle.url)
+
+ program.settings.arguments = "-windowed"
+
+ #expect(!FileManager.default.fileExists(atPath: bottle.url.path(percentEncoded: false)))
+ }
+}
diff --git a/WhiskyKit/Tests/WhiskyKitTests/QuotingTests.swift b/WhiskyKit/Tests/WhiskyKitTests/QuotingTests.swift
new file mode 100644
index 000000000..345db7727
--- /dev/null
+++ b/WhiskyKit/Tests/WhiskyKitTests/QuotingTests.swift
@@ -0,0 +1,67 @@
+//
+// QuotingTests.swift
+// WhiskyKitTests
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import AppKit
+import Foundation
+import Testing
+@testable import WhiskyKit
+
+/// Paths and names that broke the hand-escaped versions
+private let awkward = [
+ #"/Users/me/Library/Containers/Brandy/Bottles/My "Best" Games"#,
+ #"/Program Files (x86)/It's $HOME `date` \ back\slash"#,
+ "plain",
+ ""
+]
+
+@MainActor
+struct QuotingTests {
+
+ @Test(arguments: awkward)
+ func shellQuotingRoundTrips(_ text: String) throws {
+ let process = Process()
+ let output = Pipe()
+ process.executableURL = URL(filePath: "/bin/sh")
+ process.arguments = ["-c", "printf %s \(text.shellQuoted)"]
+ process.standardOutput = output
+ try process.run()
+ process.waitUntilExit()
+
+ let printed = try #require(try output.fileHandleForReading.readToEnd() ?? Data())
+ #expect(String(bytes: printed, encoding: .utf8) == text)
+ }
+
+ @Test(arguments: awkward)
+ func appleScriptQuotingRoundTrips(_ text: String) throws {
+ let script = try #require(NSAppleScript(source: "return \(text.appleScriptQuoted)"))
+ var error: NSDictionary?
+ let result = script.executeAndReturnError(&error)
+
+ #expect(error == nil)
+ #expect(result.stringValue ?? "" == text)
+ }
+
+ @Test func terminalScriptCompilesForAwkwardCommands() throws {
+ let command = awkward.map(\.shellQuoted).joined(separator: " ")
+ let script = try #require(NSAppleScript(source: Terminal.script(running: command)))
+ var error: NSDictionary?
+
+ #expect(script.compileAndReturnError(&error))
+ #expect(error == nil)
+ }
+}
diff --git a/WhiskyKit/Tests/WhiskyKitTests/SteamInstallerTests.swift b/WhiskyKit/Tests/WhiskyKitTests/SteamInstallerTests.swift
new file mode 100644
index 000000000..658fb92a7
--- /dev/null
+++ b/WhiskyKit/Tests/WhiskyKitTests/SteamInstallerTests.swift
@@ -0,0 +1,133 @@
+//
+// SteamInstallerTests.swift
+// WhiskyKitTests
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import Foundation
+import Testing
+@testable import WhiskyKit
+
+struct SteamInstallerTests {
+ private func makeBottle(_ name: String, in folder: TemporaryFolder, withSteam: Bool) throws -> Bottle {
+ let url = folder.url.appending(path: name)
+ try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
+ let bottle = Bottle(bottleUrl: url, isAvailable: true)
+ if withSteam {
+ let steam = SteamInstaller.steamExecutable(in: bottle)
+ try FileManager.default.createDirectory(at: steam.deletingLastPathComponent(),
+ withIntermediateDirectories: true)
+ try Data().write(to: steam)
+ }
+ return bottle
+ }
+
+ @Test func findsTheBottleSteamIsInstalledIn() throws {
+ let folder = try TemporaryFolder()
+ let empty = try makeBottle("Empty", in: folder, withSteam: false)
+ let games = try makeBottle("Games", in: folder, withSteam: true)
+
+ #expect(!SteamInstaller.isInstalled(in: empty))
+ #expect(SteamInstaller.steamBottle(in: [empty, games]) == games)
+ }
+
+ @Test func ignoresBottlesWhoseFilesAreMissing() throws {
+ let folder = try TemporaryFolder()
+ let games = try makeBottle("Games", in: folder, withSteam: true)
+ games.isAvailable = false
+
+ #expect(SteamInstaller.steamBottle(in: [games]) == nil)
+ }
+
+ private func makeUpdatedSteam(in folder: TemporaryFolder) throws -> (bottle: Bottle, helpers: URL) {
+ let bottle = try makeBottle("Games", in: folder, withSteam: true)
+ let steam = SteamInstaller.steamExecutable(in: bottle).deletingLastPathComponent()
+ let helpers = steam.appending(path: "bin/cef/cef.win64")
+ try FileManager.default.createDirectory(at: helpers, withIntermediateDirectories: true)
+ try FileManager.default.createDirectory(at: steam.appending(path: "package"), withIntermediateDirectories: true)
+ try Data().write(to: steam.appending(path: "package/steam_client_win64.installed"))
+ try Data("Valve helper v1".utf8).write(to: helpers.appending(path: "steamwebhelper.exe"))
+ return (bottle, helpers)
+ }
+
+ private func makeShim(in folder: TemporaryFolder) throws -> URL {
+ let shim = folder.url.appending(path: "steamwebhelper-shim.exe")
+ try Data("MZ shim BRANDY-SHIM-1".utf8).write(to: shim)
+ return shim
+ }
+
+ private func contents(_ file: URL) throws -> String {
+ try String(contentsOf: file, encoding: .utf8)
+ }
+
+ @Test func detectsWhenTheFirstUpdateHasFinished() throws {
+ let folder = try TemporaryFolder()
+ let installedOnly = try makeBottle("Installed", in: folder, withSteam: true)
+ let (updated, _) = try makeUpdatedSteam(in: folder)
+
+ #expect(!SteamInstaller.isFirstUpdateComplete(in: installedOnly))
+ #expect(SteamInstaller.isFirstUpdateComplete(in: updated))
+ }
+
+ @Test func putsTheShimInFrontOfSteamsHelper() throws {
+ let folder = try TemporaryFolder()
+ let (bottle, helpers) = try makeUpdatedSteam(in: folder)
+ let shim = try makeShim(in: folder)
+
+ try SteamInstaller.installWebHelperShim(in: bottle, shim: shim)
+ try SteamInstaller.installWebHelperShim(in: bottle, shim: shim)
+
+ #expect(try contents(helpers.appending(path: "steamwebhelper.exe")) == "MZ shim BRANDY-SHIM-1")
+ #expect(try contents(helpers.appending(path: "steamwebhelper_real.exe")) == "Valve helper v1")
+ }
+
+ @Test func reinstallsTheShimAfterSteamReplacesIt() throws {
+ let folder = try TemporaryFolder()
+ let (bottle, helpers) = try makeUpdatedSteam(in: folder)
+ let shim = try makeShim(in: folder)
+ try SteamInstaller.installWebHelperShim(in: bottle, shim: shim)
+
+ try Data("Valve helper v2".utf8).write(to: helpers.appending(path: "steamwebhelper.exe"))
+ try SteamInstaller.installWebHelperShim(in: bottle, shim: shim)
+
+ #expect(try contents(helpers.appending(path: "steamwebhelper.exe")) == "MZ shim BRANDY-SHIM-1")
+ #expect(try contents(helpers.appending(path: "steamwebhelper_real.exe")) == "Valve helper v2")
+ }
+
+ @Test func failsClearlyWithoutTheShim() throws {
+ let folder = try TemporaryFolder()
+ let (bottle, _) = try makeUpdatedSteam(in: folder)
+
+ #expect {
+ try SteamInstaller.installWebHelperShim(in: bottle, shim: folder.url.appending(path: "missing.exe"))
+ } throws: { error in
+ guard case SteamInstallerError.webHelperShimMissing = error else { return false }
+ return true
+ }
+ }
+
+ @Test func addsLaunchArgumentsOnlyForSteam() throws {
+ let folder = try TemporaryFolder()
+ let bottle = try makeBottle("Installed", in: folder, withSteam: true)
+ let steam = SteamInstaller.steamExecutable(in: bottle)
+ let game = bottle.url.appending(path: "drive_c/Games/game.exe")
+ let differentCase = URL(filePath: steam.path(percentEncoded: false).replacingOccurrences(of: "steam.exe",
+ with: "Steam.exe"))
+
+ #expect(try SteamInstaller.prepareLaunch(of: game, in: bottle).isEmpty)
+ #expect(try SteamInstaller.prepareLaunch(of: steam, in: bottle) == ["-noverifyfiles"])
+ #expect(SteamInstaller.isSteam(differentCase, in: bottle))
+ }
+}
diff --git a/WhiskyKit/Tests/WhiskyKitTests/WhiskyWineInstallerTests.swift b/WhiskyKit/Tests/WhiskyKitTests/WhiskyWineInstallerTests.swift
index 4fb55f417..77a228d42 100644
--- a/WhiskyKit/Tests/WhiskyKitTests/WhiskyWineInstallerTests.swift
+++ b/WhiskyKit/Tests/WhiskyKitTests/WhiskyWineInstallerTests.swift
@@ -22,12 +22,6 @@ import Testing
@testable import WhiskyKit
struct WhiskyWineInstallerTests {
- private let archiveURL = URL(filePath: "/Libraries.tar.gz")
-
- private func response(status: Int) throws -> HTTPURLResponse {
- try #require(HTTPURLResponse(url: archiveURL, statusCode: status, httpVersion: nil, headerFields: nil))
- }
-
/// Builds an archive laid out like the published one: a top-level `Libraries` folder
private func makeArchive(in folder: URL, version: SemanticVersion?, marker: String) throws -> URL {
let source = folder.appending(path: "source-\(marker)")
@@ -54,43 +48,6 @@ struct WhiskyWineInstallerTests {
return try String(contentsOf: marker, encoding: .utf8)
}
- @Test func rejectsANonSuccessResponse() throws {
- let folder = try TemporaryFolder()
- let download = folder.url.appending(path: "download")
- try Data("Not Found".utf8).write(to: download)
-
- #expect {
- try WhiskyWineInstaller.acceptDownload(at: download, response: response(status: 404), error: nil)
- } throws: { error in
- guard case WhiskyWineInstallerError.unexpectedStatus(404) = error else { return false }
- return true
- }
- }
-
- @Test func passesThroughTheTransportError() throws {
- #expect(throws: URLError(.timedOut)) {
- try WhiskyWineInstaller.acceptDownload(at: nil, response: nil, error: URLError(.timedOut))
- }
- }
-
- @Test func movesASuccessfulDownloadOutOfTheSessionLocation() throws {
- let folder = try TemporaryFolder()
- let download = folder.url.appending(path: "download")
- try Data("archive".utf8).write(to: download)
-
- let accepted = try WhiskyWineInstaller.acceptDownload(at: download, response: response(status: 200), error: nil)
- defer {
- do {
- try FileManager.default.removeItem(at: accepted)
- } catch {
- Issue.record(error)
- }
- }
-
- #expect(!FileManager.default.fileExists(atPath: download.path(percentEncoded: false)))
- #expect(try String(contentsOf: accepted, encoding: .utf8) == "archive")
- }
-
@Test func installsAndThenReplacesTheLibraries() throws {
let folder = try TemporaryFolder()
let applicationFolder = folder.url.appending(path: "Application")
diff --git a/WhiskyKit/Tests/WhiskyKitTests/WineProcessesTests.swift b/WhiskyKit/Tests/WhiskyKitTests/WineProcessesTests.swift
new file mode 100644
index 000000000..1e46a9178
--- /dev/null
+++ b/WhiskyKit/Tests/WhiskyKitTests/WineProcessesTests.swift
@@ -0,0 +1,68 @@
+//
+// WineProcessesTests.swift
+// WhiskyKitTests
+//
+// This file is part of Whisky.
+//
+// Whisky is free software: you can redistribute it and/or modify it under the terms
+// of the GNU General Public License as published by the Free Software Foundation,
+// either version 3 of the License, or (at your option) any later version.
+//
+// Whisky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with Whisky.
+// If not, see https://www.gnu.org/licenses/.
+//
+
+import Foundation
+import Testing
+@testable import WhiskyKit
+
+struct WineProcessesTests {
+ /// Builds a `KERN_PROCARGS2` buffer the way the kernel lays it out
+ private func procArgs(executable: String, arguments: [String], environment: [String]) -> [UInt8] {
+ var bytes = withUnsafeBytes(of: Int32(arguments.count)) { Array($0) }
+ bytes += Array(executable.utf8) + [0, 0, 0, 0]
+ for string in arguments + environment {
+ bytes += Array(string.utf8) + [0]
+ }
+ return bytes + [0]
+ }
+
+ @Test func parsesArgumentsAndEnvironmentSeparately() throws {
+ let bytes = procArgs(executable: "/Libraries/Wine/lib/wine/x86_64-unix/wine",
+ arguments: [#"C:\Program Files (x86)\Steam\steam.exe"#, "-noverifyfiles"],
+ environment: ["WINEPREFIX=/Bottles/Steam", "HOME=/Users/me"])
+
+ let process = try #require(WineProcesses.parse(bytes))
+
+ #expect(process.executablePath == "/Libraries/Wine/lib/wine/x86_64-unix/wine")
+ #expect(process.arguments == [#"C:\Program Files (x86)\Steam\steam.exe"#, "-noverifyfiles"])
+ #expect(process.environment == ["WINEPREFIX=/Bottles/Steam", "HOME=/Users/me"])
+ }
+
+ @Test func findsTheEnvironmentAfterWinesRewrittenArguments() throws {
+ // Wine replaced "wine steam.exe -silent" with the Windows command line and padded the rest
+ var bytes = withUnsafeBytes(of: Int32(3)) { Array($0) }
+ bytes += Array("/Libraries/Wine/lib/wine/x86_64-unix/wine".utf8) + [0, 0]
+ bytes += Array(#"C:\Program Files (x86)\Steam\steam.exe"#.utf8) + [0, 0, 0, 0, 0, 0]
+ bytes += Array("WINEPREFIX=/Bottles/Steam".utf8) + [0] + Array("HOME=/Users/me".utf8) + [0, 0]
+
+ let process = try #require(WineProcesses.parse(bytes))
+
+ #expect(process.arguments.first == #"C:\Program Files (x86)\Steam\steam.exe"#)
+ #expect(process.environment == ["WINEPREFIX=/Bottles/Steam", "HOME=/Users/me"])
+ }
+
+ @Test func rejectsATruncatedBuffer() {
+ #expect(WineProcesses.parse([1, 0]) == nil)
+ }
+
+ @Test func readsTheFileNameOfWindowsAndUnixPaths() {
+ #expect(WineProcesses.windowsFileName(of: #"C:\Program Files (x86)\Steam\Steam.exe"#) == "steam.exe")
+ #expect(WineProcesses.windowsFileName(of: "/Bottles/Steam/drive_c/steam.exe") == "steam.exe")
+ #expect(WineProcesses.windowsFileName(of: "steam.exe") == "steam.exe")
+ }
+}
diff --git a/images/brandy-icon.png b/images/brandy-icon.png
new file mode 100644
index 000000000..f4f67562d
Binary files /dev/null and b/images/brandy-icon.png differ
diff --git a/images/cw-dark.png b/images/cw-dark.png
deleted file mode 100644
index 18cc0dcb2..000000000
Binary files a/images/cw-dark.png and /dev/null differ
diff --git a/images/cw-light.png b/images/cw-light.png
deleted file mode 100644
index 60a9334c3..000000000
Binary files a/images/cw-light.png and /dev/null differ
diff --git a/scripts/build-whiskywine.sh b/scripts/build-whiskywine.sh
new file mode 100755
index 000000000..762677c73
--- /dev/null
+++ b/scripts/build-whiskywine.sh
@@ -0,0 +1,102 @@
+#!/bin/bash
+#
+# build-whiskywine.sh
+# Whisky
+#
+# Assembles the WhiskyWine runtime (a `Libraries` folder) from pinned upstream releases.
+# Every download is checked against a pinned SHA-256 or fetched at a pinned commit.
+# Building the Steam web helper shim needs mingw-w64 (`brew install mingw-w64`).
+# Publish the result with scripts/publish-whiskywine.sh.
+#
+# Usage: scripts/build-whiskywine.sh
+#
+
+set -euo pipefail
+
+readonly RUNTIME_MAJOR=3
+readonly RUNTIME_MINOR=1
+readonly RUNTIME_PATCH=0
+
+# Official WineHQ macOS package (Wine 11.0 stable), built by Gcenx/macOS_Wine_builds
+readonly WINE_URL="https://github.com/Gcenx/macOS_Wine_builds/releases/download/11.0_1/wine-stable-11.0_1-osx64.tar.xz"
+readonly WINE_SHA256="b50dc50ec7f41d58b115a6b685d4d1315ba3c797bd3aa0f49213f2703cb82388"
+readonly WINE_FOLDER="Wine Stable.app/Contents/Resources/wine"
+
+# The non-"builtin" DXVK variant: Wine ignores DLLs marked as builtin when they are copied into system32
+readonly DXVK_URL="https://github.com/Gcenx/DXVK-macOS/releases/download/v1.10.3-20230507-repack/dxvk-macOS-async-v1.10.3-20230507-repack.tar.gz"
+readonly DXVK_SHA256="acd1520ad105d8ef124a09c8e11a259a5dc8bdc565ad18e0e52693f9807b2477"
+readonly DXVK_FOLDER="dxvk-macOS-async-v1.10.3-20230507-repack"
+
+readonly WINETRICKS_COMMIT="f3890f670867b5ffbc3938726db45c0f7d16c8ba"
+readonly WINETRICKS_RAW="https://raw.githubusercontent.com/Winetricks/winetricks/$WINETRICKS_COMMIT"
+
+scripts_folder="$(cd "$(dirname "$0")" && pwd)"
+readonly SHIM_SOURCE="$scripts_folder/steamwebhelper-shim.c"
+readonly SHIM_COMPILER="x86_64-w64-mingw32-gcc"
+
+if ! command -v "$SHIM_COMPILER" > /dev/null; then
+ echo "$SHIM_COMPILER not found; install it with: brew install mingw-w64" >&2
+ exit 69
+fi
+
+if [[ $# -ne 1 ]]; then
+ echo "Usage: $0 " >&2
+ exit 64
+fi
+
+mkdir -p "$1"
+libraries="$(cd "$1" && pwd)/Libraries"
+if [[ -e "$libraries" ]]; then
+ echo "$libraries already exists" >&2
+ exit 73
+fi
+
+work="$(mktemp -d)"
+trap 'rm -rf "$work"' EXIT
+
+fetch() {
+ local url="$1" sha256="$2" destination="$3"
+ curl -fL --retry 3 --silent --show-error -o "$destination" "$url"
+ echo "$sha256 $destination" | shasum -a 256 -c - > /dev/null
+}
+
+echo "Downloading Wine"
+fetch "$WINE_URL" "$WINE_SHA256" "$work/wine.tar.xz"
+tar -xJf "$work/wine.tar.xz" -C "$work"
+
+echo "Downloading DXVK"
+fetch "$DXVK_URL" "$DXVK_SHA256" "$work/dxvk.tar.gz"
+tar -xzf "$work/dxvk.tar.gz" -C "$work"
+
+echo "Assembling $libraries"
+mkdir -p "$libraries/DXVK"
+ditto "$work/$WINE_FOLDER" "$libraries/Wine"
+ditto "$work/$DXVK_FOLDER/x64" "$libraries/DXVK/x64"
+ditto "$work/$DXVK_FOLDER/x32" "$libraries/DXVK/x32"
+curl -fL --retry 3 --silent --show-error -o "$libraries/winetricks" "$WINETRICKS_RAW/src/winetricks"
+curl -fL --retry 3 --silent --show-error -o "$libraries/verbs.txt" "$WINETRICKS_RAW/files/verbs/all.txt"
+chmod +x "$libraries/winetricks"
+
+echo "Building the Steam web helper shim"
+mkdir -p "$libraries/Steam"
+"$SHIM_COMPILER" -municode -mwindows -O2 -Wall -Wextra -Werror -static \
+ -o "$libraries/Steam/steamwebhelper-shim.exe" "$SHIM_SOURCE"
+
+version_file="$libraries/WhiskyWineVersion.plist"
+plutil -create xml1 "$version_file"
+plutil -insert version -dictionary "$version_file"
+plutil -insert version.major -integer "$RUNTIME_MAJOR" "$version_file"
+plutil -insert version.minor -integer "$RUNTIME_MINOR" "$version_file"
+plutil -insert version.patch -integer "$RUNTIME_PATCH" "$version_file"
+plutil -insert version.preRelease -string "" "$version_file"
+plutil -insert version.build -string "" "$version_file"
+
+for required in Wine/bin/wine Wine/bin/wineserver DXVK/x64/d3d11.dll DXVK/x32/d3d11.dll winetricks verbs.txt \
+ Steam/steamwebhelper-shim.exe; do
+ if [[ ! -e "$libraries/$required" ]]; then
+ echo "Missing $required" >&2
+ exit 70
+ fi
+done
+
+echo "Built WhiskyWine $RUNTIME_MAJOR.$RUNTIME_MINOR.$RUNTIME_PATCH at $libraries"
diff --git a/scripts/publish-whiskywine.sh b/scripts/publish-whiskywine.sh
index 80b40a40f..7d48ad7aa 100755
--- a/scripts/publish-whiskywine.sh
+++ b/scripts/publish-whiskywine.sh
@@ -4,7 +4,8 @@
# Whisky
#
# Publishes a built WhiskyWine `Libraries` folder as the assets Whisky downloads:
-# Libraries.tar.gz and WhiskyWineVersion.plist on the `whiskywine` release.
+# Libraries.tar.gz and WhiskyWineVersion.plist on the `whiskywine-` release.
+# Each major version gets its own release so an app only downloads a runtime it can run.
#
# Usage: scripts/publish-whiskywine.sh
#
@@ -13,7 +14,6 @@ set -euo pipefail
# Must match WhiskyWineInstaller.distributionURL; this checkout also has the upstream remote
readonly REPOSITORY="iStorry/Whisky"
-readonly RELEASE_TAG="whiskywine"
readonly ARCHIVE_NAME="Libraries.tar.gz"
readonly VERSION_FILE="WhiskyWineVersion.plist"
@@ -27,7 +27,7 @@ if [[ "$(basename "$libraries")" != "Libraries" ]]; then
echo "The folder must be named Libraries; the app extracts the archive expecting that top-level folder." >&2
exit 65
fi
-for required in "$VERSION_FILE" "Wine/bin/wine64" "Wine/bin/wineserver"; do
+for required in "$VERSION_FILE" "Wine/bin/wine" "Wine/bin/wineserver"; do
if [[ ! -e "$libraries/$required" ]]; then
echo "Missing $required in $libraries" >&2
exit 65
@@ -38,6 +38,7 @@ version_component() {
plutil -extract "version.$1" raw -o - "$libraries/$VERSION_FILE"
}
version="$(version_component major).$(version_component minor).$(version_component patch)"
+release_tag="whiskywine-$(version_component major)"
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
@@ -45,13 +46,13 @@ echo "Packaging WhiskyWine $version"
COPYFILE_DISABLE=1 tar -zcf "$work/$ARCHIVE_NAME" -C "$(dirname "$libraries")" Libraries
cp "$libraries/$VERSION_FILE" "$work/$VERSION_FILE"
-if ! gh release view "$RELEASE_TAG" --repo "$REPOSITORY" > /dev/null 2>&1; then
- gh release create "$RELEASE_TAG" --repo "$REPOSITORY" --title "WhiskyWine runtime" --latest=false \
+if ! gh release view "$release_tag" --repo "$REPOSITORY" > /dev/null 2>&1; then
+ gh release create "$release_tag" --repo "$REPOSITORY" --title "WhiskyWine $(version_component major) runtime" --latest=false \
--notes "Wine runtime downloaded by Whisky during setup. Not an app release."
fi
-gh release upload "$RELEASE_TAG" --repo "$REPOSITORY" "$work/$ARCHIVE_NAME" "$work/$VERSION_FILE" --clobber
-gh release edit "$RELEASE_TAG" --repo "$REPOSITORY" --notes "Wine runtime downloaded by Whisky during setup. Not an app release.
+gh release upload "$release_tag" --repo "$REPOSITORY" "$work/$ARCHIVE_NAME" "$work/$VERSION_FILE" --clobber
+gh release edit "$release_tag" --repo "$REPOSITORY" --notes "Wine runtime downloaded by Whisky during setup. Not an app release.
Current version: $version"
-echo "Published WhiskyWine $version to the $RELEASE_TAG release of $REPOSITORY"
+echo "Published WhiskyWine $version to the $release_tag release of $REPOSITORY"
diff --git a/scripts/steamwebhelper-shim.c b/scripts/steamwebhelper-shim.c
new file mode 100644
index 000000000..c29474ac5
--- /dev/null
+++ b/scripts/steamwebhelper-shim.c
@@ -0,0 +1,72 @@
+//
+// steamwebhelper-shim.c
+// Brandy
+//
+// Steam draws its interface with steamwebhelper.exe, a Chromium build. Under Wine on macOS that
+// window stays black: Chromium composites in a separate GPU process and presents through
+// DirectComposition, neither of which Wine's Mac driver supports. Steam passes no way to change
+// Chromium's switches, so Brandy puts this program in place of steamwebhelper.exe, keeps Steam's
+// original beside it as steamwebhelper_real.exe, and adds the switches for the browser process
+// (the one started without --type=). Child processes are passed through unchanged.
+//
+// The approach and switches follow Carafe's shim (https://github.com/fwhuy/carafe, MIT).
+//
+// Built by scripts/build-whiskywine.sh with mingw-w64.
+//
+
+#include
+#include
+#include
+
+// Lets Brandy tell this shim apart from Steam's helper, which Steam restores when it updates
+__attribute__((used)) static const char marker[] = "BRANDY-SHIM-1";
+
+static const wchar_t browser_switches[] =
+ L"--no-sandbox --disable-gpu --disable-gpu-compositing --in-process-gpu --disable-direct-composition";
+
+// Skips argv[0] of a Windows command line, following CommandLineToArgvW's rules for it
+static const wchar_t *skip_program_name(const wchar_t *command) {
+ if (*command == L'"') {
+ command++;
+ while (*command && *command != L'"') command++;
+ if (*command == L'"') command++;
+ } else {
+ while (*command && *command != L' ' && *command != L'\t') command++;
+ }
+ while (*command == L' ' || *command == L'\t') command++;
+ return command;
+}
+
+int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, LPWSTR command_line, int show) {
+ (void)instance; (void)previous; (void)command_line; (void)show; (void)marker;
+
+ wchar_t real[MAX_PATH * 2];
+ DWORD length = GetModuleFileNameW(NULL, real, MAX_PATH * 2);
+ wchar_t *separator = wcsrchr(real, L'\\');
+ if (length == 0 || length + 32 >= MAX_PATH * 2 || separator == NULL) return ERROR_BAD_PATHNAME;
+ wcscpy(separator + 1, L"steamwebhelper_real.exe");
+
+ const wchar_t *arguments = skip_program_name(GetCommandLineW());
+ const wchar_t *switches = wcsstr(arguments, L"--type=") == NULL ? browser_switches : L"";
+
+ size_t size = wcslen(real) + wcslen(switches) + wcslen(arguments) + 8;
+ wchar_t *command = malloc(size * sizeof(wchar_t));
+ if (command == NULL) return ERROR_NOT_ENOUGH_MEMORY;
+ swprintf(command, size, L"\"%ls\" %ls %ls", real, switches, arguments);
+
+ STARTUPINFOW startup;
+ PROCESS_INFORMATION process;
+ ZeroMemory(&startup, sizeof(startup));
+ startup.cb = sizeof(startup);
+ GetStartupInfoW(&startup);
+ BOOL started = CreateProcessW(real, command, NULL, NULL, TRUE, 0, NULL, NULL, &startup, &process);
+ free(command);
+ if (!started) return (int)GetLastError();
+
+ WaitForSingleObject(process.hProcess, INFINITE);
+ DWORD exit_code = 0;
+ GetExitCodeProcess(process.hProcess, &exit_code);
+ CloseHandle(process.hThread);
+ CloseHandle(process.hProcess);
+ return (int)exit_code;
+}