diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
index 1e84109..f195d3e 100644
--- a/.github/release-drafter.yml
+++ b/.github/release-drafter.yml
@@ -5,6 +5,9 @@ categories:
- title: '🐛 Bug Fixes'
labels:
- 'bugfix'
+ - title: '🌎 Localization'
+ labels:
+ - 'localization'
- title: '🧰 Maintenance'
label:
- 'chore'
diff --git a/.github/workflows/appcast.yml b/.github/workflows/appcast.yml
index e91415e..ce61d3f 100644
--- a/.github/workflows/appcast.yml
+++ b/.github/workflows/appcast.yml
@@ -9,21 +9,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎
- uses: actions/checkout@v2.3.4
+ uses: actions/checkout@v3
with:
- # If you're using actions/checkout@v2.3.4 you must set persist-credentials to false in most cases for the deployment to work correctly.
+ # If you're using actions/checkout@v3 you must set persist-credentials to false in most cases for the deployment to work correctly.
persist-credentials: false
- name: Cache 📦
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v3.3.1
with:
path: AppCast/vendor/bundle
key: ${{ runner.os }}-gems-v1.0-${{ hashFiles('AppCast/Gemfile') }}
restore-keys: |
${{ runner.os }}-gems-
- - name: Ruby ♦️
- uses: actions/setup-ruby@v1.1.3
+ - name: Setup Ruby, JRuby and TruffleRuby
+ uses: ruby/setup-ruby@v1.149.0
with:
ruby-version: '2.7'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ea243bf..3a4a580 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,16 +2,16 @@ name: CI
on:
push:
- branches:
+ branches:
- main
pull_request:
jobs:
test:
- runs-on: macOS-11.0
+ runs-on: macos-12
steps:
- - uses: actions/checkout@v2.3.4
+ - uses: actions/checkout@v3
- name: Run tests
- env:
- DEVELOPER_DIR: /Applications/Xcode_12.5.app
+ env:
+ DEVELOPER_DIR: /Applications/Xcode_13.2.1.app
run: xcodebuild test -scheme Xcodes
diff --git a/.gitignore b/.gitignore
index 18d371b..c32cb51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,6 +36,9 @@ Product/*
timeline.xctimeline
playground.xcworkspace
+# Jetbrains IDEA
+.idea
+
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
diff --git a/AppCast/Gemfile.lock b/AppCast/Gemfile.lock
index daa9d19..dafd74c 100644
--- a/AppCast/Gemfile.lock
+++ b/AppCast/Gemfile.lock
@@ -1,7 +1,7 @@
GEM
remote: https://rubygems.org/
specs:
- addressable (2.7.0)
+ addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
colorator (1.1.0)
concurrent-ruby (1.1.7)
@@ -71,7 +71,7 @@ GEM
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
thread_safe (0.3.6)
- tzinfo (1.2.9)
+ tzinfo (1.2.10)
thread_safe (~> 0.1)
tzinfo-data (1.2020.6)
tzinfo (>= 1.0.0)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e2e2249
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,43 @@
+# Contributing to Xcodes
+We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
+
+- Reporting a bug
+- Discussing the current state of the code
+- Submitting a fix
+- Proposing new features
+- Becoming a maintainer
+
+## We Develop with Github
+We use github to host code, to track issues and feature requests, as well as accept pull requests.
+
+## All Code Changes Happen Through Pull Requests
+Pull requests are the best way to propose changes to the codebase We actively welcome your pull requests:
+
+1. Fork the repo and create your branch from `main`.
+2. If you've added code that should be tested, add tests.
+3. If you've added new functionality, add documentation
+4. Ensure the test suite passes.
+5. Make sure your code lints.
+6. Issue that pull request!
+
+## Any contributions you make will be under the MIT Software License
+In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern.
+
+## Report bugs using Github's [issues](https://github.com/robotsandpencils/xcodesapp/issues)
+We use GitHub issues to track public bugs. Report a bug by [opening a new issue](); it's that easy!
+
+## Write bug reports with detail, background, and sample code
+
+**Great Bug Reports** tend to have:
+
+- A quick summary and/or background
+- Steps to reproduce
+ - Be specific!
+- What you expected would happen
+- What actually happens
+- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
+
+People *love* thorough bug reports.
+
+## License
+By contributing, you agree that your contributions will be licensed under its MIT License.
diff --git a/DECISIONS.md b/DECISIONS.md
index 8e85b1c..481a638 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -73,4 +73,4 @@ Based on [this blog post](https://yiqiu.me/2015/11/19/sparkle-update-on-github/)
We're deliberately not capturing system profile data with Sparkle right now, because we don't want it and because it would require additional infrastructure.
-We also considered https://github.com/mxcl/AppUpdater, but decided aganist it because it seemed less battle-tested than Sparkle and currently lacks an open source license.
+We also considered https://github.com/mxcl/AppUpdater, but decided against it because it seemed less battle-tested than Sparkle and currently lacks an open source license.
diff --git a/README.md b/README.md
index bcec0e5..bcdcb4f 100644
--- a/README.md
+++ b/README.md
@@ -2,46 +2,91 @@
The easiest way to install and switch between multiple versions of Xcode.
-_If you're looking for a command-line version of Xcodes.app, try [`xcodes`](https://github.com/RobotsAndPencils/xcodes)._
+_If you're looking for a command-line version of Xcodes.app, try [`xcodes`](https://github.com/XcodesOrg/xcodes)._
-
+
-
+
+
+
+### :tada: Announcment
+
+XcodesApp is now part of the `XcodesOrg` - [read more here](nextstep.md)
## Features
- List all available Xcode versions from [Xcode Releases'](https://xcodereleases.com) data or the Apple Developer website.
-- Install any Xcode version, fully automated from start to finish. Xcodes uses [`aria2`](https://aria2.github.io), which uses up to 16 connections to download 3-5x faster than URLSession.
+- Install any Xcode version, **fully automated** from start to finish. Xcodes uses [`aria2`](https://aria2.github.io), which uses up to 16 connections to download 3-5x faster than URLSession.
+- Automatically resumes installs if network errors.
+- Apple ID required to download Xcode versions.
- Just click a button to make a version active with `xcode-select`.
- View release notes, OS compatibility, included SDKs and compilers from [Xcode Releases](https://xcodereleases.com).
+- Dark/Light Mode supported
+## Experiments
+
+- Thanks to the wonderful work of [https://github.com/saagarjha/unxip](https://github.com/saagarjha/unxip), turn on the experiment to increase your unxipping time by up to 70%! More can be found on his repo, but bugs, high memory may occur if used.
+
+
+
+
+## Localization
+
+Xcodes supports localization in several languages.
+
+The following languages are supported because of the following community users!
+
+|||||
+|-|-|-|-|
+|French 🇫🇷 |[@dompepin](https://github.com/dompepin)|Italian 🇮🇹 |[gualtierofrigerio](https://github.com/gualtierofrigerio)|
+|Spanish 🇪🇸🇲 |[@cesartru88](https://github.com/cesartru88)|Korean 🇰🇷 |[@ryan-son](https://github.com/ryan-son)|
+|Russian 🇷🇺 |[@alexmazlov](https://github.com/alexmazlov)|Turkish 🇹🇷 |[@egesucu](https://github.com/egesucu)|
+|Hindi 🇮🇳 |[@KGurpreet](https://github.com/KGurpreet)|Chinese-Simplified 🇨🇳|[@megabitsenmzq](https://github.com/megabitsenmzq)|
+|Finnish 🇫🇮 |[@marcusziade](https://github.com/marcusziade)|Chinese-Traditional 🇹🇼|[@itszero](https://github.com/itszero)|
+|Ukranian 🇺🇦 |[@gelosi](https://github.com/gelosi)|Japanese 🇯🇵|[@tatsuz0u](https://github.com/tatsuz0u)|
+|German 🇩🇪|[@drct](https://github.com/drct)|Dutch 🇳🇱|[@jfversluis](https://github/com/jfversluis)|
+|Brazilian Portuguese 🇧🇷|[@brunomunizaf](https://github.com/brunomunizaf)|Polish 🇵🇱|[@jakex7](https://github.com/jakex7)|
+
+Want to add more languages? Simply create a PR with the updated strings file.
## Installation
Xcodes.app runs on macOS Big Sur 11.0 or later.
-### Homebrew Cask
+### Install with Homebrew
+
+Developer ID-signed and notarized release builds are available on Homebrew. These don't require Xcode to already be installed in order to use.
```sh
brew install --cask xcodes
-
-# These are Developer ID-signed and notarized release builds and don't require Xcode to already be installed in order to use.
```
-### Download a release
+### Manually install
-1. Download the latest version [here](https://github.com/RobotsAndPencils/XcodesApp/releases/latest) using the **Xcodes.zip** asset. These are Developer ID-signed and notarized release builds and don't require Xcode to already be installed in order to use.
+1. Download the latest version [here](https://github.com/XcodesOrg/XcodesApp/releases/latest) using the **Xcodes.zip** asset. These are Developer ID-signed and notarized release builds and don't require Xcode to already be installed in order to use.
2. Move the unzipped `Xcodes.app` to your `/Applications` directory
## Development
-You'll need macOS 11 Big Sur and Xcode 12 in order to build and run Xcodes.app.
+You'll need macOS 12 Big Sur and Xcode 13 in order to build and run Xcodes.app.
-If you aren't a Robots and Pencils employee you'll need to change the CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT build setting to your Apple Developer team ID in order for code signing validation to succeed between the main app and the privileged helper.
+`Unxip` and `aria2` must be compiled as a universal binary
+```
+# compile for Intel
+ swiftc -parse-as-library -O -target x86_64-apple-macos11 unxip.swift
+# compile for M1
+ swiftc -parse-as-library -O -target arm64-apple-macos11 unxip.swift
+
+# combine for universal binary
+ lipo -create -output unxip unxip_intel unxip_m1
+# check it
+ lipo -archs unxip
+```
Notable design decisions are recorded in [DECISIONS.md](./DECISIONS.md). The Apple authentication flow is described in [Apple.paw](https://github.com/XcodesOrg/xcodes/blob/main/Apple.paw), which will allow you to play with the API endpoints that are involved using the [Paw](https://paw.cloud) app.
[`xcode-install`](https://github.com/xcpretty/xcode-install) and [fastlane/spaceship](https://github.com/fastlane/fastlane/tree/master/spaceship) both deserve credit for figuring out the hard parts of what makes this possible.
+
Releasing a new version
@@ -50,9 +95,9 @@ Follow the steps below to build and release a new version of Xcodes.app. For any
```sh
# Update the version number in Xcode and commit the change, if necessary
-# Question: Did anything in XPCHelper change?
+# Question: Did anything in XPCHelper change?
# - com.robotsandpencils.XcodesApp.Helper folder and HelperXPCShared
-# - if so, bump the version number in com.robotsandpencils.XcodesApp.Helper target.
+# - if so, bump the version number in com.robotsandpencils.XcodesApp.Helper target.
# Note: you do not have to bump the version number if nothing has changed.
# Note2: If you do bump the version, the end user, must re-install the XPCHelper and give permission again.
@@ -77,17 +122,20 @@ scripts/package_release.sh
# Notarize the app
# Do this from the Product directory so the app is zipped without being nested inside Product
# Create a app specific password on appleid.apple.com if you haven't already
-# % xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "" -p
+# xcrun notarytool store-credentials "AC_PASSWORD" \
+# --apple-id "test@example.com" \
+# --team-id "teamid" \
+# --password "app specific password"
pushd Product
-../scripts/notarize.sh "test@example.com" "@keychain:AC_PASSWORD" Xcodes.zip
+../scripts/notarize.sh Xcodes.zip
# Sign the .zip for Sparkle, note the signature in the output for later
# If you're warned about the signing key not being found, see the Xcodes 1Password vault for the key and installation instructions.
../scripts/sign_update Xcodes.zip
popd
-# Go to https://github.com/RobotsAndPencils/XcodesApp/releases
+# Go to https://github.com/XcodesOrg/XcodesApp/releases
# If there are uncategorized PRs, add the appropriate label and run the Release Drafter action manually
# Edit the latest draft release
# Set its tag to the tag you just pushed
@@ -102,10 +150,8 @@ popd
```
-## Contact
+## Maintainers
-
+[Matt Kiazyk](https://github.com/mattkiazyk) - [Twitter](https://www.twitter.com/mattkiazyk)
-Made with ❤️ by [Robots & Pencils](http://www.robotsandpencils.com)
-
-[Twitter](https://twitter.com/robotsNpencils) | [GitHub](https://github.com/robotsandpencils)
+[Twitter](https://twitter.com/xcodesApp) | [GitHub](https://github.com/xcodesOrg)
diff --git a/Scripts/notarize.sh b/Scripts/notarize.sh
index b9dd426..a2ab52f 100755
--- a/Scripts/notarize.sh
+++ b/Scripts/notarize.sh
@@ -17,46 +17,39 @@
# https://developer.apple.com/documentation/xcode/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow
# Adapted from https://github.com/keybase/client/blob/46f5df0aa64ff19198ba7b044bbb7cd907c0be9f/packaging/desktop/package_darwin.sh
-username="$1"
-password="$2"
-asc_provider="$3"
-file="$4"
+file="$1"
+team_id="$2"
echo "Uploading to notarization service"
-uuid=$(xcrun altool \
- --notarize-app \
- --primary-bundle-id "com.robotsandpencils.XcodesApp.zip" \
- --username "$username" \
- --password "$password" \
- --asc-provider "$asc_provider" \
- --file "$file" 2>&1 | \
- grep 'RequestUUID' | \
- awk '{ print $3 }')
+result=$(xcrun notarytool submit "$file" \
+ --keychain-profile "AC_PASSWORD" \
+ --team-id "$team_id" \
+ --wait)
+# echo "done1"
+echo $result
+
+# My grep/awk is bad and I can't figure out how to get the UUID out properly
+# uuid=$("$result" | \
+# grep 'id:' | tail -n1 | \
+# cut -d":" -f2-)
echo "Successfully uploaded to notarization service, polling for result: $uuid"
-sleep 15
- while :
- do
- fullstatus=$(xcrun altool \
- --notarization-info "$uuid" \
- --username "$username" \
- --password "$password" \
- --asc-provider "$asc_provider" 2>&1)
- status=$(echo "$fullstatus" | grep 'Status\:' | awk '{ print $2 }')
- if [ "$status" = "success" ]; then
- echo "Notarization success"
- exit 0
- elif [ "$status" = "in" ]; then
- echo "Notarization still in progress, sleeping for 15 seconds and trying again"
- sleep 15
- else
- echo "Notarization failed, full status below"
- echo "$fullstatus"
- exit 1
- fi
- done
+# we should check here using the info (or notarytool log) to check the results and log
+#
+
+# fullstatus=$(xcrun notarytool info "$uuid" \
+# --keychain-profile "AC_PASSWORD" 2>&1)
+# status=$(echo "$fullstatus" | grep 'status\:' | awk '{ print $2 }')
+# if [ "$status" = "Accepted" ]; then
+# echo "Notarization success"
+# exit 0
+# else
+# echo "Notarization failed, full status below"
+# echo "$fullstatus"
+# exit 1
+# fi
# Remove .zip
rm $file
@@ -66,4 +59,4 @@ app_path="$(basename -s ".zip" "$file").app"
xcrun stapler staple "$app_path"
# Zip the stapled app for distribution
-ditto -c -k --sequesterRsrc --keepParent "$file" "$app_path"
+ditto -c -k --sequesterRsrc --keepParent "$app_path" "$file"
diff --git a/Scripts/package_release.sh b/Scripts/package_release.sh
index 3cbd887..a56e14c 100755
--- a/Scripts/package_release.sh
+++ b/Scripts/package_release.sh
@@ -10,6 +10,9 @@ PROJECT_DIR=$(pwd)/$PROJECT_NAME/Resources
SCRIPTS_DIR=$(pwd)/Scripts
INFOPLIST_FILE="Info.plist"
+# If needed ensure that the unxip binary is signed with a hardened runtime so we can notarize
+# codesign --force --options runtime --sign "Developer ID Application: Robots and Pencils Inc." $PROJECT_DIR/unxip
+
# Ensure a clean build
rm -rf Archive/*
rm -rf Product/*
diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj
index 9664992..0fc83e8 100644
--- a/Xcodes.xcodeproj/project.pbxproj
+++ b/Xcodes.xcodeproj/project.pbxproj
@@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
+ 36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */; };
+ 36741BFF291E50F500A85AAE /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36741BFE291E50F500A85AAE /* FileError.swift */; };
536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; };
536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */; };
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53CBAB2B263DCC9100410495 /* XcodesAlert.swift */; };
@@ -96,17 +98,24 @@
CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC4D2599B33D003DCC5A /* MainToolbar.swift */; };
CAFBDC68259A308B003DCC5A /* InfoPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC67259A308B003DCC5A /* InfoPane.swift */; };
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBDC6B259A3098003DCC5A /* View+Conditional.swift */; };
- CAFE4A9A25B7C7A30064FE51 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = CAFE4A9925B7C7A30064FE51 /* Sparkle */; };
CAFE4AAC25B7D2C70064FE51 /* GeneralPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4AAB25B7D2C70064FE51 /* GeneralPreferencePane.swift */; };
CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4AB325B7D3AF0064FE51 /* AdvancedPreferencePane.swift */; };
CAFE4ABC25B7D54B0064FE51 /* UpdatesPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */; };
CAFFFED8259CDA5000903F81 /* XcodeListViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */; };
+ E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; };
+ E872EE4E2808D4F100D3DD8B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E872EE502808D4F100D3DD8B /* Localizable.strings */; };
+ E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; };
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
+ E8CBDB8927ADE32300B22292 /* unxip in Copy aria2c */ = {isa = PBXBuildFile; fileRef = E8CBDB8627ADD92000B22292 /* unxip */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ E8CBDB8B27AE02FF00B22292 /* ExperiementsPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */; };
+ E8D0296F284B029800647641 /* BottomStatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D0296E284B029800647641 /* BottomStatusBar.swift */; };
+ E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D655BF288DD04700A139C2 /* SelectedActionType.swift */; };
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; };
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; };
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; };
+ E8F81FC4282D8A17006CBD0F /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E8F81FC3282D8A17006CBD0F /* Sparkle */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -153,6 +162,7 @@
dstPath = "";
dstSubfolderSpec = 6;
files = (
+ E8CBDB8927ADE32300B22292 /* unxip in Copy aria2c */,
CAA8589325A2B77E00ACF8C0 /* aria2c in Copy aria2c */,
);
name = "Copy aria2c";
@@ -161,10 +171,27 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 15FAD1652811D15600B63259 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = ""; };
+ 23703D6E29EBF63500DFA346 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; };
+ 25E2FA26284769A00014A318 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; };
+ 327DF109286ABE6B00D694D5 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; };
+ 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPreferencePane.swift; sourceTree = ""; };
+ 36741BFE291E50F500A85AAE /* FileError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = ""; };
+ 4A5AAA1D28118FAD00528958 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; };
536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = ""; };
536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesSheet.swift; sourceTree = ""; };
53CBAB2B263DCC9100410495 /* XcodesAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesAlert.swift; sourceTree = ""; };
+ 58F743C02810A34900EEC0F3 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; };
+ 5AA8A6102877EDAD009ECDB0 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; };
63EAA4EA259944450046AB8F /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = ""; };
+ 6CA3A090282EBE72005A6E35 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; };
+ 7CBF284E28606D2C001AA66B /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; };
+ 9D3C3AF7282EBE3300CB0D37 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; };
+ A0187D39285792D1002F46F9 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; };
+ AAB037D32839BD4700017680 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; };
+ AB4EB0DE28541FA000FF3B1D /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; };
+ B648F22B2810C1130096781B /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; };
+ C0AE7FA4283002DC00DA63D2 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; };
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = ""; };
CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateUpdateTests.swift; sourceTree = ""; };
CA25192925A9644800F08414 /* XcodeInstallState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeInstallState.swift; sourceTree = ""; };
@@ -269,9 +296,17 @@
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatesPreferencePane.swift; sourceTree = ""; };
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeListViewRow.swift; sourceTree = ""; };
CAFFFEEE259CEAC400903F81 /* RingProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingProgressViewStyle.swift; sourceTree = ""; };
+ E2AFDCCA28F024D000864ADD /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; };
+ E81D7E9F2805250100A205FC /* Collection+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+.swift"; sourceTree = ""; };
+ E872EE4F2808D4F100D3DD8B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
+ E87AB3C42939B65E00D72F43 /* Hardware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hardware.swift; sourceTree = ""; };
E87DD6EA25D053FA00D86808 /* Progress+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Progress+.swift"; sourceTree = ""; };
E89342F925EDCC17007CF557 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; };
E8977EA225C11E1500835F80 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; };
+ E8CBDB8627ADD92000B22292 /* unxip */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; path = unxip; sourceTree = ""; };
+ E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperiementsPreferencePane.swift; sourceTree = ""; };
+ E8D0296E284B029800647641 /* BottomStatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomStatusBar.swift; sourceTree = ""; };
+ E8D655BF288DD04700A139C2 /* SelectedActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedActionType.swift; sourceTree = ""; };
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; };
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = ""; };
/* End PBXFileReference section */
@@ -288,7 +323,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
- CAFE4A9A25B7C7A30064FE51 /* Sparkle in Frameworks */,
+ E8F81FC4282D8A17006CBD0F /* Sparkle in Frameworks */,
CABFA9E42592F08E00380FEE /* Version in Frameworks */,
CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */,
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
@@ -409,6 +444,7 @@
CAE42486259A68A300B8B246 /* XcodeListCategory.swift */,
CAD2E7A32449574E00113D76 /* XcodeListView.swift */,
CAFFFED7259CDA5000903F81 /* XcodeListViewRow.swift */,
+ E8D0296E284B029800647641 /* BottomStatusBar.swift */,
);
path = XcodeList;
sourceTree = "";
@@ -429,6 +465,7 @@
CAA858C325A2BE4E00ACF8C0 /* Downloader.swift */,
CABFA9B22592EEEA00380FEE /* Entry+.swift */,
CABFA9A92592EEE900380FEE /* Environment.swift */,
+ 36741BFE291E50F500A85AAE /* FileError.swift */,
CABFA9B82592EEEA00380FEE /* FileManager+.swift */,
CAFBDB942598FE96003DCC5A /* FocusedValues.swift */,
CABFA9AC2592EEE900380FEE /* Foundation.swift */,
@@ -453,6 +490,9 @@
CA25192925A9644800F08414 /* XcodeInstallState.swift */,
CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */,
E87DD6EA25D053FA00D86808 /* Progress+.swift */,
+ E81D7E9F2805250100A205FC /* Collection+.swift */,
+ E8D655BF288DD04700A139C2 /* SelectedActionType.swift */,
+ E87AB3C42939B65E00D72F43 /* Hardware.swift */,
);
path = Backend;
sourceTree = "";
@@ -484,6 +524,8 @@
CA9FF83E2594FBC000E47BAF /* Licenses.rtf */,
CAD2E7AE2449575000113D76 /* Xcodes.entitlements */,
CA8FB64D256E17B100469DA5 /* XcodesTest.entitlements */,
+ E8CBDB8627ADD92000B22292 /* unxip */,
+ E872EE502808D4F100D3DD8B /* Localizable.strings */,
);
path = Resources;
sourceTree = "";
@@ -557,10 +599,12 @@
isa = PBXGroup;
children = (
CAFE4AB325B7D3AF0064FE51 /* AdvancedPreferencePane.swift */,
+ 36741BFC291E4FDB00A85AAE /* DownloadPreferencePane.swift */,
CAFE4AAB25B7D2C70064FE51 /* GeneralPreferencePane.swift */,
CAFE4ABB25B7D54B0064FE51 /* UpdatesPreferencePane.swift */,
E8977EA225C11E1500835F80 /* PreferencesView.swift */,
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */,
+ E8CBDB8A27AE02FF00B22292 /* ExperiementsPreferencePane.swift */,
);
path = Preferences;
sourceTree = "";
@@ -620,7 +664,7 @@
CABFA9FC2592F13300380FEE /* LegibleError */,
CA9FF86C25951C6E00E47BAF /* XCModel */,
CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */,
- CAFE4A9925B7C7A30064FE51 /* Sparkle */,
+ E8F81FC3282D8A17006CBD0F /* Sparkle */,
);
productName = XcodesMac;
productReference = CAD2E79E2449574E00113D76 /* Xcodes.app */;
@@ -676,6 +720,22 @@
knownRegions = (
en,
Base,
+ hi,
+ fr,
+ ru,
+ es,
+ "zh-Hans",
+ tr,
+ ko,
+ it,
+ ja,
+ "zh-Hant",
+ de,
+ uk,
+ fi,
+ "pt-BR",
+ nl,
+ pl,
);
mainGroup = CAD2E7952449574E00113D76;
packageReferences = (
@@ -687,7 +747,7 @@
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */,
CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */,
CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */,
- CAFE4A9825B7C7A30064FE51 /* XCRemoteSwiftPackageReference "Sparkle" */,
+ E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */,
);
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
projectDirPath = "";
@@ -705,6 +765,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ E872EE4E2808D4F100D3DD8B /* Localizable.strings in Resources */,
CAD2E7A92449575000113D76 /* Preview Assets.xcassets in Resources */,
CA9FF83F2594FBC000E47BAF /* Licenses.rtf in Resources */,
CAA858DB25A3E11F00ACF8C0 /* aria2-release-1.35.0.tar.gz in Resources */,
@@ -785,12 +846,14 @@
CABFA9BD2592EEEA00380FEE /* Environment.swift in Sources */,
CABFA9C32592EEEA00380FEE /* Downloads.swift in Sources */,
CAC281DA259F985100B8AB0B /* InstallationStep.swift in Sources */,
+ E8CBDB8B27AE02FF00B22292 /* ExperiementsPreferencePane.swift in Sources */,
E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */,
CA378F992466567600A58CE0 /* AppState.swift in Sources */,
CAD2E7A42449574E00113D76 /* XcodeListView.swift in Sources */,
CAA1CB45255A5B60003FD669 /* SignIn2FAView.swift in Sources */,
CABFA9C52592EEEA00380FEE /* FileManager+.swift in Sources */,
CABFA9CD2592EEEA00380FEE /* Foundation.swift in Sources */,
+ 36741BFF291E50F500A85AAE /* FileError.swift in Sources */,
CA9FF8872595607900E47BAF /* InstalledXcode.swift in Sources */,
53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */,
CA42DD6E25AEA8B200BC0B0C /* Logger.swift in Sources */,
@@ -800,6 +863,8 @@
CAFE4AB425B7D3AF0064FE51 /* AdvancedPreferencePane.swift in Sources */,
CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */,
CABFA9C12592EEEA00380FEE /* Version+.swift in Sources */,
+ E8D655C0288DD04700A139C2 /* SelectedActionType.swift in Sources */,
+ 36741BFD291E4FDB00A85AAE /* DownloadPreferencePane.swift in Sources */,
CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */,
CABFA9CE2592EEEA00380FEE /* Version+Xcode.swift in Sources */,
CAFBDB912598FE80003DCC5A /* SelectedXcode.swift in Sources */,
@@ -807,6 +872,7 @@
CA25192A25A9644800F08414 /* XcodeInstallState.swift in Sources */,
E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */,
CAA1CB35255A5AD5003FD669 /* SignInCredentialsView.swift in Sources */,
+ E81D7EA02805250100A205FC /* Collection+.swift in Sources */,
CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */,
CABFAA2D2592FBFC00380FEE /* Configure.swift in Sources */,
CA73510D257BFCEF00EA9CF8 /* NSAttributedString+.swift in Sources */,
@@ -816,6 +882,7 @@
CAC281CD259F97FA00B8AB0B /* ObservingProgressIndicator.swift in Sources */,
CABFA9C22592EEEA00380FEE /* Publisher+Resumable.swift in Sources */,
CAFBDC68259A308B003DCC5A /* InfoPane.swift in Sources */,
+ E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */,
CAA1CB4D255A5CFD003FD669 /* SignInPhoneListView.swift in Sources */,
CAFBDC6C259A3098003DCC5A /* View+Conditional.swift in Sources */,
CABFA9CF2592EEEA00380FEE /* Process.swift in Sources */,
@@ -828,6 +895,7 @@
CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */,
CABFA9C92592EEEA00380FEE /* URLRequest+Apple.swift in Sources */,
CABFAA432593104F00380FEE /* AboutView.swift in Sources */,
+ E8D0296F284B029800647641 /* BottomStatusBar.swift in Sources */,
E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */,
CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */,
CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */,
@@ -866,11 +934,39 @@
};
/* End PBXTargetDependency section */
+/* Begin PBXVariantGroup section */
+ E872EE502808D4F100D3DD8B /* Localizable.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ E872EE4F2808D4F100D3DD8B /* en */,
+ 15FAD1652811D15600B63259 /* hi */,
+ B648F22B2810C1130096781B /* fr */,
+ 4A5AAA1D28118FAD00528958 /* ru */,
+ 58F743C02810A34900EEC0F3 /* es */,
+ C0AE7FA4283002DC00DA63D2 /* zh-Hans */,
+ 9D3C3AF7282EBE3300CB0D37 /* tr */,
+ AAB037D32839BD4700017680 /* ko */,
+ 25E2FA26284769A00014A318 /* it */,
+ AB4EB0DE28541FA000FF3B1D /* ja */,
+ 6CA3A090282EBE72005A6E35 /* zh-Hant */,
+ A0187D39285792D1002F46F9 /* de */,
+ 7CBF284E28606D2C001AA66B /* uk */,
+ 5AA8A6102877EDAD009ECDB0 /* fi */,
+ 327DF109286ABE6B00D694D5 /* pt-BR */,
+ E2AFDCCA28F024D000864ADD /* nl */,
+ 23703D6E29EBF63500DFA346 /* pl */,
+ );
+ name = Localizable.strings;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
/* Begin XCBuildConfiguration section */
CA8FB635256E154800469DA5 /* Test */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -924,6 +1020,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Test;
@@ -936,6 +1033,7 @@
CODE_SIGN_IDENTITY = "-";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
@@ -945,7 +1043,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MARKETING_VERSION = 1.1.0;
+ MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp;
PRODUCT_NAME = Xcodes;
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1054,6 +1152,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -1107,6 +1206,7 @@
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
@@ -1115,6 +1215,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
@@ -1161,6 +1262,7 @@
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
name = Release;
@@ -1172,6 +1274,7 @@
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = PBH8V487HB;
ENABLE_HARDENED_RUNTIME = YES;
@@ -1181,7 +1284,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MARKETING_VERSION = 1.1.0;
+ MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp;
PRODUCT_NAME = Xcodes;
SWIFT_VERSION = 5.0;
@@ -1195,6 +1298,7 @@
CODE_SIGN_ENTITLEMENTS = Xcodes/Resources/Xcodes.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
DEVELOPMENT_TEAM = PBH8V487HB;
ENABLE_HARDENED_RUNTIME = YES;
@@ -1204,7 +1308,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
- MARKETING_VERSION = 1.1.0;
+ MARKETING_VERSION = 1.10.0;
PRODUCT_BUNDLE_IDENTIFIER = com.robotsandpencils.XcodesApp;
PRODUCT_NAME = Xcodes;
SWIFT_VERSION = 5.0;
@@ -1363,12 +1467,12 @@
minimumVersion = 0.6.0;
};
};
- CAFE4A9825B7C7A30064FE51 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
+ E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */ = {
isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/sparkle-project/Sparkle/";
+ repositoryURL = "https://github.com/sparkle-project/Sparkle";
requirement = {
- kind = exactVersion;
- version = "1.24.0-spm";
+ kind = upToNextMajorVersion;
+ minimumVersion = 2.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -1418,9 +1522,9 @@
package = CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */;
productName = CombineExpectations;
};
- CAFE4A9925B7C7A30064FE51 /* Sparkle */ = {
+ E8F81FC3282D8A17006CBD0F /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
- package = CAFE4A9825B7C7A30064FE51 /* XCRemoteSwiftPackageReference "Sparkle" */;
+ package = E8F81FC2282D8A17006CBD0F /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
/* End XCSwiftPackageProductDependency section */
diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index c92db7b..58f4e08 100644
--- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -60,8 +60,8 @@
"repositoryURL": "https://github.com/sparkle-project/Sparkle/",
"state": {
"branch": null,
- "revision": "891afd44c7075e699924ed9b81d8dc94a5111dfd",
- "version": "1.24.0-spm"
+ "revision": "286edd1fa22505a9e54d170e9fd07d775ea233f2",
+ "version": "2.1.0"
}
},
{
diff --git a/Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme b/Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme
index b181a5b..f0379f7 100644
--- a/Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme
+++ b/Xcodes.xcodeproj/xcshareddata/xcschemes/Xcodes.xcscheme
@@ -44,6 +44,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = "en"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
diff --git a/Xcodes/AcknowledgementsGenerator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/Xcodes/AcknowledgementsGenerator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/Xcodes/AcknowledgementsGenerator/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/Xcodes/AcknowledgementsGenerator/Package.swift b/Xcodes/AcknowledgementsGenerator/Package.swift
index c41837e..622680e 100644
--- a/Xcodes/AcknowledgementsGenerator/Package.swift
+++ b/Xcodes/AcknowledgementsGenerator/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.3
+// swift-tools-version:5.4
import PackageDescription
@@ -14,7 +14,7 @@ let package = Package(
dependencies: [
],
targets: [
- .target(
+ .executableTarget(
name: "AcknowledgementsGenerator",
dependencies: []
),
diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift
index 462286d..6f33a4a 100644
--- a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift
+++ b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift
@@ -14,12 +14,22 @@ public class Client {
return Current.network.dataTask(with: URLRequest.itcServiceKey)
.map(\.data)
.decode(type: ServiceKeyResponse.self, decoder: JSONDecoder())
- .flatMap { serviceKeyResponse -> AnyPublisher in
+ .flatMap { serviceKeyResponse -> AnyPublisher<(String, String), Swift.Error> in
serviceKey = serviceKeyResponse.authServiceKey
- return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password))
- .mapError { $0 as Swift.Error }
+
+ // Fixes issue https://github.com/RobotsAndPencils/XcodesApp/issues/360
+ // On 2023-02-23, Apple added a custom implementation of hashcash to their auth flow
+ // Without this addition, Apple ID's would get set to locked
+ return self.loadHashcash(accountName: accountName, serviceKey: serviceKey)
+ .map { return (serviceKey, $0)}
.eraseToAnyPublisher()
}
+ .flatMap { (serviceKey, hashcash) -> AnyPublisher in
+
+ return Current.network.dataTask(with: URLRequest.signIn(serviceKey: serviceKey, accountName: accountName, password: password, hashcash: hashcash))
+ .mapError { $0 as Swift.Error }
+ .eraseToAnyPublisher()
+ }
.flatMap { result -> AnyPublisher in
let (data, response) = result
return Just(data)
@@ -56,6 +66,44 @@ public class Client {
.mapError { $0 as Swift.Error }
.eraseToAnyPublisher()
}
+
+ func loadHashcash(accountName: String, serviceKey: String) -> AnyPublisher {
+
+ Result {
+ try URLRequest.federate(account: accountName, serviceKey: serviceKey)
+ }
+ .publisher
+ .flatMap { request in
+ Current.network.dataTask(with: request)
+ .mapError { $0 as Error }
+ .tryMap { (data, response) throws -> (String) in
+ guard let urlResponse = response as? HTTPURLResponse else {
+ throw AuthenticationError.invalidSession
+ }
+ switch urlResponse.statusCode {
+ case 200..<300:
+
+ let httpResponse = response as! HTTPURLResponse
+ guard let bitsString = httpResponse.allHeaderFields["X-Apple-HC-Bits"] as? String, let bits = UInt(bitsString) else {
+ throw AuthenticationError.invalidHashcash
+ }
+ guard let challenge = httpResponse.allHeaderFields["X-Apple-HC-Challenge"] as? String else {
+ throw AuthenticationError.invalidHashcash
+ }
+ guard let hashcash = Hashcash().mint(resource: challenge, bits: bits) else {
+ throw AuthenticationError.invalidHashcash
+ }
+ return (hashcash)
+ case 400, 401:
+ throw AuthenticationError.invalidHashcash
+ case let code:
+ throw AuthenticationError.badStatusCode(statusCode: code, data: data, response: urlResponse)
+ }
+ }
+ }
+ .eraseToAnyPublisher()
+
+ }
func handleTwoStepOrFactor(data: Data, response: URLResponse, serviceKey: String) -> AnyPublisher {
let httpResponse = response as! HTTPURLResponse
@@ -148,11 +196,22 @@ public class Client {
/// Use the olympus session endpoint to see if the existing session is still valid
public func validateSession() -> AnyPublisher {
return Current.network.dataTask(with: URLRequest.olympusSession)
- .tryMap { (data, response) in
- guard
- let jsonObject = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
- jsonObject["provider"] != nil
- else { throw AuthenticationError.invalidSession }
+ .tryMap { result -> Data in
+ let httpResponse = result.response as! HTTPURLResponse
+ if httpResponse.statusCode == 401 {
+ throw AuthenticationError.notAuthorized
+ }
+
+ return result.data
+ }
+ .decode(type: AppleSession.self, decoder: JSONDecoder())
+ .tryMap { session in
+ // A user that is a non-paid Apple Developer will have a provider == nil
+ // Those users can still download Xcode.
+ // Non Apple Developers will get caught in the download as invalid
+// if session.provider == nil {
+// throw AuthenticationError.notDeveloperAppleId
+// }
}
.eraseToAnyPublisher()
}
@@ -174,10 +233,12 @@ public enum AuthenticationState: Equatable {
case unauthenticated
case waitingForSecondFactor(TwoFactorOption, AuthOptionsResponse, AppleSessionData)
case authenticated
+ case notAppleDeveloper
}
public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
case invalidSession
+ case invalidHashcash
case invalidUsernameOrPassword(username: String)
case incorrectSecurityCode
case unexpectedSignInResponse(statusCode: Int, message: String?)
@@ -186,11 +247,16 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
case accountUsesUnknownAuthenticationKind(String?)
case accountLocked(String)
case badStatusCode(statusCode: Int, data: Data, response: HTTPURLResponse)
-
+ case notDeveloperAppleId
+ case notAuthorized
+ case invalidResult(resultString: String?)
+
public var errorDescription: String? {
switch self {
case .invalidSession:
return "Your authentication session is invalid. Try signing in again."
+ case .invalidHashcash:
+ return "Could not create a hashcash for the session."
case .invalidUsernameOrPassword:
return "Invalid username and password combination."
case .incorrectSecurityCode:
@@ -212,6 +278,12 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable {
return message
case let .badStatusCode(statusCode, _, _):
return "Received an unexpected status code: \(statusCode). If you continue to have problems, please submit a bug report in the Help menu."
+ case .notDeveloperAppleId:
+ return "You are not registered as an Apple Developer. Please visit Apple Developer Registration. https://developer.apple.com/register/"
+ case .notAuthorized:
+ return "You are not authorized. Please Sign in with your Apple ID first."
+ case let .invalidResult(resultString):
+ return resultString ?? "If you continue to have problems, please submit a bug report in the Help menu."
}
}
}
@@ -362,3 +434,20 @@ public enum SecurityCode {
}
}
}
+
+/// Object returned from olympus/v1/session
+/// Used to check Provider, and show name
+/// If Provider is nil, we can assume the Apple User is NOT an Apple Developer and can't download Xcode.
+public struct AppleSession: Decodable, Equatable {
+ public let user: AppleUser
+ public let provider: AppleProvider?
+}
+
+public struct AppleProvider: Decodable, Equatable {
+ public let providerId: Int
+ public let name: String
+}
+
+public struct AppleUser: Decodable, Equatable {
+ public let fullName: String
+}
diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift
new file mode 100644
index 0000000..135f1ea
--- /dev/null
+++ b/Xcodes/AppleAPI/Sources/AppleAPI/Hashcash.swift
@@ -0,0 +1,96 @@
+//
+// Hashcash.swift
+//
+//
+// Created by Matt Kiazyk on 2023-02-23.
+//
+
+import Foundation
+import CryptoKit
+import CommonCrypto
+
+/*
+# This App Store Connect hashcash spec was generously donated by...
+ #
+ # __ _
+ # __ _ _ __ _ __ / _|(_) __ _ _ _ _ __ ___ ___
+ # / _` || '_ \ | '_ \ | |_ | | / _` || | | || '__|/ _ \/ __|
+ # | (_| || |_) || |_) || _|| || (_| || |_| || | | __/\__ \
+ # \__,_|| .__/ | .__/ |_| |_| \__, | \__,_||_| \___||___/
+ # |_| |_| |___/
+ #
+ #
+*/
+public struct Hashcash {
+ /// A function to returned a minted hash, using a bit and resource string
+ ///
+ /**
+ X-APPLE-HC: 1:11:20230223170600:4d74fb15eb23f465f1f6fcbf534e5877::6373
+ ^ ^ ^ ^ ^
+ | | | | +-- Counter
+ | | | +-- Resource
+ | | +-- Date YYMMDD[hhmm[ss]]
+ | +-- Bits (number of leading zeros)
+ +-- Version
+
+ We can't use an off-the-shelf Hashcash because Apple's implementation is not quite the same as the spec/convention.
+ 1. The spec calls for a nonce called "Rand" to be inserted between the Ext and Counter. They don't do that at all.
+ 2. The Counter conventionally encoded as base-64 but Apple just uses the decimal number's string representation.
+
+ Iterate from Counter=0 to Counter=N finding an N that makes the SHA1(X-APPLE-HC) lead with Bits leading zero bits
+ We get the "Resource" from the X-Apple-HC-Challenge header and Bits from X-Apple-HC-Bits
+ */
+ /// - Parameters:
+ /// - resource: a string to be used for minting
+ /// - bits: grabbed from `X-Apple-HC-Bits` header
+ /// - date: Default uses Date() otherwise used for testing to check.
+ /// - Returns: A String hash to use in `X-Apple-HC` header on /signin
+ public func mint(resource: String,
+ bits: UInt = 10,
+ date: String? = nil) -> String? {
+
+ let ver = "1"
+
+ var ts: String
+ if let date = date {
+ ts = date
+ } else {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyMMddHHmmss"
+ ts = formatter.string(from: Date())
+ }
+
+ let challenge = "\(ver):\(bits):\(ts):\(resource):"
+
+ var counter = 0
+
+ while true {
+ guard let digest = ("\(challenge):\(counter)").sha1 else {
+ print("ERROR: Can't generate SHA1 digest")
+ return nil
+ }
+
+ if digest == bits {
+ return "\(challenge):\(counter)"
+ }
+ counter += 1
+ }
+ }
+}
+
+extension String {
+ var sha1: Int? {
+
+ let data = Data(self.utf8)
+ var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH))
+ data.withUnsafeBytes {
+ _ = CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest)
+ }
+ let bigEndianValue = digest.withUnsafeBufferPointer {
+ ($0.baseAddress!.withMemoryRebound(to: UInt32.self, capacity: 1) { $0 })
+ }.pointee
+ let value = UInt32(bigEndian: bigEndianValue)
+ return value.leadingZeroBitCount
+ }
+}
+
diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift b/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift
index 01b98fb..d052d63 100644
--- a/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift
+++ b/Xcodes/AppleAPI/Sources/AppleAPI/URLRequest+Apple.swift
@@ -7,6 +7,7 @@ public extension URL {
static let requestSecurityCode = URL(string: "https://idmsa.apple.com/appleauth/auth/verify/phone")!
static func submitSecurityCode(_ code: SecurityCode) -> URL { URL(string: "https://idmsa.apple.com/appleauth/auth/verify/\(code.urlPathComponent)/securitycode")! }
static let trust = URL(string: "https://idmsa.apple.com/appleauth/auth/2sv/trust")!
+ static let federate = URL(string: "https://idmsa.apple.com/appleauth/auth/federate")!
static let olympusSession = URL(string: "https://appstoreconnect.apple.com/olympus/v1/session")!
}
@@ -15,7 +16,7 @@ public extension URLRequest {
return URLRequest(url: .itcServiceKey)
}
- static func signIn(serviceKey: String, accountName: String, password: String) -> URLRequest {
+ static func signIn(serviceKey: String, accountName: String, password: String, hashcash: String) -> URLRequest {
struct Body: Encodable {
let accountName: String
let password: String
@@ -27,6 +28,7 @@ public extension URLRequest {
request.allHTTPHeaderFields?["Content-Type"] = "application/json"
request.allHTTPHeaderFields?["X-Requested-With"] = "XMLHttpRequest"
request.allHTTPHeaderFields?["X-Apple-Widget-Key"] = serviceKey
+ request.allHTTPHeaderFields?["X-Apple-HC"] = hashcash
request.allHTTPHeaderFields?["Accept"] = "application/json, text/javascript"
request.httpMethod = "POST"
request.httpBody = try! JSONEncoder().encode(Body(accountName: accountName, password: password))
@@ -117,4 +119,21 @@ public extension URLRequest {
static var olympusSession: URLRequest {
return URLRequest(url: .olympusSession)
}
+
+ static func federate(account: String, serviceKey: String) throws -> URLRequest {
+ struct FederateRequest: Encodable {
+ let accountName: String
+ let rememberMe: Bool
+ }
+ var request = URLRequest(url: .signIn)
+ request.allHTTPHeaderFields?["Accept"] = "application/json"
+ request.allHTTPHeaderFields?["Content-Type"] = "application/json"
+ request.httpMethod = "GET"
+
+// let encoder = JSONEncoder()
+// encoder.outputFormatting = .withoutEscapingSlashes
+// request.httpBody = try encoder.encode(FederateRequest(accountName: account, rememberMe: true))
+
+ return request
+ }
}
diff --git a/Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift b/Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift
index 283dbc6..ecb93e7 100644
--- a/Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift
+++ b/Xcodes/AppleAPI/Tests/AppleAPITests/AppleAPITests.swift
@@ -2,14 +2,26 @@ import XCTest
@testable import AppleAPI
final class AppleAPITests: XCTestCase {
- func testExample() {
- // This is an example of a functional test case.
- // Use XCTAssert and related functions to verify your tests produce the correct
- // results.
- XCTAssertEqual(AppleAPI().text, "Hello, World!")
+
+ func testValidHashCashMint() {
+ let bits: UInt = 11
+ let resource = "4d74fb15eb23f465f1f6fcbf534e5877"
+ let testDate = "20230223170600"
+
+ let stamp = Hashcash().mint(resource: resource, bits: bits, date: testDate)
+ XCTAssertEqual(stamp, "1:11:20230223170600:4d74fb15eb23f465f1f6fcbf534e5877::6373")
+ }
+ func testValidHashCashMint2() {
+ let bits: UInt = 10
+ let resource = "bb63edf88d2f9c39f23eb4d6f0281158"
+ let testDate = "20230224001754"
+
+ let stamp = Hashcash().mint(resource: resource, bits: bits, date: testDate)
+ XCTAssertEqual(stamp, "1:10:20230224001754:bb63edf88d2f9c39f23eb4d6f0281158::866")
}
static var allTests = [
- ("testExample", testExample),
+ ("testValidHashCashMint", testValidHashCashMint),
+ ("testValidHashCashMint2", testValidHashCashMint2),
]
}
diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift
index d295d68..ccf637d 100644
--- a/Xcodes/Backend/AppState+Install.swift
+++ b/Xcodes/Backend/AppState+Install.swift
@@ -24,10 +24,10 @@ extension AppState {
if autoInstallType == .newestBeta {
Logger.appState.info("Auto installing newest Xcode Beta")
// install it, as user doesn't have it installed and it's either latest beta or latest release
- install(id: newestXcode.id)
+ checkMinVersionAndInstall(id: newestXcode.id)
} else if autoInstallType == .newestVersion && newestXcode.version.isNotPrerelease {
Logger.appState.info("Auto installing newest Xcode")
- install(id: newestXcode.id)
+ checkMinVersionAndInstall(id: newestXcode.id)
} else {
Logger.appState.info("No new Xcodes version found to auto install")
}
@@ -40,9 +40,13 @@ extension AppState {
}
private func install(_ installationType: InstallationType, downloader: Downloader, attemptNumber: Int) -> AnyPublisher {
+
Logger.appState.info("Using \(downloader) downloader")
- return getXcodeArchive(installationType, downloader: downloader)
+ return validateSession()
+ .flatMap { _ in
+ self.getXcodeArchive(installationType, downloader: downloader)
+ }
.flatMap { xcode, url -> AnyPublisher in
self.installArchivedXcode(xcode, at: url)
}
@@ -82,22 +86,23 @@ extension AppState {
private func getXcodeArchive(_ installationType: InstallationType, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
switch installationType {
case .version(let availableXcode):
- if let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version.isEquivalent(to: availableXcode.version) }) {
+ if let installedXcode = Current.files.installedXcodes(Path.installDirectory).first(where: { $0.version.isEquivalent(to: availableXcode.version) }) {
return Fail(error: InstallationError.versionAlreadyInstalled(installedXcode))
.eraseToAnyPublisher()
}
+
return downloadXcode(availableXcode: availableXcode, downloader: downloader)
}
}
private func downloadXcode(availableXcode: AvailableXcode, downloader: Downloader) -> AnyPublisher<(AvailableXcode, URL), Error> {
- downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
- DispatchQueue.main.async {
- self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
- }
- })
- .map { return (availableXcode, $0) }
- .eraseToAnyPublisher()
+ self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in
+ DispatchQueue.main.async {
+ self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress))
+ }
+ })
+ .map { return (availableXcode, $0) }
+ .eraseToAnyPublisher()
}
public func downloadOrUseExistingArchive(for availableXcode: AvailableXcode, downloader: Downloader, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher {
@@ -172,7 +177,7 @@ extension AppState {
public func installArchivedXcode(_ availableXcode: AvailableXcode, at archiveURL: URL) -> AnyPublisher {
do {
- let destinationURL = Path.root.join("Applications").join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url
+ let destinationURL = Path.installDirectory.join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url
switch archiveURL.pathExtension {
case "xip":
return unarchiveAndMoveXIP(availableXcode: availableXcode, at: archiveURL, to: destinationURL)
@@ -208,7 +213,7 @@ extension AppState {
.handleEvents(receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
- self.presentedAlert = .generic(title: "Unable to install archived Xcode", message: error.legibleLocalizedDescription)
+ self.presentedAlert = .generic(title: localizeString("Alert.InstallArchive.Error.Title"), message: error.legibleLocalizedDescription)
}
})
.catch { _ in
@@ -232,8 +237,8 @@ extension AppState {
func unarchiveAndMoveXIP(availableXcode: AvailableXcode, at source: URL, to destination: URL) -> AnyPublisher {
self.setInstallationStep(of: availableXcode.version, to: .unarchiving)
-
- return Current.shell.unxip(source)
+
+ return unxipOrUnxipExperiment(source)
.catch { error -> AnyPublisher in
if let executionError = error as? ProcessExecutionError {
if executionError.standardError.contains("damaged and can’t be expanded") {
@@ -272,6 +277,16 @@ extension AppState {
})
.eraseToAnyPublisher()
}
+
+ func unxipOrUnxipExperiment(_ source: URL) -> AnyPublisher {
+ if unxipExperiment {
+ // All hard work done by https://github.com/saagarjha/unxip
+ // Compiled to binary with `swiftc -parse-as-library -O unxip.swift`
+ return Current.shell.unxipExperiment(source)
+ } else {
+ return Current.shell.unxip(source)
+ }
+ }
public func verifySecurityAssessment(of xcode: InstalledXcode) -> AnyPublisher {
return Current.shell.spctlAssess(xcode.path.url)
@@ -345,7 +360,7 @@ extension AppState {
receiveCompletion: { completion in
if case let .failure(error) = completion {
self.error = error
- self.presentedAlert = .generic(title: "Unable to perform post install steps", message: error.legibleLocalizedDescription)
+ self.presentedAlert = .generic(title: localizeString("Alert.PostInstall.Title"), message: error.legibleLocalizedDescription)
}
},
receiveValue: {}
@@ -486,60 +501,41 @@ public enum InstallationError: LocalizedError, Equatable {
public var errorDescription: String? {
switch self {
case .damagedXIP(let url):
- return "The archive \"\(url.lastPathComponent)\" is damaged and can't be expanded."
+ return String(format: localizeString("InstallationError.DamagedXIP"), url.lastPathComponent)
case let .notEnoughFreeSpaceToExpandArchive(archivePath, version):
- return """
- The archive “\(archivePath.basename())” can’t be expanded because the current volume doesn’t have enough free space.
-
- Make more space available to expand the archive and then install Xcode \(version.appleDescription) again to start installation from where it left off.
- """
+ return String(format: localizeString("InstallationError.NotEnoughFreeSpaceToExpandArchive"), archivePath.basename(), version.appleDescription)
case .failedToMoveXcodeToApplications:
- return "Failed to move Xcode to the /Applications directory."
+ return String(format: localizeString("InstallationError.FailedToMoveXcodeToApplications"), Path.installDirectory.string)
case .failedSecurityAssessment(let xcode, let output):
- return """
- Xcode \(xcode.version) failed its security assessment with the following output:
- \(output)
- It remains installed at \(xcode.path) if you wish to use it anyways.
- """
+ return String(format: localizeString("InstallationError.FailedSecurityAssessment"), String(xcode.version), output, xcode.path.string)
case .codesignVerifyFailed(let output):
- return """
- The downloaded Xcode failed code signing verification with the following output:
- \(output)
- """
+ return String(format: localizeString("InstallationError.CodesignVerifyFailed"), output)
case .unexpectedCodeSigningIdentity(let identity, let certificateAuthority):
- return """
- The downloaded Xcode doesn't have the expected code signing identity.
- Got:
- \(identity)
- \(certificateAuthority)
- Expected:
- \(XcodeTeamIdentifier)
- \(XcodeCertificateAuthority)
- """
+ return String(format: localizeString("InstallationError.UnexpectedCodeSigningIdentity"), identity, certificateAuthority, XcodeTeamIdentifier, XcodeCertificateAuthority)
case .unsupportedFileFormat(let fileExtension):
- return "xcodes doesn't (yet) support installing Xcode from the \(fileExtension) file format."
+ return String(format: localizeString("InstallationError.UnsupportedFileFormat"), fileExtension)
case .missingSudoerPassword:
- return "Missing password. Please try again."
+ return localizeString("InstallationError.MissingSudoerPassword")
case let .unavailableVersion(version):
- return "Could not find version \(version.appleDescription)."
+ return String(format: localizeString("InstallationError.UnavailableVersion"), version.appleDescription)
case .noNonPrereleaseVersionAvailable:
- return "No non-prerelease versions available."
+ return localizeString("InstallationError.NoNonPrereleaseVersionAvailable")
case .noPrereleaseVersionAvailable:
- return "No prerelease versions available."
+ return localizeString("InstallationError.NoPrereleaseVersionAvailable")
case .missingUsernameOrPassword:
- return "Missing username or a password. Please try again."
+ return localizeString("InstallationError.MissingUsernameOrPassword")
case let .versionAlreadyInstalled(installedXcode):
- return "\(installedXcode.version.appleDescription) is already installed at \(installedXcode.path)"
+ return String(format: localizeString("InstallationError.VersionAlreadyInstalled"), installedXcode.version.appleDescription, installedXcode.path.string)
case let .invalidVersion(version):
- return "\(version) is not a valid version number."
+ return String(format: localizeString("InstallationError.InvalidVersion"), version)
case let .versionNotInstalled(version):
- return "\(version.appleDescription) is not installed."
+ return String(format: localizeString("InstallationError.VersionNotInstalled"), version.appleDescription)
case let .postInstallStepsNotPerformed(version, helperInstallState):
switch helperInstallState {
case .installed:
- return "Installation was completed, but some post-install steps weren't performed automatically. These will be performed when you first launch Xcode \(version.appleDescription)."
+ return String(format: localizeString("InstallationError.PostInstallStepsNotPerformed.Installed"), version.appleDescription)
case .notInstalled, .unknown:
- return "Installation was completed, but some post-install steps weren't performed automatically. Xcodes performs these steps with a privileged helper, which appears to not be installed. You can install it from Preferences > Advanced.\n\nThese steps will be performed when you first launch Xcode \(version.appleDescription)."
+ return String(format: localizeString("InstallationError.PostInstallStepsNotPerformed.NotInstalled"), version.appleDescription)
}
}
}
diff --git a/Xcodes/Backend/AppState+Update.swift b/Xcodes/Backend/AppState+Update.swift
index 62a327d..8dad172 100644
--- a/Xcodes/Backend/AppState+Update.swift
+++ b/Xcodes/Backend/AppState+Update.swift
@@ -47,7 +47,7 @@ extension AppState {
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
if error as? AuthenticationError != .invalidSession {
self.error = error
- self.presentedAlert = .generic(title: "Unable to update selected Xcode", message: error.legibleLocalizedDescription)
+ self.presentedAlert = .generic(title: localizeString("Alert.Update.Error.Title"), message: error.legibleLocalizedDescription)
}
case .finished:
Current.defaults.setDate(Current.date(), forKey: "lastUpdated")
@@ -71,7 +71,12 @@ extension AppState {
private func updateAvailableXcodes(from dataSource: DataSource) -> AnyPublisher<[AvailableXcode], Error> {
switch dataSource {
case .apple:
- return signInIfNeeded()
+ return signInIfNeeded()
+ .flatMap { [unowned self] in
+ // this will check to see if the Apple ID is a valid Apple Developer or not.
+ // If it's not, we can't use the Apple source to get xcode info.
+ self.validateSession()
+ }
.flatMap { [unowned self] in self.releasedXcodes().combineLatest(self.prereleaseXcodes()) }
.receive(on: DispatchQueue.main)
.map { releasedXcodes, prereleaseXcodes in
@@ -125,15 +130,20 @@ extension AppState {
extension AppState {
// MARK: - Apple
- private func releasedXcodes() -> AnyPublisher<[AvailableXcode], Error> {
+ private func releasedXcodes() -> AnyPublisher<[AvailableXcode], Swift.Error> {
Current.network.dataTask(with: URLRequest.downloads)
.map(\.data)
.decode(type: Downloads.self, decoder: configure(JSONDecoder()) {
$0.dateDecodingStrategy = .formatted(.downloadsDateModified)
})
- .map { downloads -> [AvailableXcode] in
- let xcodes = downloads
- .downloads
+ .tryMap { downloads -> [AvailableXcode] in
+ if downloads.hasError {
+ throw AuthenticationError.invalidResult(resultString: downloads.resultsString)
+ }
+ guard let downloadList = downloads.downloads else {
+ throw AuthenticationError.invalidResult(resultString: localizeString("DownloadingError"))
+ }
+ let xcodes = downloadList
.filter { $0.name.range(of: "^Xcode [0-9]", options: .regularExpression) != nil }
.compactMap { download -> AvailableXcode? in
let urlPrefix = URL(string: "https://download.developer.apple.com/")!
diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift
index 3f5731e..ebd44b5 100644
--- a/Xcodes/Backend/AppState.swift
+++ b/Xcodes/Backend/AppState.swift
@@ -10,18 +10,18 @@ import os.log
class AppState: ObservableObject {
private let client = AppleAPI.Client()
-
+
// MARK: - Published Properties
@Published var authenticationState: AuthenticationState = .unauthenticated
@Published var availableXcodes: [AvailableXcode] = [] {
willSet {
if newValue.count > availableXcodes.count && availableXcodes.count != 0 {
- Current.notificationManager.scheduleNotification(title: "New Xcode versions", body: "New Xcode versions are available to download.", category: .normal)
+ Current.notificationManager.scheduleNotification(title: localizeString("Notification.NewXcodeVersion.Title"), body: localizeString("Notification.NewXcodeVersion.Body"), category: .normal)
}
updateAllXcodes(
availableXcodes: newValue,
- installedXcodes: Current.files.installedXcodes(Path.root/"Applications"),
+ installedXcodes: Current.files.installedXcodes(Path.installDirectory),
selectedXcodePath: selectedXcodePath
)
}
@@ -34,7 +34,7 @@ class AppState: ObservableObject {
willSet {
updateAllXcodes(
availableXcodes: availableXcodes,
- installedXcodes: Current.files.installedXcodes(Path.root/"Applications"),
+ installedXcodes: Current.files.installedXcodes(Path.installDirectory),
selectedXcodePath: newValue
)
}
@@ -43,7 +43,6 @@ class AppState: ObservableObject {
var isUpdating: Bool { updatePublisher != nil }
@Published var presentedSheet: XcodesSheet? = nil
@Published var isProcessingAuthRequest = false
- @Published var secondFactorData: SecondFactorData?
@Published var xcodeBeingConfirmedForUninstallation: Xcode?
@Published var presentedAlert: XcodesAlert?
@Published var helperInstallState: HelperInstallState = .notInstalled
@@ -55,7 +54,51 @@ class AppState: ObservableObject {
@Published var error: Error?
@Published var authError: Error?
-
+
+ // MARK: Advanced Preferences
+ @Published var localPath = "" {
+ didSet {
+ Current.defaults.set(localPath, forKey: "localPath")
+ }
+ }
+
+ @Published var installPath = "" {
+ didSet {
+ Current.defaults.set(installPath, forKey: "installPath")
+ }
+ }
+
+ @Published var unxipExperiment = false {
+ didSet {
+ Current.defaults.set(unxipExperiment, forKey: "unxipExperiment")
+ }
+ }
+
+ @Published var createSymLinkOnSelect = false {
+ didSet {
+ Current.defaults.set(createSymLinkOnSelect, forKey: "createSymLinkOnSelect")
+ }
+ }
+
+ var createSymLinkOnSelectDisabled: Bool {
+ return onSelectActionType == .rename
+ }
+
+ @Published var onSelectActionType = SelectedActionType.none {
+ didSet {
+ Current.defaults.set(onSelectActionType.rawValue, forKey: "onSelectActionType")
+
+ if onSelectActionType == .rename {
+ createSymLinkOnSelect = false
+ }
+ }
+ }
+
+ @Published var showOpenInRosettaOption = false {
+ didSet {
+ Current.defaults.set(showOpenInRosettaOption, forKey: "showOpenInRosettaOption")
+ }
+ }
// MARK: - Publisher Cancellables
var cancellables = Set()
@@ -77,6 +120,17 @@ class AppState: ObservableObject {
savedUsername != nil
}
+ var bottomStatusBarMessage: String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "dd/MM/yyyy"
+ let finishDate = formatter.date(from: "11/06/2022")
+
+ if Date().compare(finishDate!) == .orderedAscending {
+ return String(format: localizeString("WWDC.Message"), "2022")
+ }
+ return ""
+ }
+
// MARK: - Init
init() {
@@ -84,6 +138,16 @@ class AppState: ObservableObject {
try? loadCachedAvailableXcodes()
checkIfHelperIsInstalled()
setupAutoInstallTimer()
+ setupDefaults()
+ }
+
+ func setupDefaults() {
+ localPath = Current.defaults.string(forKey: "localPath") ?? Path.defaultXcodesApplicationSupport.string
+ unxipExperiment = Current.defaults.bool(forKey: "unxipExperiment") ?? false
+ createSymLinkOnSelect = Current.defaults.bool(forKey: "createSymLinkOnSelect") ?? false
+ onSelectActionType = SelectedActionType(rawValue: Current.defaults.string(forKey: "onSelectActionType") ?? "none") ?? .none
+ installPath = Current.defaults.string(forKey: "installPath") ?? Path.defaultInstallDirectory.string
+ showOpenInRosettaOption = Current.defaults.bool(forKey: "showOpenInRosettaOption") ?? false
}
// MARK: Timer
@@ -99,13 +163,23 @@ class AppState: ObservableObject {
}
// MARK: - Authentication
+ func validateADCSession(path: String) -> AnyPublisher {
+ return Current.network.dataTask(with: URLRequest.downloadADCAuth(path: path))
+ .receive(on: DispatchQueue.main)
+ .tryMap { _ in
+ }
+ .eraseToAnyPublisher()
+ }
+
func validateSession() -> AnyPublisher {
- return client.validateSession()
+
+ return Current.network.validateSession()
.receive(on: DispatchQueue.main)
.handleEvents(receiveCompletion: { completion in
if case .failure = completion {
- self.authenticationState = .unauthenticated
- self.presentedSheet = .signIn
+ // this is causing some awkwardness with showing an alert with the error and also popping up the sign in view
+ // self.authenticationState = .unauthenticated
+ // self.presentedSheet = .signIn
}
})
.eraseToAnyPublisher()
@@ -159,12 +233,11 @@ class AppState: ObservableObject {
}
func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) {
- self.secondFactorData = SecondFactorData(
+ self.presentedSheet = .twoFactor(.init(
option: option,
authOptions: authOptions,
sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt)
- )
- self.presentedSheet = .twoFactor
+ ))
}
func requestSMS(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) {
@@ -187,7 +260,11 @@ class AppState: ObservableObject {
}
func choosePhoneNumberForSMS(authOptions: AuthOptionsResponse, sessionData: AppleSessionData) {
- secondFactorData = SecondFactorData(option: .smsPendingChoice, authOptions: authOptions, sessionData: sessionData)
+ self.presentedSheet = .twoFactor(.init(
+ option: .smsPendingChoice,
+ authOptions: authOptions,
+ sessionData: sessionData
+ ))
}
func submitSecurityCode(_ code: SecurityCode, sessionData: AppleSessionData) {
@@ -215,9 +292,8 @@ class AppState: ObservableObject {
self.authError = error
case .finished:
switch self.authenticationState {
- case .authenticated, .unauthenticated:
+ case .authenticated, .unauthenticated, .notAppleDeveloper:
self.presentedSheet = nil
- self.secondFactorData = nil
case let .waitingForSecondFactor(option, authOptions, sessionData):
self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt)
}
@@ -257,7 +333,7 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
- self.presentedAlert = .generic(title: "Unable to install helper", message: error.legibleLocalizedDescription)
+ self.presentedAlert = .generic(title: localizeString("Alert.PrivilegedHelper.Error.Title"), message: error.legibleLocalizedDescription)
}
},
receiveValue: {}
@@ -292,6 +368,29 @@ class AppState: ObservableObject {
// MARK: - Install
+ func checkMinVersionAndInstall(id: Xcode.ID) {
+ guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
+
+ // Check to see if users MacOS is supported
+ if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
+ let split = requiredMacOSVersion.components(separatedBy: ".").compactMap { Int($0) }
+ let xcodeMinimumMacOSVersion = OperatingSystemVersion(majorVersion: split[safe: 0] ?? 0, minorVersion: split[safe: 1] ?? 0, patchVersion: split[safe: 2] ?? 0)
+
+ if !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion) {
+ // prompt
+ self.presentedAlert = .checkMinSupportedVersion(xcode: availableXcode, macOS: ProcessInfo.processInfo.operatingSystemVersion.versionString())
+ return
+ }
+ }
+
+ switch self.dataSource {
+ case .apple:
+ install(id: id)
+ case .xcodeReleases:
+ install(id: id)
+ }
+ }
+
func install(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
@@ -303,7 +402,7 @@ class AppState: ObservableObject {
self.$authenticationState
.filter { state in
switch state {
- case .authenticated, .unauthenticated: return true
+ case .authenticated, .unauthenticated, .notAppleDeveloper: return true
case .waitingForSecondFactor: return false
}
}
@@ -312,6 +411,9 @@ class AppState: ObservableObject {
if state == .unauthenticated {
throw AuthenticationError.invalidSession
}
+ if state == .notAppleDeveloper {
+ throw AuthenticationError.notDeveloperAppleId
+ }
return Void()
}
}
@@ -321,8 +423,18 @@ class AppState: ObservableObject {
// We need the cookies from its response in order to download Xcodes though,
// so perform it here first just to be sure.
Current.network.dataTask(with: URLRequest.downloads)
- .receive(on: DispatchQueue.main)
- .map { _ in Void() }
+ .map(\.data)
+ .decode(type: Downloads.self, decoder: configure(JSONDecoder()) {
+ $0.dateDecodingStrategy = .formatted(.downloadsDateModified)
+ })
+ .tryMap { downloads -> Void in
+ if downloads.hasError {
+ throw AuthenticationError.invalidResult(resultString: downloads.resultsString)
+ }
+ if downloads.downloads == nil {
+ throw AuthenticationError.invalidResult(resultString: localizeString("DownloadingError"))
+ }
+ }
.mapError { $0 as Error }
}
.flatMap { [unowned self] in
@@ -336,7 +448,7 @@ class AppState: ObservableObject {
// Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
if error as? AuthenticationError != .invalidSession {
self.error = error
- self.presentedAlert = .generic(title: "Unable to install Xcode", message: error.legibleLocalizedDescription)
+ self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
}
if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
self.allXcodes[index].installState = .notInstalled
@@ -347,6 +459,31 @@ class AppState: ObservableObject {
)
}
+ /// Skips using the username/password to log in to Apple, and simply gets a Auth Cookie used in downloading
+ /// As of Nov 2022 this was returning a 403 forbidden
+ func installWithoutLogin(id: Xcode.ID) {
+ guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
+
+ installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2)
+ .receive(on: DispatchQueue.main)
+ .sink(
+ receiveCompletion: { [unowned self] completion in
+ self.installationPublishers[id] = nil
+ if case let .failure(error) = completion {
+ // Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
+ if error as? AuthenticationError != .invalidSession {
+ self.error = error
+ self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
+ }
+ if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
+ self.allXcodes[index].installState = .notInstalled
+ }
+ }
+ },
+ receiveValue: { _ in }
+ )
+ }
+
func cancelInstall(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
@@ -366,13 +503,13 @@ class AppState: ObservableObject {
}
// MARK: - Uninstall
- func uninstall(id: Xcode.ID) {
+ func uninstall(xcode: Xcode) {
guard
- let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }),
+ let installedXcodePath = xcode.installedPath,
uninstallPublisher == nil
else { return }
- uninstallPublisher = uninstallXcode(path: installedXcode.path)
+ uninstallPublisher = uninstallXcode(path: installedXcodePath)
.flatMap { [unowned self] _ in
self.updateSelectedXcodePath()
}
@@ -380,7 +517,7 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
- self.presentedAlert = .generic(title: "Unable to uninstall Xcode", message: error.legibleLocalizedDescription)
+ self.presentedAlert = .generic(title: localizeString("Alert.Uninstall.Error.Title"), message: error.legibleLocalizedDescription)
}
self.uninstallPublisher = nil
},
@@ -388,10 +525,15 @@ class AppState: ObservableObject {
)
}
- func reveal(id: Xcode.ID) {
+ func reveal(xcode: Xcode) {
// TODO: show error if not
- guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
- NSWorkspace.shared.activateFileViewerSelecting([installedXcode.path.url])
+ guard let installedXcodePath = xcode.installedPath else { return }
+ NSWorkspace.shared.activateFileViewerSelecting([installedXcodePath.url])
+ }
+
+ func reveal(path: String) {
+ let url = URL(fileURLWithPath: path)
+ NSWorkspace.shared.activateFileViewerSelecting([url])
}
/// Make an Xcode active, a.k.a select it, in the `xcode-select` sense.
@@ -404,26 +546,31 @@ class AppState: ObservableObject {
/// If they consent to installing the helper then this method will be invoked again with `shouldPrepareUserForHelperInstallation` set to false.
/// This will install the helper and make the Xcode active.
///
- /// - Parameter id: The identifier of the Xcode to make active.
+ /// - Parameter xcode: The Xcode to make active.
/// - Parameter shouldPrepareUserForHelperInstallation: Whether the user should be presented with an alert preparing them for helper installation before making the Xcode version active.
- func select(id: Xcode.ID, shouldPrepareUserForHelperInstallation: Bool = true) {
+ func select(xcode: Xcode, shouldPrepareUserForHelperInstallation: Bool = true) {
guard helperInstallState == .installed || shouldPrepareUserForHelperInstallation == false else {
isPreparingUserForActionRequiringHelper = { [unowned self] userConsented in
guard userConsented else { return }
- self.select(id: id, shouldPrepareUserForHelperInstallation: false)
+ self.select(xcode: xcode, shouldPrepareUserForHelperInstallation: false)
}
presentedAlert = .privilegedHelper
return
}
- guard
- let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }),
+ guard
+ var installedXcodePath = xcode.installedPath,
selectPublisher == nil
else { return }
+
+ if onSelectActionType == .rename {
+ guard let newDestinationXcodePath = renameToXcode(xcode: xcode) else { return }
+ installedXcodePath = newDestinationXcodePath
+ }
selectPublisher = installHelperIfNecessary()
.flatMap {
- Current.helper.switchXcodePath(installedXcode.path.string)
+ Current.helper.switchXcodePath(installedXcodePath.string)
}
.flatMap { [unowned self] _ in
self.updateSelectedXcodePath()
@@ -432,7 +579,11 @@ class AppState: ObservableObject {
receiveCompletion: { [unowned self] completion in
if case let .failure(error) = completion {
self.error = error
- self.presentedAlert = .generic(title: "Unable to select Xcode", message: error.legibleLocalizedDescription)
+ self.presentedAlert = .generic(title: localizeString("Alert.Select.Error.Title"), message: error.legibleLocalizedDescription)
+ } else {
+ if self.createSymLinkOnSelect {
+ createSymbolicLink(xcode: xcode)
+ }
}
self.selectPublisher = nil
},
@@ -440,16 +591,100 @@ class AppState: ObservableObject {
)
}
- func open(id: Xcode.ID) {
- guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
- NSWorkspace.shared.openApplication(at: installedXcode.path.url, configuration: .init())
+ func open(xcode: Xcode, openInRosetta: Bool? = false) {
+ switch xcode.installState {
+ case let .installed(path):
+ let config = NSWorkspace.OpenConfiguration.init()
+ if (openInRosetta ?? false) {
+ config.architecture = CPU_TYPE_X86_64
+ }
+ config.allowsRunningApplicationSubstitution = false
+ NSWorkspace.shared.openApplication(at: path.url, configuration: config)
+ default:
+ Logger.appState.error("\(xcode.id) is not installed")
+ return
+ }
}
- func copyPath(id: Xcode.ID) {
- guard let installedXcode = Current.files.installedXcodes(Path.root/"Applications").first(where: { $0.version == id }) else { return }
+ func copyPath(xcode: Xcode) {
+ guard let installedXcodePath = xcode.installedPath else { return }
+
NSPasteboard.general.declareTypes([.URL, .string], owner: nil)
- NSPasteboard.general.writeObjects([installedXcode.path.url as NSURL])
- NSPasteboard.general.setString(installedXcode.path.string, forType: .string)
+ NSPasteboard.general.writeObjects([installedXcodePath.url as NSURL])
+ NSPasteboard.general.setString(installedXcodePath.string, forType: .string)
+ }
+
+ func copyReleaseNote(xcode: Xcode) {
+ guard let url = xcode.releaseNotesURL else { return }
+ NSPasteboard.general.declareTypes([.URL, .string], owner: nil)
+ NSPasteboard.general.writeObjects([url as NSURL])
+ NSPasteboard.general.setString(url.absoluteString, forType: .string)
+ }
+
+ func createSymbolicLink(xcode: Xcode, isBeta: Bool = false) {
+ guard let installedXcodePath = xcode.installedPath else { return }
+
+ let destinationPath: Path = Path.installDirectory/"Xcode\(isBeta ? "-Beta" : "").app"
+
+ // does an Xcode.app file exist?
+ if FileManager.default.fileExists(atPath: destinationPath.string) {
+ do {
+ // if it's not a symlink, error because we don't want to delete an actual xcode.app file
+ let attributes: [FileAttributeKey : Any]? = try? FileManager.default.attributesOfItem(atPath: destinationPath.string)
+
+ if attributes?[.type] as? FileAttributeType == FileAttributeType.typeSymbolicLink {
+ try FileManager.default.removeItem(atPath: destinationPath.string)
+ Logger.appState.info("Successfully deleted old symlink")
+ } else {
+ self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: localizeString("Alert.SymLink.Message"))
+ return
+ }
+ } catch {
+ self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.localizedDescription)
+ }
+ }
+
+ do {
+ try FileManager.default.createSymbolicLink(atPath: destinationPath.string, withDestinationPath: installedXcodePath.string)
+ Logger.appState.info("Successfully created symbolic link with Xcode\(isBeta ? "-Beta": "").app")
+ } catch {
+ Logger.appState.error("Unable to create symbolic Link")
+ self.error = error
+ self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
+ }
+ }
+
+ func renameToXcode(xcode: Xcode) -> Path? {
+ guard let installedXcodePath = xcode.installedPath else { return nil }
+
+ let destinationPath: Path = Path.installDirectory/"Xcode.app"
+
+ // rename any old named `Xcode.app` to the Xcodes versioned named files
+ if FileManager.default.fileExists(atPath: destinationPath.string) {
+ if let originalXcode = Current.files.installedXcode(destination: destinationPath) {
+ let newName = "Xcode-\(originalXcode.version.descriptionWithoutBuildMetadata).app"
+ Logger.appState.debug("Found Xcode.app - renaming back to \(newName)")
+ do {
+ try destinationPath.rename(to: newName)
+ } catch {
+ Logger.appState.error("Unable to create rename Xcode.app back to original")
+ self.error = error
+ // TODO UPDATE MY ERROR STRING
+ self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
+ }
+ }
+ }
+ // rename passed in xcode to xcode.app
+ Logger.appState.debug("Found Xcode.app - renaming back to Xcode.app")
+ do {
+ return try installedXcodePath.rename(to: "Xcode.app")
+ } catch {
+ Logger.appState.error("Unable to create rename Xcode.app back to original")
+ self.error = error
+ // TODO UPDATE MY ERROR STRING
+ self.presentedAlert = .generic(title: localizeString("Alert.SymLink.Title"), message: error.legibleLocalizedDescription)
+ }
+ return nil
}
func updateAllXcodes(availableXcodes: [AvailableXcode], installedXcodes: [InstalledXcode], selectedXcodePath: String?) {
@@ -580,10 +815,10 @@ class AppState: ObservableObject {
var message: String
var id: String { title + message }
}
+}
- struct SecondFactorData {
- let option: TwoFactorOption
- let authOptions: AuthOptionsResponse
- let sessionData: AppleSessionData
+extension OperatingSystemVersion {
+ func versionString() -> String {
+ return String(majorVersion) + "." + String(minorVersion) + "." + String(patchVersion)
}
}
diff --git a/Xcodes/Backend/AvailableXcode.swift b/Xcodes/Backend/AvailableXcode.swift
index cf3f0e4..d0f877e 100644
--- a/Xcodes/Backend/AvailableXcode.swift
+++ b/Xcodes/Backend/AvailableXcode.swift
@@ -14,6 +14,9 @@ public struct AvailableXcode: Codable {
public let sdks: SDKs?
public let compilers: Compilers?
public let fileSize: Int64?
+ public var downloadPath: String {
+ return url.path
+ }
public init(
version: Version,
diff --git a/Xcodes/Backend/Collection+.swift b/Xcodes/Backend/Collection+.swift
new file mode 100644
index 0000000..5a59f33
--- /dev/null
+++ b/Xcodes/Backend/Collection+.swift
@@ -0,0 +1,17 @@
+//
+// Collection+.swift
+// Xcodes
+//
+// Created by Matt Kiazyk on 2022-04-11.
+// Copyright © 2022 Robots and Pencils. All rights reserved.
+//
+
+import Foundation
+
+public extension Collection {
+
+ /// Returns the element at the specified index iff it is within bounds, otherwise nil.
+ subscript (safe index: Index) -> Element? {
+ return indices.contains(index) ? self[index] : nil
+ }
+}
diff --git a/Xcodes/Backend/DateFormatter+.swift b/Xcodes/Backend/DateFormatter+.swift
index a9eb59e..a0073de 100644
--- a/Xcodes/Backend/DateFormatter+.swift
+++ b/Xcodes/Backend/DateFormatter+.swift
@@ -5,6 +5,8 @@ extension DateFormatter {
static let downloadsDateModified: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yy HH:mm"
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ formatter.calendar = .init(identifier: .iso8601)
return formatter
}()
diff --git a/Xcodes/Backend/Downloads.swift b/Xcodes/Backend/Downloads.swift
index 55096e4..aab9779 100644
--- a/Xcodes/Backend/Downloads.swift
+++ b/Xcodes/Backend/Downloads.swift
@@ -3,7 +3,13 @@ import Path
import Version
struct Downloads: Codable {
- let downloads: [Download]
+ let resultCode: Int
+ let resultsString: String?
+ let downloads: [Download]?
+
+ var hasError: Bool {
+ return resultCode != 0
+ }
}
// Set to Int64 as ByteCountFormatter uses it.
diff --git a/Xcodes/Backend/Entry+.swift b/Xcodes/Backend/Entry+.swift
index bdf85f7..c6e8c11 100644
--- a/Xcodes/Backend/Entry+.swift
+++ b/Xcodes/Backend/Entry+.swift
@@ -2,13 +2,12 @@ import Foundation
import Path
extension Entry {
- var isAppBundle: Bool {
+ static func isAppBundle(kind: Kind, path: Path) -> Bool {
kind == .directory &&
path.extension == "app" &&
!path.isSymlink
}
-
- var infoPlist: InfoPlist? {
+ static func infoPlist(kind: Kind, path: Path) -> InfoPlist? {
let infoPlistPath = path.join("Contents").join("Info.plist")
guard
let infoPlistData = try? Data(contentsOf: infoPlistPath.url),
@@ -17,4 +16,12 @@ extension Entry {
return infoPlist
}
+
+ var isAppBundle: Bool {
+ Entry.isAppBundle(kind: kind, path: path)
+ }
+
+ var infoPlist: InfoPlist? {
+ Entry.infoPlist(kind: kind, path: path)
+ }
}
diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift
index 6d8af24..9a43ec8 100644
--- a/Xcodes/Backend/Environment.swift
+++ b/Xcodes/Backend/Environment.swift
@@ -43,7 +43,7 @@ public struct Shell {
"--max-connection-per-server=16",
"--split=16",
"--summary-interval=1",
- "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)",
+ "--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process
"--dir=\(destination.parent.string)",
"--out=\(destination.basename())",
"--human-readable=false", // sets the output to use bytes instead of formatting
@@ -111,6 +111,12 @@ public struct Shell {
return (progress, publisher)
}
+
+ public var unxipExperiment: (URL) -> AnyPublisher = { url in
+ let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)!
+ return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
+ }
+
}
public struct Files {
@@ -158,7 +164,16 @@ public struct Files {
}
public var installedXcodes = _installedXcodes
+
+ public func installedXcode(destination: Path) -> InstalledXcode? {
+ if Entry.isAppBundle(kind: destination.isDirectory ? .directory : .file, path: destination) && Entry.infoPlist(kind: destination.isDirectory ? .directory : .file, path: destination)?.bundleID == "com.apple.dt.Xcode" {
+ return InstalledXcode.init(path: destination)
+ } else {
+ return nil
+ }
+ }
}
+
private func _installedXcodes(destination: Path) -> [InstalledXcode] {
((try? destination.ls()) ?? [])
.filter { $0.isAppBundle && $0.infoPlist?.bundleID == "com.apple.dt.Xcode" }
@@ -169,7 +184,7 @@ private func _installedXcodes(destination: Path) -> [InstalledXcode] {
public struct Network {
private static let client = AppleAPI.Client()
- public var dataTask: (URLRequest) -> AnyPublisher = {
+ public var dataTask: (URLRequest) -> AnyPublisher = {
AppleAPI.Current.network.session.dataTaskPublisher(for: $0)
.mapError { $0 as Error }
.eraseToAnyPublisher()
@@ -183,6 +198,10 @@ public struct Network {
public func downloadTask(with url: URL, to saveLocation: URL, resumingWith resumeData: Data?) -> (progress: Progress, publisher: AnyPublisher<(saveLocation: URL, response: URLResponse), Error>) {
return downloadTask(url, saveLocation, resumeData)
}
+
+ public var validateSession: () -> AnyPublisher = {
+ return client.validateSession()
+ }
}
public struct Keychain {
@@ -234,6 +253,11 @@ public struct Defaults {
public func get(forKey key: String) -> Any? {
get(key)
}
+
+ public var bool: (String) -> Bool? = { UserDefaults.standard.bool(forKey: $0) }
+ public func bool(forKey key: String) -> Bool? {
+ bool(key)
+ }
}
private let helperClient = HelperClient()
diff --git a/Xcodes/Backend/FileError.swift b/Xcodes/Backend/FileError.swift
new file mode 100644
index 0000000..c3965bb
--- /dev/null
+++ b/Xcodes/Backend/FileError.swift
@@ -0,0 +1,23 @@
+//
+// FileError.swift
+// Xcodes
+//
+// Created by Leon Wolf on 06.10.22.
+// Copyright © 2022 Robots and Pencils. All rights reserved.
+//
+
+import Foundation
+import LegibleError
+
+enum FileError: LocalizedError{
+ case fileNotFound(_ fileName: String)
+}
+
+extension FileError {
+ var errorDescription: String? {
+ switch self {
+ case .fileNotFound(let fileName):
+ return String(format: localizeString("Alert.Uninstall.Error.Message.FileNotFound"), fileName)
+ }
+ }
+}
diff --git a/Xcodes/Backend/FileManager+.swift b/Xcodes/Backend/FileManager+.swift
index 12c96e1..72fd1ee 100644
--- a/Xcodes/Backend/FileManager+.swift
+++ b/Xcodes/Backend/FileManager+.swift
@@ -11,7 +11,11 @@ extension FileManager {
@discardableResult
func trashItem(at url: URL) throws -> URL {
var resultingItemURL: NSURL!
- try trashItem(at: url, resultingItemURL: &resultingItemURL)
+ if fileExists(atPath: url.path) {
+ try trashItem(at: url, resultingItemURL: &resultingItemURL)
+ } else {
+ throw FileError.fileNotFound(url.lastPathComponent)
+ }
return resultingItemURL as URL
}
-}
\ No newline at end of file
+}
diff --git a/Xcodes/Backend/Hardware.swift b/Xcodes/Backend/Hardware.swift
new file mode 100644
index 0000000..0f7601b
--- /dev/null
+++ b/Xcodes/Backend/Hardware.swift
@@ -0,0 +1,28 @@
+import Foundation
+
+
+struct Hardware {
+
+ ///
+ /// Determines the architecture of the Mac on which we're running. Returns `arm64` for Apple Silicon
+ /// and `x86_64` for Intel-based Macs or `nil` if the system call fails.
+ static func getMachineHardwareName() -> String?
+ {
+ var sysInfo = utsname()
+ let retVal = uname(&sysInfo)
+ var finalString: String? = nil
+
+ if retVal == EXIT_SUCCESS
+ {
+ let bytes = Data(bytes: &sysInfo.machine, count: Int(_SYS_NAMELEN))
+ finalString = String(data: bytes, encoding: .utf8)
+ }
+
+ // _SYS_NAMELEN will include a billion null-terminators. Clear those out so string comparisons work as you expect.
+ return finalString?.trimmingCharacters(in: CharacterSet(charactersIn: "\0"))
+ }
+
+ static func isAppleSilicon() -> Bool {
+ return Hardware.getMachineHardwareName() == "arm64"
+ }
+}
diff --git a/Xcodes/Backend/HelperClient.swift b/Xcodes/Backend/HelperClient.swift
index e917900..284d23d 100644
--- a/Xcodes/Backend/HelperClient.swift
+++ b/Xcodes/Backend/HelperClient.swift
@@ -361,7 +361,7 @@ enum HelperClientError: LocalizedError {
var errorDescription: String? {
switch self {
case .failedToCreateRemoteObjectProxy:
- return "Unable to communicate with privileged helper."
+ return localizeString("HelperClient.error")
case let .message(message):
return message
}
diff --git a/Xcodes/Backend/InstallationStep.swift b/Xcodes/Backend/InstallationStep.swift
index f434229..ca2001d 100644
--- a/Xcodes/Backend/InstallationStep.swift
+++ b/Xcodes/Backend/InstallationStep.swift
@@ -16,17 +16,17 @@ enum InstallationStep: Equatable, CustomStringConvertible {
var message: String {
switch self {
case .downloading:
- return "Downloading"
+ return localizeString("Downloading")
case .unarchiving:
- return "Unarchiving (This can take a while)"
+ return localizeString("Unarchiving")
case .moving(let destination):
- return "Moving to \(destination)"
+ return String(format: localizeString("Moving"), destination)
case .trashingArchive:
- return "Moving archive to the Trash"
+ return localizeString("TrashingArchive")
case .checkingSecurity:
- return "Security verification"
+ return localizeString("CheckingSecurity")
case .finishing:
- return "Finishing"
+ return localizeString("Finishing")
}
}
diff --git a/Xcodes/Backend/NotificationManager.swift b/Xcodes/Backend/NotificationManager.swift
index cb37a5a..7c2a3af 100644
--- a/Xcodes/Backend/NotificationManager.swift
+++ b/Xcodes/Backend/NotificationManager.swift
@@ -22,9 +22,9 @@ public enum XcodesNotificationType: String, Identifiable, CaseIterable, CustomSt
public var description: String {
switch self {
case .newVersionAvailable:
- return "New version is available"
+ return localizeString("Notification.NewVersionAvailable")
case .finishedInstalling:
- return "Finished installing"
+ return localizeString("Notification.FinishedInstalling")
}
}
}
diff --git a/Xcodes/Backend/Path+.swift b/Xcodes/Backend/Path+.swift
index ba20bef..36e0042 100644
--- a/Xcodes/Backend/Path+.swift
+++ b/Xcodes/Backend/Path+.swift
@@ -1,6 +1,31 @@
import Path
+import Foundation
extension Path {
- static let xcodesApplicationSupport = Path.applicationSupport/"com.robotsandpencils.XcodesApp"
- static let cacheFile = xcodesApplicationSupport/"available-xcodes.json"
+ static let defaultXcodesApplicationSupport = Path.applicationSupport/"com.robotsandpencils.XcodesApp"
+ static var xcodesApplicationSupport: Path {
+ guard let savedApplicationSupport = Current.defaults.string(forKey: "localPath") else {
+ return defaultXcodesApplicationSupport
+ }
+ guard let path = Path(savedApplicationSupport) else {
+ return defaultXcodesApplicationSupport
+ }
+ return path
+ }
+
+ static var cacheFile: Path {
+ return xcodesApplicationSupport/"available-xcodes.json"
+ }
+
+ static let defaultInstallDirectory = Path.root/"Applications"
+
+ static var installDirectory: Path {
+ guard let savedInstallDirectory = Current.defaults.string(forKey: "installPath") else {
+ return defaultInstallDirectory
+ }
+ guard let path = Path(savedInstallDirectory) else {
+ return defaultInstallDirectory
+ }
+ return path
+ }
}
diff --git a/Xcodes/Backend/SelectedActionType.swift b/Xcodes/Backend/SelectedActionType.swift
new file mode 100644
index 0000000..bce3a88
--- /dev/null
+++ b/Xcodes/Backend/SelectedActionType.swift
@@ -0,0 +1,31 @@
+//
+// SelectedActionType.swift
+// Xcodes
+//
+// Created by Matt Kiazyk on 2022-07-24.
+// Copyright © 2022 Robots and Pencils. All rights reserved.
+//
+
+import Foundation
+public enum SelectedActionType: String, CaseIterable, Identifiable, CustomStringConvertible {
+ case none
+ case rename
+
+ public var id: Self { self }
+
+ public static var `default` = SelectedActionType.none
+
+ public var description: String {
+ switch self {
+ case .none: return localizeString("OnSelectDoNothing")
+ case .rename: return localizeString("OnSelectRenameXcode")
+ }
+ }
+
+ public var detailedDescription: String {
+ switch self {
+ case .none: return localizeString("OnSelectDoNothingDescription")
+ case .rename: return localizeString("OnSelectRenameXcodeDescription")
+ }
+ }
+}
diff --git a/Xcodes/Backend/URLRequest+Apple.swift b/Xcodes/Backend/URLRequest+Apple.swift
index efe9afc..99e4b6e 100644
--- a/Xcodes/Backend/URLRequest+Apple.swift
+++ b/Xcodes/Backend/URLRequest+Apple.swift
@@ -4,6 +4,7 @@ extension URL {
static let download = URL(string: "https://developer.apple.com/download")!
static let downloads = URL(string: "https://developer.apple.com/services-account/QH65B2/downloadws/listDownloads.action")!
static let downloadXcode = URL(string: "https://developer.apple.com/devcenter/download.action")!
+ static let downloadADCAuth = URL(string: "https://developerservices2.apple.com/services/download")!
}
extension URLRequest {
@@ -25,4 +26,13 @@ extension URLRequest {
request.allHTTPHeaderFields?["Accept"] = "*/*"
return request
}
+
+ static func downloadADCAuth(path: String) -> URLRequest {
+ var components = URLComponents(url: .downloadADCAuth, resolvingAgainstBaseURL: false)!
+ components.queryItems = [URLQueryItem(name: "path", value: path)]
+ var request = URLRequest(url: components.url!)
+ request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
+ request.allHTTPHeaderFields?["Accept"] = "*/*"
+ return request
+ }
}
diff --git a/Xcodes/Backend/Xcode.swift b/Xcodes/Backend/Xcode.swift
index f78ec76..603e35d 100644
--- a/Xcodes/Backend/Xcode.swift
+++ b/Xcodes/Backend/Xcode.swift
@@ -3,6 +3,7 @@ import Foundation
import Version
import struct XCModel.SDKs
import struct XCModel.Compilers
+import Path
struct Xcode: Identifiable, CustomStringConvertible {
let version: Version
@@ -57,4 +58,13 @@ struct Xcode: Identifiable, CustomStringConvertible {
return nil
}
}
+
+ var installedPath: Path? {
+ switch installState {
+ case .installed(let path):
+ return path
+ default:
+ return nil
+ }
+ }
}
diff --git a/Xcodes/Backend/XcodeCommands.swift b/Xcodes/Backend/XcodeCommands.swift
index 58ee191..76e9924 100644
--- a/Xcodes/Backend/XcodeCommands.swift
+++ b/Xcodes/Backend/XcodeCommands.swift
@@ -18,6 +18,7 @@ struct XcodeCommands: Commands {
OpenCommand()
RevealCommand()
CopyPathCommand()
+ CreateSymbolicLinkCommand()
Divider()
@@ -44,7 +45,7 @@ struct InstallButton: View {
private func install() {
guard let xcode = xcode else { return }
- appState.install(id: xcode.id)
+ appState.checkMinVersionAndInstall(id: xcode.id)
}
}
@@ -55,7 +56,7 @@ struct CancelInstallButton: View {
var body: some View {
Button(action: cancelInstall) {
Text("Cancel")
- .help("Stop installation")
+ .help(localizeString("StopInstallation"))
}
}
@@ -74,7 +75,7 @@ struct SelectButton: View {
if xcode?.selected == true {
Text("Active")
} else {
- Text("Make active")
+ Text("MakeActive")
}
}
.disabled(xcode?.selected != false)
@@ -83,7 +84,7 @@ struct SelectButton: View {
private func select() {
guard let xcode = xcode else { return }
- appState.select(id: xcode.id)
+ appState.select(xcode: xcode)
}
}
@@ -91,16 +92,34 @@ struct OpenButton: View {
@EnvironmentObject var appState: AppState
let xcode: Xcode?
+ var openInRosetta: Bool {
+ appState.showOpenInRosettaOption && Hardware.isAppleSilicon()
+ }
+
var body: some View {
- Button(action: open) {
- Text("Open")
+ if openInRosetta {
+ Menu("Open") {
+ Button(action: open) {
+ Text("Open")
+ }
+ .help("Open")
+ Button(action: open) {
+ Text("Open In Rosetta")
+ }
+ .help("Open In Rosetta")
+ }
+ } else {
+ Button(action: open) {
+ Text("Open")
+ }
+ .help("Open")
}
- .help("Open")
+
}
private func open() {
guard let xcode = xcode else { return }
- appState.open(id: xcode.id)
+ appState.open(xcode: xcode, openInRosetta: openInRosetta)
}
}
@@ -125,14 +144,14 @@ struct RevealButton: View {
var body: some View {
Button(action: reveal) {
- Text("Reveal in Finder")
+ Text("RevealInFinder")
}
- .help("Reveal in Finder")
+ .help("RevealInFinder")
}
private func reveal() {
guard let xcode = xcode else { return }
- appState.reveal(id: xcode.id)
+ appState.reveal(xcode: xcode)
}
}
@@ -142,14 +161,66 @@ struct CopyPathButton: View {
var body: some View {
Button(action: copyPath) {
- Text("Copy Path")
+ Text("CopyPath")
}
- .help("Copy path")
+ .help("CopyPath")
}
private func copyPath() {
guard let xcode = xcode else { return }
- appState.copyPath(id: xcode.id)
+ appState.copyPath(xcode: xcode)
+ }
+}
+
+struct CopyReleaseNoteButton: View {
+ @EnvironmentObject var appState: AppState
+ let xcode: Xcode?
+
+ var body: some View {
+ Button(action: copyReleaseNote) {
+ Text("CopyReleaseNoteURL")
+ }
+ .help("CopyReleaseNoteURL")
+ }
+
+ private func copyReleaseNote() {
+ guard let xcode = xcode else { return }
+ appState.copyReleaseNote(xcode: xcode)
+ }
+}
+
+
+struct CreateSymbolicLinkButton: View {
+ @EnvironmentObject var appState: AppState
+ let xcode: Xcode?
+
+ var body: some View {
+ Button(action: createSymbolicLink) {
+ Text("CreateSymLink")
+ }
+ .help("CreateSymLink")
+ }
+
+ private func createSymbolicLink() {
+ guard let xcode = xcode else { return }
+ appState.createSymbolicLink(xcode: xcode)
+ }
+}
+
+struct CreateSymbolicBetaLinkButton: View {
+ @EnvironmentObject var appState: AppState
+ let xcode: Xcode?
+
+ var body: some View {
+ Button(action: createSymbolicBetaLink) {
+ Text("CreateSymLinkBeta")
+ }
+ .help("CreateSymLinkBeta")
+ }
+
+ private func createSymbolicBetaLink() {
+ guard let xcode = xcode else { return }
+ appState.createSymbolicLink(xcode: xcode, isBeta: true)
}
}
@@ -225,3 +296,15 @@ struct UninstallCommand: View {
.disabled(selectedXcode.unwrapped?.installState.installed != true)
}
}
+
+struct CreateSymbolicLinkCommand: View {
+ @EnvironmentObject var appState: AppState
+ @FocusedValue(\.selectedXcode) private var selectedXcode: SelectedXcode?
+
+ var body: some View {
+ CreateSymbolicLinkButton(xcode: selectedXcode.unwrapped)
+ .keyboardShortcut("s", modifiers: [.command, .option])
+ .disabled(selectedXcode.unwrapped?.installState.installed != true)
+ }
+}
+
diff --git a/Xcodes/Frontend/About/AboutView.swift b/Xcodes/Frontend/About/AboutView.swift
index a34a817..4967e2e 100644
--- a/Xcodes/Frontend/About/AboutView.swift
+++ b/Xcodes/Frontend/About/AboutView.swift
@@ -12,16 +12,13 @@ struct AboutView: View {
Text(Bundle.main.bundleName!)
.font(.largeTitle)
- Text("Version \(Bundle.main.shortVersion!) (\(Bundle.main.version!))")
-
- Color.clear
- .frame(width: 300, height: 16)
+ Text(String(format: localizeString("VersionWithBuild"), Bundle.main.shortVersion!, Bundle.main.version!))
HStack(spacing: 32) {
Button(action: {
openURL(URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!)
}) {
- Label("GitHub Repo", systemImage: "link")
+ Label("GithubRepo", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
@@ -30,6 +27,24 @@ struct AboutView: View {
}
.buttonStyle(LinkButtonStyle())
}
+ Color.clear
+ .frame(width: 300, height: 0)
+ Label("UnxipExperiment", systemImage: "lightbulb")
+ HStack(spacing: 32) {
+ Button(action: {
+ openURL(URL(string: "https://github.com/saagarjha/unxip/")!)
+ }) {
+ Label("GithubRepo", systemImage: "link")
+ }
+ .buttonStyle(LinkButtonStyle())
+
+ Button(action: {
+ openURL(URL(string: "https://github.com/saagarjha/unxip/blob/main/LICENSE")!)
+ }) {
+ Label("License", systemImage: "link")
+ }
+ .buttonStyle(LinkButtonStyle())
+ }
Text(Bundle.main.humanReadableCopyright!)
.font(.footnote)
diff --git a/Xcodes/Frontend/Common/ObservingProgressIndicator.swift b/Xcodes/Frontend/Common/ObservingProgressIndicator.swift
index 88a11ef..a677486 100644
--- a/Xcodes/Frontend/Common/ObservingProgressIndicator.swift
+++ b/Xcodes/Frontend/Common/ObservingProgressIndicator.swift
@@ -46,11 +46,11 @@ public struct ObservingProgressIndicator: View {
isIndeterminate: progress.progress.isIndeterminate,
style: style
)
- .help("Downloading: \(Int((progress.progress.fractionCompleted * 100)))% complete")
+ .help(String(format: localizeString("DownloadingPercentDescription"), Int((progress.progress.fractionCompleted * 100))))
if showsAdditionalDescription, progress.progress.xcodesLocalizedDescription.isEmpty == false {
Text(progress.progress.xcodesLocalizedDescription)
- .font(.subheadline)
+ .font(.subheadline.monospacedDigit())
.foregroundColor(.secondary)
}
}
diff --git a/Xcodes/Frontend/Common/XcodesAlert.swift b/Xcodes/Frontend/Common/XcodesAlert.swift
index c57d089..c928501 100644
--- a/Xcodes/Frontend/Common/XcodesAlert.swift
+++ b/Xcodes/Frontend/Common/XcodesAlert.swift
@@ -4,12 +4,14 @@ enum XcodesAlert: Identifiable {
case cancelInstall(xcode: Xcode)
case privilegedHelper
case generic(title: String, message: String)
+ case checkMinSupportedVersion(xcode: AvailableXcode, macOS: String)
var id: Int {
switch self {
case .cancelInstall: return 1
case .privilegedHelper: return 2
case .generic: return 3
+ case .checkMinSupportedVersion: return 4
}
}
}
diff --git a/Xcodes/Frontend/Common/XcodesSheet.swift b/Xcodes/Frontend/Common/XcodesSheet.swift
index 0a39add..a0270b5 100644
--- a/Xcodes/Frontend/Common/XcodesSheet.swift
+++ b/Xcodes/Frontend/Common/XcodesSheet.swift
@@ -1,8 +1,39 @@
import Foundation
+import AppleAPI
enum XcodesSheet: Identifiable {
case signIn
- case twoFactor
+ case twoFactor(SecondFactorData)
- var id: Int { hashValue }
+ var id: Int { Kind(self).hashValue }
+
+ struct SecondFactorData {
+ let option: TwoFactorOption
+ let authOptions: AuthOptionsResponse
+ let sessionData: AppleSessionData
+ }
+}
+
+extension XcodesSheet {
+ private enum Kind: Hashable {
+ case signIn, twoFactor(TwoFactorOption)
+
+ enum TwoFactorOption {
+ case smsSent
+ case codeSent
+ case smsPendingChoice
+ }
+
+ init(_ sheet: XcodesSheet) {
+ switch sheet {
+ case .signIn: self = .signIn
+ case .twoFactor(let data):
+ switch data.option {
+ case .smsSent: self = .twoFactor(.smsSent)
+ case .smsPendingChoice: self = .twoFactor(.smsPendingChoice)
+ case .codeSent: self = .twoFactor(.codeSent)
+ }
+ }
+ }
+ }
}
diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift
index c00a85c..88195da 100644
--- a/Xcodes/Frontend/InfoPane/InfoPane.swift
+++ b/Xcodes/Frontend/InfoPane/InfoPane.swift
@@ -31,11 +31,11 @@ struct InfoPane: View {
case let .installed(path):
HStack {
Text(path.string)
- Button(action: { appState.reveal(id: xcode.id) }) {
+ Button(action: { appState.reveal(xcode: xcode) }) {
Image(systemName: "arrow.right.circle.fill")
}
.buttonStyle(PlainButtonStyle())
- .help("Reveal in Finder")
+ .help("RevealInFinder")
}
HStack {
@@ -90,11 +90,11 @@ struct InfoPane: View {
if !xcode.identicalBuilds.isEmpty {
VStack(alignment: .leading) {
HStack {
- Text("Identical Builds")
+ Text("IdenticalBuilds")
Image(systemName: "square.fill.on.square.fill")
.foregroundColor(.secondary)
.accessibility(hidden: true)
- .help("Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together.")
+ .help("IdenticalBuilds.help")
}
.font(.headline)
@@ -105,9 +105,9 @@ struct InfoPane: View {
}
.frame(maxWidth: .infinity, alignment: .leading)
.accessibilityElement()
- .accessibility(label: Text("Identical Builds"))
+ .accessibility(label: Text("IdenticalBuilds"))
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
- .accessibility(hint: Text("Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together."))
+ .accessibility(hint: Text("IdenticalBuilds.help"))
} else {
EmptyView()
}
@@ -117,10 +117,10 @@ struct InfoPane: View {
private func releaseDate(for xcode: Xcode) -> some View {
if let releaseDate = xcode.releaseDate {
VStack(alignment: .leading) {
- Text("Release Date")
+ Text("ReleaseDate")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
- Text(DateFormatter.downloadsReleaseDate.string(from: releaseDate))
+ Text("\(releaseDate, style: .date)")
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -133,15 +133,23 @@ struct InfoPane: View {
private func releaseNotes(for xcode: Xcode) -> some View {
if let releaseNotesURL = xcode.releaseNotesURL {
Button(action: { openURL(releaseNotesURL) }) {
- Label("Release Notes", systemImage: "link")
+ Label("ReleaseNotes", systemImage: "link")
}
.buttonStyle(LinkButtonStyle())
+ .contextMenu(menuItems: {
+ releaseNotesMenu(for: xcode)
+ })
.frame(maxWidth: .infinity, alignment: .leading)
- .help("View Release Notes")
+ .help("ReleaseNotes.help")
} else {
EmptyView()
}
}
+
+ @ViewBuilder
+ private func releaseNotesMenu(for xcode: Xcode) -> some View {
+ CopyReleaseNoteButton(xcode: xcode)
+ }
@ViewBuilder
private func compatibility(for xcode: Xcode) -> some View {
@@ -150,7 +158,7 @@ struct InfoPane: View {
Text("Compatibility")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
- Text("Requires macOS \(requiredMacOSVersion) or later")
+ Text(String(format: localizeString("MacOSRequirement"), requiredMacOSVersion))
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
}
@@ -217,7 +225,7 @@ struct InfoPane: View {
// if we've downloaded it no need to show the download size
if let downloadFileSize = xcode.downloadFileSizeString, case .notInstalled = xcode.installState {
VStack(alignment: .leading) {
- Text("Download Size")
+ Text("DownloadSize")
.font(.headline)
.frame(maxWidth: .infinity, alignment: .leading)
Text("\(downloadFileSize)")
@@ -231,7 +239,7 @@ struct InfoPane: View {
@ViewBuilder
private var empty: some View {
- Text("No Xcode Selected")
+ Text("NoXcodeSelected")
.font(.title)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
diff --git a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift
index a1992d5..863f204 100644
--- a/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift
+++ b/Xcodes/Frontend/InfoPane/InstallationStepDetailView.swift
@@ -5,7 +5,7 @@ struct InstallationStepDetailView: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
- Text("Step \(installationStep.stepNumber) of \(installationStep.stepCount): \(installationStep.message)")
+ Text(String(format: localizeString("InstallationStepDescription"), installationStep.stepNumber, installationStep.stepCount, installationStep.message))
switch installationStep {
case let .downloading(progress):
diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift
index fe87402..fcde005 100644
--- a/Xcodes/Frontend/MainWindow.swift
+++ b/Xcodes/Frontend/MainWindow.swift
@@ -12,16 +12,17 @@ struct MainWindow: View {
// FB8979533 SceneStorage doesn't restore value after app is quit by user
@AppStorage("isShowingInfoPane") private var isShowingInfoPane = false
@AppStorage("xcodeListCategory") private var category: XcodeListCategory = .all
+ @AppStorage("isInstalledOnly") private var isInstalledOnly = false
var body: some View {
HSplitView {
- XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category)
+ XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly)
.frame(minWidth: 300)
.layoutPriority(1)
.alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in
- Alert(title: Text("Uninstall Xcode \(xcode.description)?"),
- message: Text("It will be moved to the Trash, but won't be emptied."),
- primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(id: xcode.id) }),
+ Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)),
+ message: Text("Alert.Uninstall.Message"),
+ primaryButton: .destructive(Text("Uninstall"), action: { self.appState.uninstall(xcode: xcode) }),
secondaryButton: .cancel(Text("Cancel")))
}
@@ -32,9 +33,12 @@ struct MainWindow: View {
}
.mainToolbar(
category: $category,
+ isInstalledOnly: $isInstalledOnly,
isShowingInfoPane: $isShowingInfoPane,
searchText: $searchText
)
+ .bottomStatusBar()
+ .padding([.top], 0)
.navigationSubtitle(subtitleText)
.frame(minWidth: 600, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
.emittingError($appState.error, recoveryHandler: { _ in })
@@ -43,8 +47,8 @@ struct MainWindow: View {
case .signIn:
signInView()
.environmentObject(appState)
- case .twoFactor:
- secondFactorView(appState.secondFactorData!)
+ case .twoFactor(let secondFactorData):
+ secondFactorView(secondFactorData)
.environmentObject(appState)
}
}
@@ -58,21 +62,21 @@ struct MainWindow: View {
private var subtitleText: Text {
if let lastUpdated = lastUpdated.map(Date.init(timeIntervalSince1970:)) {
- return Text("Updated at \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
+ return Text("\(localizeString("UpdatedAt")) \(lastUpdated, style: .date) \(lastUpdated, style: .time)")
} else {
return Text("")
}
}
@ViewBuilder
- private func secondFactorView(_ secondFactorData: AppState.SecondFactorData) -> some View {
+ private func secondFactorView(_ secondFactorData: XcodesSheet.SecondFactorData) -> some View {
switch secondFactorData.option {
case .codeSent:
- SignIn2FAView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
+ SignIn2FAView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsSent(let trustedPhoneNumber):
- SignInSMSView(isPresented: $appState.secondFactorData.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
+ SignInSMSView(isPresented: $appState.presentedSheet.isNotNil, trustedPhoneNumber: trustedPhoneNumber, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
case .smsPendingChoice:
- SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
+ SignInPhoneListView(isPresented: $appState.presentedSheet.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData)
}
}
@@ -99,10 +103,10 @@ struct MainWindow: View {
switch alertType {
case let .cancelInstall(xcode):
return Alert(
- title: Text("Are you sure you want to stop the installation of Xcode \(xcode.description)?"),
- message: Text("Any progress will be discarded."),
+ title: Text(String(format: localizeString("Alert.CancelInstall.Title"), xcode.description)),
+ message: Text("Alert.CancelInstall.Message"),
primaryButton: .destructive(
- Text("Stop Installation"),
+ Text("Alert.CancelInstall.PrimaryButton"),
action: {
self.appState.cancelInstall(id: xcode.id)
}
@@ -111,8 +115,8 @@ struct MainWindow: View {
)
case .privilegedHelper:
return Alert(
- title: Text("Privileged Helper"),
- message: Text("Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it."),
+ title: Text("Alert.PrivilegedHelper.Title"),
+ message: Text("Alert.PrivilegedHelper.Message"),
primaryButton: .default(Text("Install"), action: {
// The isPreparingUserForActionRequiringHelper closure is set to nil by the alert's binding when its dismissed.
// We need to capture it to be invoked after that happens.
@@ -145,10 +149,22 @@ struct MainWindow: View {
title: Text(title),
message: Text(message),
dismissButton: .default(
- Text("Ok"),
+ Text("OK"),
action: { appState.presentedAlert = nil }
)
)
+ case let .checkMinSupportedVersion(xcode, deviceVersion):
+ return Alert(
+ title: Text("Alert.MinSupported.Title"),
+ message: Text(String(format: localizeString("Alert.MinSupported.Message"), xcode.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)),
+ primaryButton: .default(
+ Text("Install"),
+ action: {
+ self.appState.install(id: xcode.version)
+ }
+ ),
+ secondaryButton: .cancel(Text("Cancel"))
+ )
}
}
}
diff --git a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift
index 9606456..21d6b8c 100644
--- a/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift
+++ b/Xcodes/Frontend/Preferences/AdvancedPreferencePane.swift
@@ -1,63 +1,140 @@
import AppleAPI
import SwiftUI
+import Path
struct AdvancedPreferencePane: View {
@EnvironmentObject var appState: AppState
- @AppStorage("dataSource") var dataSource: DataSource = .xcodeReleases
- @AppStorage("downloader") var downloader: Downloader = .aria2
var body: some View {
VStack(alignment: .leading, spacing: 20) {
- GroupBox(label: Text("Data Source")) {
+
+ GroupBox(label: Text("InstallDirectory")) {
VStack(alignment: .leading) {
- Picker("Data Source", selection: $dataSource) {
- ForEach(DataSource.allCases) { dataSource in
- Text(dataSource.description)
- .tag(dataSource)
+ HStack(alignment: .top, spacing: 5) {
+ Text(appState.installPath).font(.footnote)
+ .fixedSize(horizontal: false, vertical: true)
+ .lineLimit(2)
+ Button(action: { appState.reveal(path: appState.installPath) }) {
+ Image(systemName: "arrow.right.circle.fill")
+ }
+ .buttonStyle(PlainButtonStyle())
+ .help("RevealInFinder")
+ .fixedSize()
+ }
+ Button("Change") {
+ let panel = NSOpenPanel()
+ panel.allowsMultipleSelection = false
+ panel.canChooseDirectories = true
+ panel.canChooseFiles = false
+ panel.canCreateDirectories = true
+ panel.allowedContentTypes = [.folder]
+ panel.directoryURL = URL(fileURLWithPath: appState.installPath)
+
+ if panel.runModal() == .OK {
+
+ guard let pathURL = panel.url, let path = Path(url: pathURL) else { return }
+ self.appState.installPath = path.string
}
}
- .labelsHidden()
-
- AttributedText(dataSourceFootnote)
+ Text("InstallPathDescription")
+ .font(.footnote)
+ .fixedSize(horizontal: false, vertical: true)
}
-
}
.groupBoxStyle(PreferencesGroupBoxStyle())
- GroupBox(label: Text("Downloader")) {
+ GroupBox(label: Text("LocalCachePath")) {
VStack(alignment: .leading) {
- Picker("Downloader", selection: $downloader) {
- ForEach(Downloader.allCases) { downloader in
- Text(downloader.description)
- .tag(downloader)
+ HStack(alignment: .top, spacing: 5) {
+ Text(appState.localPath).font(.footnote)
+ .fixedSize(horizontal: false, vertical: true)
+ .lineLimit(2)
+ Button(action: { appState.reveal(path: appState.localPath) }) {
+ Image(systemName: "arrow.right.circle.fill")
+ }
+ .buttonStyle(PlainButtonStyle())
+ .help("RevealInFinder")
+ .fixedSize()
+ }
+ Button("Change") {
+ let panel = NSOpenPanel()
+ panel.allowsMultipleSelection = false
+ panel.canChooseDirectories = true
+ panel.canChooseFiles = false
+ panel.canCreateDirectories = true
+ panel.allowedContentTypes = [.folder]
+ panel.directoryURL = URL(fileURLWithPath: appState.localPath)
+
+ if panel.runModal() == .OK {
+
+ guard let pathURL = panel.url, let path = Path(url: pathURL) else { return }
+ self.appState.localPath = path.string
}
}
- .labelsHidden()
-
- AttributedText(downloaderFootnote)
+ Text("LocalCachePathDescription")
+ .font(.footnote)
+ .fixedSize(horizontal: false, vertical: true)
}
-
}
.groupBoxStyle(PreferencesGroupBoxStyle())
- GroupBox(label: Text("Privileged Helper")) {
+ GroupBox(label: Text("Active/Select")) {
+ VStack(alignment: .leading) {
+ Picker("OnSelect", selection: $appState.onSelectActionType) {
+
+ Text(SelectedActionType.none.description)
+ .tag(SelectedActionType.none)
+ Text(SelectedActionType.rename.description)
+ .tag(SelectedActionType.rename)
+ }
+ .labelsHidden()
+ .pickerStyle(.inline)
+
+ Text(appState.onSelectActionType.detailedDescription)
+ .font(.footnote)
+ .fixedSize(horizontal: false, vertical: true)
+ Spacer()
+ .frame(height: 20)
+
+ Toggle("AutomaticallyCreateSymbolicLink", isOn: $appState.createSymLinkOnSelect)
+ .disabled(appState.createSymLinkOnSelectDisabled)
+ Text("AutomaticallyCreateSymbolicLinkDescription")
+ .font(.footnote)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ .groupBoxStyle(PreferencesGroupBoxStyle())
+
+ if Hardware.isAppleSilicon() {
+ GroupBox(label: Text("Apple Silicon")) {
+ Toggle("ShowOpenInRosetta", isOn: $appState.showOpenInRosettaOption)
+ .disabled(appState.createSymLinkOnSelectDisabled)
+ Text("ShowOpenInRosettaDescription")
+ .font(.footnote)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ .groupBoxStyle(PreferencesGroupBoxStyle())
+ }
+
+ GroupBox(label: Text("PrivilegedHelper")) {
VStack(alignment: .leading, spacing: 8) {
switch appState.helperInstallState {
case .unknown:
ProgressView()
.scaleEffect(0.5, anchor: .center)
case .installed:
- Text("Helper is installed")
+ Text("HelperInstalled")
case .notInstalled:
HStack {
- Text("Helper is not installed")
- Button("Install helper") {
+ Text("HelperNotInstalled")
+ Button("InstallHelper") {
appState.installHelperIfNecessary()
}
}
}
- Text("Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it.")
+ Text("PrivilegedHelperDescription")
.font(.footnote)
.fixedSize(horizontal: false, vertical: true)
@@ -66,41 +143,6 @@ struct AdvancedPreferencePane: View {
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
- .frame(width: 400)
- }
-
- private var dataSourceFootnote: NSAttributedString {
- let string = """
- The Apple data source scrapes the Apple Developer website. It will always show the latest releases that are available, but is more fragile.
-
- Xcode Releases is an unofficial list of Xcode releases. It's provided as well-formed data, contains extra information that is not readily available from Apple, and is less likely to break if Apple redesigns their developer website.
- """
- let attributedString = NSMutableAttributedString(
- string: string,
- attributes: [
- .font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
- .foregroundColor: NSColor.labelColor
- ]
- )
- attributedString.addAttribute(.link, value: URL(string: "https://xcodereleases.com")!, range: NSRange(string.range(of: "Xcode Releases")!, in: string))
- return attributedString
- }
-
- private var downloaderFootnote: NSAttributedString {
- let string = """
- aria2 uses up to 16 connections to download Xcode 3-5x faster than URLSession. It's bundled as an executable along with its source code within Xcodes to comply with its GPLv2 license.
-
- URLSession is the default Apple API for making URL requests.
- """
- let attributedString = NSMutableAttributedString(
- string: string,
- attributes: [
- .font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
- .foregroundColor: NSColor.labelColor
- ]
- )
- attributedString.addAttribute(.link, value: URL(string: "https://github.com/aria2/aria2")!, range: NSRange(string.range(of: "aria2")!, in: string))
- return attributedString
}
}
@@ -109,7 +151,9 @@ struct AdvancedPreferencePane_Previews: PreviewProvider {
Group {
AdvancedPreferencePane()
.environmentObject(AppState())
+ .frame(maxWidth: 500)
}
+ .frame(width: 500, height: 700, alignment: .center)
}
}
diff --git a/Xcodes/Frontend/Preferences/DownloadPreferencePane.swift b/Xcodes/Frontend/Preferences/DownloadPreferencePane.swift
new file mode 100644
index 0000000..e453dc2
--- /dev/null
+++ b/Xcodes/Frontend/Preferences/DownloadPreferencePane.swift
@@ -0,0 +1,82 @@
+import AppleAPI
+import SwiftUI
+
+struct DownloadPreferencePane: View {
+ @EnvironmentObject var appState: AppState
+
+ @AppStorage("dataSource") var dataSource: DataSource = .xcodeReleases
+ @AppStorage("downloader") var downloader: Downloader = .aria2
+
+ var body: some View {
+ VStack(alignment: .leading) {
+ GroupBox(label: Text("DataSource")) {
+ VStack(alignment: .leading) {
+ Picker("DataSource", selection: $dataSource) {
+ ForEach(DataSource.allCases) { dataSource in
+ Text(dataSource.description)
+ .tag(dataSource)
+ }
+ }
+ .labelsHidden()
+
+ AttributedText(dataSourceFootnote)
+ }
+
+ }
+ .groupBoxStyle(PreferencesGroupBoxStyle())
+
+ GroupBox(label: Text("Downloader")) {
+ VStack(alignment: .leading) {
+ Picker("Downloader", selection: $downloader) {
+ ForEach(Downloader.allCases) { downloader in
+ Text(downloader.description)
+ .tag(downloader)
+ }
+ }
+ .labelsHidden()
+
+ AttributedText(downloaderFootnote)
+ }
+
+ }
+ .groupBoxStyle(PreferencesGroupBoxStyle())
+
+ }
+ }
+
+ private var dataSourceFootnote: NSAttributedString {
+ let string = localizeString("DataSourceDescription")
+ let attributedString = NSMutableAttributedString(
+ string: string,
+ attributes: [
+ .font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
+ .foregroundColor: NSColor.labelColor
+ ]
+ )
+ attributedString.addAttribute(.link, value: URL(string: "https://xcodereleases.com")!, range: NSRange(string.range(of: "Xcode Releases")!, in: string))
+ return attributedString
+ }
+
+ private var downloaderFootnote: NSAttributedString {
+ let string = localizeString("DownloaderDescription")
+ let attributedString = NSMutableAttributedString(
+ string: string,
+ attributes: [
+ .font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
+ .foregroundColor: NSColor.labelColor
+ ]
+ )
+ attributedString.addAttribute(.link, value: URL(string: "https://github.com/aria2/aria2")!, range: NSRange(string.range(of: "aria2")!, in: string))
+ return attributedString
+ }
+}
+
+struct DownloadPreferencePane_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ GeneralPreferencePane()
+ .environmentObject(AppState())
+ .frame(maxWidth: 500)
+ }
+ }
+}
diff --git a/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift b/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift
new file mode 100644
index 0000000..3696552
--- /dev/null
+++ b/Xcodes/Frontend/Preferences/ExperiementsPreferencePane.swift
@@ -0,0 +1,49 @@
+import AppleAPI
+import SwiftUI
+import Path
+
+struct ExperimentsPreferencePane: View {
+ @EnvironmentObject var appState: AppState
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 20) {
+ GroupBox(label: Text("FasterUnxip")) {
+ VStack(alignment: .leading) {
+ Toggle(
+ "UseUnxipExperiment",
+ isOn: $appState.unxipExperiment
+ )
+ AttributedText(unxipFootnote)
+ }
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ .groupBoxStyle(PreferencesGroupBoxStyle())
+
+ Divider()
+ }
+ }
+
+ private var unxipFootnote: NSAttributedString {
+ let string = localizeString("FasterUnxipDescription")
+ let attributedString = NSMutableAttributedString(
+ string: string,
+ attributes: [
+ .font: NSFont.preferredFont(forTextStyle: .footnote, options: [:]),
+ .foregroundColor: NSColor.labelColor
+ ]
+ )
+ attributedString.addAttribute(.link, value: URL(string: "https://twitter.com/_saagarjha")!, range: NSRange(string.range(of: "@_saagarjha")!, in: string))
+ attributedString.addAttribute(.link, value: URL(string: "https://github.com/saagarjha/unxip")!, range: NSRange(string.range(of: "https://github.com/saagarjha/unxip")!, in: string))
+ return attributedString
+ }
+}
+
+struct ExperimentsPreferencePane_Previews: PreviewProvider {
+ static var previews: some View {
+ Group {
+ ExperimentsPreferencePane()
+ .environmentObject(AppState())
+ .frame(maxWidth: 500)
+ }
+ }
+}
diff --git a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift
index 35d5eaf..4735c24 100644
--- a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift
+++ b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift
@@ -6,11 +6,11 @@ struct GeneralPreferencePane: View {
var body: some View {
VStack(alignment: .leading) {
- GroupBox(label: Text("Apple ID")) {
+ GroupBox(label: Text("AppleID")) {
if appState.authenticationState == .authenticated {
SignedInView()
} else {
- Button("Sign In", action: { self.appState.presentedSheet = .signIn })
+ Button("SignIn", action: { self.appState.presentedSheet = .signIn })
}
}
.groupBoxStyle(PreferencesGroupBoxStyle())
@@ -21,7 +21,6 @@ struct GeneralPreferencePane: View {
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
- .frame(width: 400)
}
}
@@ -30,6 +29,7 @@ struct GeneralPreferencePane_Previews: PreviewProvider {
Group {
GeneralPreferencePane()
.environmentObject(AppState())
+ .frame(maxWidth: 500)
}
}
}
diff --git a/Xcodes/Frontend/Preferences/NotificationsView.swift b/Xcodes/Frontend/Preferences/NotificationsView.swift
index 9b7297b..b878a06 100644
--- a/Xcodes/Frontend/Preferences/NotificationsView.swift
+++ b/Xcodes/Frontend/Preferences/NotificationsView.swift
@@ -8,17 +8,17 @@ struct NotificationsView: View {
switch Current.notificationManager.notificationStatus {
case .shownAndAccepted:
- Text("Access Granted. You will receive notifications from Xcodes.")
+ Text("AccessGranted")
.fixedSize(horizontal: false, vertical: true)
case .shownAndDenied:
- Text("⚠️ Access Denied ⚠️\n\nPlease open your Notification Settings and select Xcodes if you wish to allow access.")
+ Text("AccessDenied")
.fixedSize(horizontal: false, vertical: true)
- Button("Notification Settings", action: {
+ Button("NotificationSettings", action: {
NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.notifications")!)
})
default:
- Button("Enable Notifications", action: {
+ Button("EnableNotifications", action: {
Current.notificationManager.requestAccess()
})
}
diff --git a/Xcodes/Frontend/Preferences/PreferencesView.swift b/Xcodes/Frontend/Preferences/PreferencesView.swift
index 8e896aa..83ab775 100644
--- a/Xcodes/Frontend/Preferences/PreferencesView.swift
+++ b/Xcodes/Frontend/Preferences/PreferencesView.swift
@@ -2,9 +2,10 @@ import SwiftUI
struct PreferencesView: View {
private enum Tabs: Hashable {
- case general, updates, advanced
+ case general, updates, advanced, experiment
}
@EnvironmentObject var appState: AppState
+ @EnvironmentObject var updater: ObservableUpdater
var body: some View {
TabView {
@@ -15,17 +16,29 @@ struct PreferencesView: View {
}
.tag(Tabs.general)
UpdatesPreferencePane()
+ .environmentObject(updater)
.tabItem {
Label("Updates", systemImage: "arrow.triangle.2.circlepath.circle")
}
.tag(Tabs.updates)
+ DownloadPreferencePane()
+ .environmentObject(appState)
+ .tabItem {
+ Label("Downloads", systemImage: "icloud.and.arrow.down")
+ }
AdvancedPreferencePane()
.environmentObject(appState)
.tabItem {
Label("Advanced", systemImage: "gearshape.2")
}
.tag(Tabs.advanced)
+ ExperimentsPreferencePane()
+ .tabItem {
+ Label("Experiments", systemImage: "lightbulb")
+ }
+ .tag(Tabs.experiment)
}
.padding(20)
+ .frame(width: 500)
}
}
diff --git a/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift b/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift
index 69de839..349d9be 100644
--- a/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift
+++ b/Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift
@@ -3,7 +3,7 @@ import Sparkle
import SwiftUI
struct UpdatesPreferencePane: View {
- @StateObject var updater = ObservableUpdater()
+ @EnvironmentObject var updater: ObservableUpdater
@AppStorage("autoInstallation") var autoInstallationType: AutoInstallationType = .none
@@ -12,12 +12,12 @@ struct UpdatesPreferencePane: View {
GroupBox(label: Text("Versions")) {
VStack(alignment: .leading) {
Toggle(
- "Automatically install new versions of Xcode",
+ "AutomaticInstallNewVersion",
isOn: $autoInstallationType.isAutoInstalling
)
Toggle(
- "Include prerelease/beta versions",
+ "IncludePreRelease",
isOn: $autoInstallationType.isAutoInstallingBeta
)
}
@@ -27,37 +27,37 @@ struct UpdatesPreferencePane: View {
Divider()
- GroupBox(label: Text("Xcodes.app Updates")) {
+ GroupBox(label: Text("AppUpdates")) {
VStack(alignment: .leading) {
Toggle(
- "Automatically check for app updates",
+ "CheckForAppUpdates",
isOn: $updater.automaticallyChecksForUpdates
)
+ .fixedSize(horizontal: true, vertical: false)
Toggle(
- "Include prerelease app versions",
+ "IncludePreRelease",
isOn: $updater.includePrereleaseVersions
)
- Button("Check Now") {
- SUUpdater.shared()?.checkForUpdates(nil)
+ Button("CheckNow") {
+ updater.checkForUpdates()
}
- Text("Last checked: \(lastUpdatedString)")
+ Text(String(format: localizeString("LastChecked"), lastUpdatedString))
.font(.footnote)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.groupBoxStyle(PreferencesGroupBoxStyle())
}
- .frame(width: 400)
}
private var lastUpdatedString: String {
if let lastUpdatedDate = updater.lastUpdateCheckDate {
return Self.formatter.string(from: lastUpdatedDate)
} else {
- return "Never"
+ return localizeString("Never")
}
}
@@ -68,9 +68,11 @@ struct UpdatesPreferencePane: View {
}
class ObservableUpdater: ObservableObject {
+ private let updater: SPUUpdater
+
@Published var automaticallyChecksForUpdates = false {
didSet {
- SUUpdater.shared()?.automaticallyChecksForUpdates = automaticallyChecksForUpdates
+ updater.automaticallyChecksForUpdates = automaticallyChecksForUpdates
}
}
private var automaticallyChecksForUpdatesObservation: NSKeyValueObservation?
@@ -81,15 +83,17 @@ class ObservableUpdater: ObservableObject {
UserDefaults.standard.setValue(includePrereleaseVersions, forKey: "includePrereleaseVersions")
if includePrereleaseVersions {
- SUUpdater.shared()?.feedURL = .prereleaseAppcast
+ updater.setFeedURL(.prereleaseAppcast)
} else {
- SUUpdater.shared()?.feedURL = .appcast
+ updater.setFeedURL(.appcast)
}
}
}
init() {
- automaticallyChecksForUpdatesObservation = SUUpdater.shared()?.observe(
+ updater = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil).updater
+
+ automaticallyChecksForUpdatesObservation = updater.observe(
\.automaticallyChecksForUpdates,
options: [.initial, .new, .old],
changeHandler: { [unowned self] updater, change in
@@ -97,7 +101,7 @@ class ObservableUpdater: ObservableObject {
self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
}
)
- lastUpdateCheckDateObservation = SUUpdater.shared()?.observe(
+ lastUpdateCheckDateObservation = updater.observe(
\.lastUpdateCheckDate,
options: [.initial, .new, .old],
changeHandler: { [unowned self] updater, change in
@@ -106,11 +110,15 @@ class ObservableUpdater: ObservableObject {
)
includePrereleaseVersions = UserDefaults.standard.bool(forKey: "includePrereleaseVersions")
}
+
+ func checkForUpdates() {
+ updater.checkForUpdates()
+ }
}
extension URL {
- static let appcast = URL(string: "https://robotsandpencils.github.io/XcodesApp/appcast.xml")!
- static let prereleaseAppcast = URL(string: "https://robotsandpencils.github.io/XcodesApp/appcast_pre.xml")!
+ static let appcast = URL(string: "https://www.xcodes.app/appcast.xml")!
+ static let prereleaseAppcast = URL(string: "https://www.xcodes.app/appcast_pre.xml")!
}
struct UpdatesPreferencePane_Previews: PreviewProvider {
@@ -118,6 +126,7 @@ struct UpdatesPreferencePane_Previews: PreviewProvider {
Group {
UpdatesPreferencePane()
.environmentObject(AppState())
+ .frame(maxWidth: 500)
}
}
}
diff --git a/Xcodes/Frontend/SignIn/SignIn2FAView.swift b/Xcodes/Frontend/SignIn/SignIn2FAView.swift
index 37f4830..2e336e4 100644
--- a/Xcodes/Frontend/SignIn/SignIn2FAView.swift
+++ b/Xcodes/Frontend/SignIn/SignIn2FAView.swift
@@ -10,7 +10,8 @@ struct SignIn2FAView: View {
var body: some View {
VStack(alignment: .leading) {
- Text("Enter the \(authOptions.securityCode.length) digit code from one of your trusted devices:")
+ Text(String(format: localizeString("DigitCodeDescription"), authOptions.securityCode.length))
+ .fixedSize(horizontal: true, vertical: false)
HStack {
Spacer()
@@ -22,7 +23,7 @@ struct SignIn2FAView: View {
HStack {
Button("Cancel", action: { isPresented = false })
.keyboardShortcut(.cancelAction)
- Button("Send SMS", action: { appState.choosePhoneNumberForSMS(authOptions: authOptions, sessionData: sessionData) })
+ Button("SendSMS", action: { appState.choosePhoneNumberForSMS(authOptions: authOptions, sessionData: sessionData) })
Spacer()
ProgressButton(isInProgress: appState.isProcessingAuthRequest,
action: { appState.submitSecurityCode(.device(code: code), sessionData: sessionData) }) {
diff --git a/Xcodes/Frontend/SignIn/SignInCredentialsView.swift b/Xcodes/Frontend/SignIn/SignInCredentialsView.swift
index 593f735..6d07065 100644
--- a/Xcodes/Frontend/SignIn/SignInCredentialsView.swift
+++ b/Xcodes/Frontend/SignIn/SignInCredentialsView.swift
@@ -7,16 +7,16 @@ struct SignInCredentialsView: View {
var body: some View {
VStack(alignment: .leading) {
- Text("Sign in with your Apple ID.")
+ Text("SignInWithApple")
.bold()
.padding(.vertical)
HStack {
- Text("Apple ID:")
+ Text("AppleID")
.frame(minWidth: 100, alignment: .trailing)
TextField("example@icloud.com", text: $username)
}
HStack {
- Text("Password:")
+ Text("Password")
.frame(minWidth: 100, alignment: .trailing)
SecureField("Required", text: $password)
}
diff --git a/Xcodes/Frontend/SignIn/SignInPhoneListView.swift b/Xcodes/Frontend/SignIn/SignInPhoneListView.swift
index e2dae0d..1efea67 100644
--- a/Xcodes/Frontend/SignIn/SignInPhoneListView.swift
+++ b/Xcodes/Frontend/SignIn/SignInPhoneListView.swift
@@ -11,14 +11,14 @@ struct SignInPhoneListView: View {
var body: some View {
VStack(alignment: .leading) {
if let phoneNumbers = authOptions.trustedPhoneNumbers, !phoneNumbers.isEmpty {
- Text("Select a trusted phone number to receive a \(authOptions.securityCode.length) digit code via SMS:")
+ Text(String(format: localizeString("SelectTrustedPhone"), authOptions.securityCode.length))
List(phoneNumbers, selection: $selectedPhoneNumberID) {
Text($0.numberWithDialCode)
}
} else {
AttributedText(
- NSAttributedString(string: "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication.\n\nSee https://support.apple.com/en-ca/HT204915.")
+ NSAttributedString(string: localizeString("NoTrustedPhones"))
.convertingURLsToLinkAttributes()
)
Spacer()
diff --git a/Xcodes/Frontend/SignIn/SignInSMSView.swift b/Xcodes/Frontend/SignIn/SignInSMSView.swift
index 51c2c0a..74d7af0 100644
--- a/Xcodes/Frontend/SignIn/SignInSMSView.swift
+++ b/Xcodes/Frontend/SignIn/SignInSMSView.swift
@@ -11,7 +11,7 @@ struct SignInSMSView: View {
var body: some View {
VStack(alignment: .leading) {
- Text("Enter the \(authOptions.securityCode.length) digit code sent to \(trustedPhoneNumber.numberWithDialCode): ")
+ Text(String(format: localizeString("EnterDigitCodeDescription"), authOptions.securityCode.length, trustedPhoneNumber.numberWithDialCode))
HStack {
Spacer()
diff --git a/Xcodes/Frontend/SignIn/SignedInView.swift b/Xcodes/Frontend/SignIn/SignedInView.swift
index 1cf5e5d..4b3bbf1 100644
--- a/Xcodes/Frontend/SignIn/SignedInView.swift
+++ b/Xcodes/Frontend/SignIn/SignedInView.swift
@@ -10,7 +10,7 @@ struct SignedInView: View {
var body: some View {
HStack(alignment:.top, spacing: 10) {
Text(username)
- Button("Sign Out", action: appState.signOut)
+ Button("SignOut", action: appState.signOut)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
diff --git a/Xcodes/Frontend/XcodeList/BottomStatusBar.swift b/Xcodes/Frontend/XcodeList/BottomStatusBar.swift
new file mode 100644
index 0000000..cd38c3b
--- /dev/null
+++ b/Xcodes/Frontend/XcodeList/BottomStatusBar.swift
@@ -0,0 +1,49 @@
+//
+// BottomStatusBar.swift
+// Xcodes
+//
+// Created by Matt Kiazyk on 2022-06-03.
+// Copyright © 2022 Robots and Pencils. All rights reserved.
+//
+
+import Foundation
+import SwiftUI
+
+struct BottomStatusModifier: ViewModifier {
+ @EnvironmentObject var appState: AppState
+
+ func body(content: Content) -> some View {
+ VStack(spacing: 0) {
+ content
+ VStack(spacing: 0) {
+ Divider()
+ HStack {
+ Text(appState.bottomStatusBarMessage)
+ .font(.subheadline)
+ Spacer()
+ Text(Bundle.main.shortVersion!)
+ .font(.subheadline)
+ }
+ .frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
+ .padding([.leading, .trailing], 10)
+ }
+ .frame(maxWidth: .infinity, maxHeight: 30, alignment: .leading)
+ }
+ }
+}
+
+extension View {
+ func bottomStatusBar() -> some View {
+ self.modifier(
+ BottomStatusModifier()
+ )
+ }
+}
+
+struct Previews_BottomStatusBar_Previews: PreviewProvider {
+ static var previews: some View {
+ HStack {
+
+ }.bottomStatusBar()
+ }
+}
diff --git a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift
index 92d3c2d..e2e81f1 100644
--- a/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift
+++ b/Xcodes/Frontend/XcodeList/InstallationStepRowView.swift
@@ -22,7 +22,7 @@ struct InstallationStepRowView: View {
.scaleEffect(0.5)
}
- Text("Step \(installationStep.stepNumber) of \(installationStep.stepCount): \(installationStep.message)")
+ Text(String(format: localizeString("InstallationStepDescription"), installationStep.stepNumber, installationStep.stepCount, installationStep.message))
.font(.footnote)
Button(action: cancel) {
@@ -31,7 +31,7 @@ struct InstallationStepRowView: View {
}
.buttonStyle(PlainButtonStyle())
.foregroundColor(highlighted ? .white : .secondary)
- .help("Stop installation")
+ .help("StopInstallation")
}
.frame(minWidth: 80)
}
diff --git a/Xcodes/Frontend/XcodeList/MainToolbar.swift b/Xcodes/Frontend/XcodeList/MainToolbar.swift
index bedf3be..69a48f2 100644
--- a/Xcodes/Frontend/XcodeList/MainToolbar.swift
+++ b/Xcodes/Frontend/XcodeList/MainToolbar.swift
@@ -3,6 +3,7 @@ import SwiftUI
struct MainToolbarModifier: ViewModifier {
@EnvironmentObject var appState: AppState
@Binding var category: XcodeListCategory
+ @Binding var isInstalledOnly: Bool
@Binding var isShowingInfoPane: Bool
@Binding var searchText: String
@@ -12,12 +13,12 @@ struct MainToolbarModifier: ViewModifier {
}
private var toolbar: some ToolbarContent {
- ToolbarItemGroup(placement: .status) {
+ ToolbarItemGroup {
Button(action: { appState.presentedSheet = .signIn }, label: {
Label("Login", systemImage: "person.circle")
})
- .help("Login")
-
+ .help("LoginDescription")
+
ProgressButton(
isInProgress: appState.isUpdating,
action: appState.update
@@ -25,23 +26,54 @@ struct MainToolbarModifier: ViewModifier {
Label("Refresh", systemImage: "arrow.clockwise")
}
.keyboardShortcut(KeyEquivalent("r"))
- .help("Refresh")
+ .help("RefreshDescription")
Button(action: {
switch category {
- case .all: category = .installed
- case .installed: category = .all
+ case .all: category = .release
+ case .release: category = .beta
+ case .beta: category = .all
}
}) {
switch category {
case .all:
- Label("Filter", systemImage: "line.horizontal.3.decrease.circle")
- case .installed:
- Label("Filter", systemImage: "line.horizontal.3.decrease.circle.fill")
- .foregroundColor(.accentColor)
+ Label("All", systemImage: "line.horizontal.3.decrease.circle")
+ case .release:
+ if #available(macOS 11.3, *) {
+ Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
+ .labelStyle(TitleAndIconLabelStyle())
+ .foregroundColor(.accentColor)
+ } else {
+ Label("ReleaseOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
+ .labelStyle(TitleOnlyLabelStyle())
+ .foregroundColor(.accentColor)
+ }
+ case .beta:
+ if #available(macOS 11.3, *) {
+ Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
+ .labelStyle(TitleAndIconLabelStyle())
+ .foregroundColor(.accentColor)
+ } else {
+ Label("BetaOnly", systemImage: "line.horizontal.3.decrease.circle.fill")
+ .labelStyle(TitleOnlyLabelStyle())
+ .foregroundColor(.accentColor)
+ }
}
}
- .help("Filter installed versions")
+ .help("FilterAvailableDescription")
+
+ Button(action: {
+ isInstalledOnly.toggle()
+ }) {
+ if isInstalledOnly {
+ Label("Filter", systemImage: "arrow.down.app.fill")
+ .foregroundColor(.accentColor)
+ } else {
+ Label("Filter", systemImage: "arrow.down.app")
+
+ }
+ }
+ .help("FilterInstalledDescription")
Button(action: { isShowingInfoPane.toggle() }) {
if isShowingInfoPane {
@@ -52,12 +84,23 @@ struct MainToolbarModifier: ViewModifier {
}
}
.keyboardShortcut(KeyboardShortcut("i", modifiers: [.command, .option]))
- .help("Show or hide the info pane")
-
- TextField("Search...", text: $searchText)
+ .help("InfoDescription")
+
+ Button(action: {
+ if #available(macOS 13, *) {
+ NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
+ } else {
+ NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)
+ }
+ }, label: {
+ Label("Preferences", systemImage: "gearshape")
+ })
+ .help("PreferencesDescription")
+
+ TextField("Search", text: $searchText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 200)
- .help("Search list")
+ .help("SearchDescription")
}
}
}
@@ -65,12 +108,14 @@ struct MainToolbarModifier: ViewModifier {
extension View {
func mainToolbar(
category: Binding,
+ isInstalledOnly: Binding,
isShowingInfoPane: Binding,
searchText: Binding
) -> some View {
self.modifier(
MainToolbarModifier(
category: category,
+ isInstalledOnly: isInstalledOnly,
isShowingInfoPane: isShowingInfoPane,
searchText: searchText
)
diff --git a/Xcodes/Frontend/XcodeList/XcodeListCategory.swift b/Xcodes/Frontend/XcodeList/XcodeListCategory.swift
index 520414e..52ea790 100644
--- a/Xcodes/Frontend/XcodeList/XcodeListCategory.swift
+++ b/Xcodes/Frontend/XcodeList/XcodeListCategory.swift
@@ -2,14 +2,16 @@ import Foundation
enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible {
case all
- case installed
+ case release
+ case beta
var id: Self { self }
var description: String {
switch self {
- case .all: return "All"
- case .installed: return "Installed"
+ case .all: return localizeString("All")
+ case .release: return localizeString("Release")
+ case .beta: return localizeString("Beta")
}
}
}
diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift
index 4905e78..101010f 100644
--- a/Xcodes/Frontend/XcodeList/XcodeListView.swift
+++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift
@@ -7,11 +7,13 @@ struct XcodeListView: View {
@Binding var selectedXcodeID: Xcode.ID?
private let searchText: String
private let category: XcodeListCategory
+ private let isInstalledOnly: Bool
- init(selectedXcodeID: Binding, searchText: String, category: XcodeListCategory) {
+ init(selectedXcodeID: Binding, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool) {
self._selectedXcodeID = selectedXcodeID
self.searchText = searchText
self.category = category
+ self.isInstalledOnly = isInstalledOnly
}
var visibleXcodes: [Xcode] {
@@ -19,14 +21,20 @@ struct XcodeListView: View {
switch category {
case .all:
xcodes = appState.allXcodes
- case .installed:
- xcodes = appState.allXcodes.filter { $0.installState.installed }
+ case .release:
+ xcodes = appState.allXcodes.filter { $0.version.isNotPrerelease }
+ case .beta:
+ xcodes = appState.allXcodes.filter { $0.version.isPrerelease }
}
if !searchText.isEmpty {
xcodes = xcodes.filter { $0.description.contains(searchText) }
}
+ if isInstalledOnly {
+ xcodes = xcodes.filter { $0.installState.installed }
+ }
+
return xcodes
}
@@ -40,7 +48,7 @@ struct XcodeListView: View {
struct XcodeListView_Previews: PreviewProvider {
static var previews: some View {
Group {
- XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all)
+ XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false)
.environmentObject({ () -> AppState in
let a = AppState()
a.allXcodes = [
diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift
index 1b7e477..555c361 100644
--- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift
+++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift
@@ -6,26 +6,26 @@ struct XcodeListViewRow: View {
let xcode: Xcode
let selected: Bool
let appState: AppState
-
+
var body: some View {
HStack {
appIconView(for: xcode)
-
+
VStack(alignment: .leading) {
HStack {
Text(verbatim: "\(xcode.description) \(xcode.version.buildMetadataIdentifiersDisplay)")
.font(.body)
-
+
if !xcode.identicalBuilds.isEmpty {
Image(systemName: "square.fill.on.square.fill")
.font(.subheadline)
.foregroundColor(.secondary)
- .accessibility(label: Text("Identical Builds"))
+ .accessibility(label: Text("IdenticalBuilds"))
.accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", ")))
- .help("Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together.")
+ .help("IdenticalBuilds.help")
}
}
-
+
if case let .installed(path) = xcode.installState {
Text(verbatim: path.string)
.font(.caption)
@@ -35,9 +35,9 @@ struct XcodeListViewRow: View {
.font(.caption)
}
}
-
+
Spacer()
-
+
selectControl(for: xcode)
.padding(.trailing, 16)
installControl(for: xcode)
@@ -53,14 +53,18 @@ struct XcodeListViewRow: View {
OpenButton(xcode: xcode)
RevealButton(xcode: xcode)
CopyPathButton(xcode: xcode)
+ CreateSymbolicLinkButton(xcode: xcode)
+ if xcode.version.isPrerelease {
+ CreateSymbolicBetaLinkButton(xcode: xcode)
+ }
Divider()
UninstallButton(xcode: xcode)
-
+
#if DEBUG
- Divider()
- Button("Perform post-install steps") {
- appState.performPostInstallSteps(for: InstalledXcode(path: path)!) as Void
- }
+ Divider()
+ Button("Perform post-install steps") {
+ appState.performPostInstallSteps(for: InstalledXcode(path: path)!) as Void
+ }
#endif
}
}
@@ -76,38 +80,40 @@ struct XcodeListViewRow: View {
.foregroundColor(.secondary)
}
}
-
+
@ViewBuilder
private func selectControl(for xcode: Xcode) -> some View {
if xcode.installState.installed {
if xcode.selected {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
- .help("This is the active version")
+ .help("ActiveVersionDescription")
} else {
- Button(action: { appState.select(id: xcode.id) }) {
+ Button(action: { appState.select(xcode: xcode) }) {
Image(systemName: "checkmark.circle")
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())
- .help("Make this the active version")
+ .help("MakeActiveVersionDescription")
}
} else {
EmptyView()
}
}
-
+
@ViewBuilder
private func installControl(for xcode: Xcode) -> some View {
switch xcode.installState {
case .installed:
- Button("OPEN") { appState.open(id: xcode.id) }
+ Button("Open") { appState.open(xcode: xcode) }
+ .textCase(.uppercase)
.buttonStyle(AppStoreButtonStyle(primary: true, highlighted: selected))
- .help("Open this version")
+ .help("OpenDescription")
case .notInstalled:
- Button("INSTALL") { appState.install(id: xcode.id) }
+ Button("Install") { appState.checkMinVersionAndInstall(id: xcode.id) }
+ .textCase(.uppercase)
.buttonStyle(AppStoreButtonStyle(primary: false, highlighted: selected))
- .help("Install this version")
+ .help("InstallDescription")
case let .installing(installationStep):
InstallationStepRowView(
installationStep: installationStep,
@@ -126,31 +132,31 @@ struct XcodeListViewRow_Previews: PreviewProvider {
selected: false,
appState: AppState()
)
-
+
XcodeListViewRow(
xcode: Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil),
selected: false,
appState: AppState()
)
-
+
XcodeListViewRow(
xcode: Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil),
selected: false,
appState: AppState()
)
-
+
XcodeListViewRow(
xcode: Xcode(version: Version("12.0.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
selected: false,
appState: AppState()
)
-
+
XcodeListViewRow(
xcode: Xcode(version: Version("12.0.0+1234A")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
selected: false,
appState: AppState()
)
-
+
XcodeListViewRow(
xcode: Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil),
selected: false,
diff --git a/Xcodes/Resources/Info.plist b/Xcodes/Resources/Info.plist
index 3f498fe..49fe676 100644
--- a/Xcodes/Resources/Info.plist
+++ b/Xcodes/Resources/Info.plist
@@ -19,13 +19,13 @@
CFBundleShortVersionString
$(MARKETING_VERSION)
CFBundleVersion
- 7
+ $(CURRENT_PROJECT_VERSION)
CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT
$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
- Copyright © 2020 Robots and Pencils.
+ Copyright © 2023 Robots and Pencils.
NSPrincipalClass
NSApplication
NSSupportsAutomaticTermination
@@ -38,7 +38,7 @@
identifier "com.robotsandpencils.XcodesApp.Helper" and info [CFBundleShortVersionString] >= "1.0.0" and anchor apple generic and certificate leaf[subject.OU] = "$(CODE_SIGNING_SUBJECT_ORGANIZATIONAL_UNIT)"
SUFeedURL
- https://robotsandpencils.github.io/XcodesApp/appcast.xml
+ https://www.xcodes.app/appcast.xml
SUPublicEDKey
SEcz0vgUSeBTOoAXYe+64zea95G6lIf5NgzFs3InYJQ=
diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf
index 58f4a3d..93bc0ff 100644
--- a/Xcodes/Resources/Licenses.rtf
+++ b/Xcodes/Resources/Licenses.rtf
@@ -1,4 +1,4 @@
-{\rtf1\ansi\ansicpg1252\cocoartf2578
+{\rtf1\ansi\ansicpg1252\cocoartf2639
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 .SFNS-Regular;}
{\colortbl;\red255\green255\blue255;}
{\*\expandedcolortbl;;}
@@ -402,7 +402,7 @@ bspatch.c and bsdiff.c, from bsdiff 4.3 :\
sais.c and sais.c, from sais-lite (2010/08/07) :\
Copyright (c) 2008-2010 Yuta Mori.\
\
-SUDSAVerifier.m:\
+SUSignatureVerifier.m:\
Copyright (c) 2011 Mark Hamlin.\
\
All rights reserved.\
diff --git a/Xcodes/Resources/de.lproj/Localizable.strings b/Xcodes/Resources/de.lproj/Localizable.strings
new file mode 100644
index 0000000..e40d7cc
--- /dev/null
+++ b/Xcodes/Resources/de.lproj/Localizable.strings
@@ -0,0 +1,235 @@
+// Menu
+"Menu.About" = "Über Xcodes";
+"Menu.CheckForUpdates" = "Prüfe auf Updates...";
+"Menu.Acknowledgements" = "Xcodes Anerkennungen";
+"Menu.GitHubRepo" = "Xcodes GitHub-Repo";
+"Menu.ReportABug" = "Bug melden";
+"Menu.RequestNewFeature" = "Neues Feature anfordern";
+
+// Common
+"Install" = "Installieren";
+"InstallDescription" = "Diese Version installieren";
+"RevealInFinder" = "Im Finder anzeigen";
+"Active" = "Aktiv";
+"MakeActive" = "Aktivieren";
+"Open" = "Öffnen";
+"OpenDescription" = "Diese Version öffnen";
+"CopyPath" = "Pfad kopieren";
+"CreateSymLink" = "Symlink als Xcode.app erstellen";
+"CreateSymLinkBeta" = "Symlink als Xcode-Beta.app erstellen";
+"Uninstall" = "Deinstallieren";
+"Selected" = "Ausgewählt";
+"Select" = "Auswählen";
+"Cancel" = "Abbrechen";
+"Next" = "Nächstes";
+"Continue" = "Fortfahren";
+"Close" = "Schließen";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Identische Builds";
+"IdenticalBuilds.help" = "Manchmal sind Prerelease- and Release-Version der exakt gleich Build. Xcodes zeigt diese beiden Versionen automatisch zusammen an.";
+
+"ReleaseDate" = "Release-Datum";
+"ReleaseNotes" = "Release-Notes";
+"ReleaseNotes.help" = "Release-Notes anzeigen";
+"CopyReleaseNoteURL" = "URL kopieren";
+"Compatibility" = "Kompatibilität";
+"MacOSRequirement" = "Erfordert macOS %@ oder neuer";
+"SDKs" = "SDKs";
+"Compilers" = "Compiler";
+"DownloadSize" = "Download-Größe";
+"NoXcodeSelected" = "Kein Xcode ausgewählt";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Schritt %1$d von %2$d: %3$@";
+"DownloadingPercentDescription" = "Download: %d%% vollständig";
+"StopInstallation" = "Installation stoppen";
+"DownloadingError" = "Keine Download-Informationen gefunden";
+
+// About
+"VersionWithBuild" = "Version %@ (%@)";
+"GithubRepo" = "GitHub-Repo";
+"Acknowledgements" = "Anerkennungen";
+"UnxipExperiment" = "Unxip-Experiment";
+"License" = "Lizenz";
+
+// General Preference Pane
+"General" = "Allgemein";
+"AppleID" = "Apple-ID";
+"SignIn" = "Anmelden";
+"Notifications" = "Benachrichtigungen";
+
+// Updates Preference Pane
+"Updates" = "Updates";
+"Versions" = "Versionen";
+"AutomaticInstallNewVersion" = "Neue Versionen von Xcode automatisch installieren";
+"IncludePreRelease" = "Prerelease-/Beta-Versionen einschließen";
+"AppUpdates" = "Xcodes.app-Updates";
+"CheckForAppUpdates" = "Automatisch auf App-Updates prüfen";
+"CheckNow" = "Jetzt prüfen";
+"LastChecked" = "Letzte Prüfung: %@";
+"Never" = "Nie";
+
+// Download Preference Pane
+"Downloads" = "Downloads";
+"DataSource" = "Datenquelle";
+"DataSourceDescription" = "Die Apple-Datenquelle liest die Apple Developer-Website aus. Sie zeigt immer die neuesten Releases an, die verfügbar sind, ist allerdings etwas instabiler.\n\nXcode Releases ist eine inoffizielle Liste von Xcode-Veröffentlichungen. Sie wird als formatierte Daten bereitsgestellt, enthält Extrainformationen die nicht ohne weiteres von Apple erhältlich sind und ist mit höherer Wahrscheinlichkeit weiter verfügbar, sollte Apple seine Entwickler-Website neu gestalten.";
+"Downloader" = "Downloader";
+"DownloaderDescription" = "aria2 verwendet bis zu 16 Verbindungen, um Xcode 3-5x schneller als URLSession herunterzuladen. Es ist zusammen mit seinem Quellcode in Xcode enthalten, um seiner GPLv2-Lizenz nachzukommen.\n\nURLSession ist Apples Standard-API für URL-Requests.";
+
+// Advanced Preference Pane
+"Advanced" = "Erweitert";
+"LocalCachePath" = "Lokaler Cache-Pfad";
+"LocalCachePathDescription" = "Xcodes speichert verfügbare Xcode-Versionen zwischen und lädt neue Versionen temporär in ein Verzeichnis.";
+"Change" = "Ändern";
+"Active/Select" = "Aktiv/Auswählen";
+"InstallDirectory" = "Installationsverzeichnis";
+"InstallPathDescription" = "Xcodes sucht and installiert in ein einzelnes Verzeichnis. Standard (und empfohlen beizubehalten) ist /Applications. Änderungen am Speicherort von Xcode können dazu führen, dass andere Apps/Dienste aufhören zu funktionieren. ";
+
+"OnSelectDoNothing" = "Name beibehalten als Xcode-X.X.X.app";
+"OnSelectDoNothingDescription" = "Bei Auswahl wird der Name mit Version beibehalten, z. B. Xcode-13.4.1.app";
+"AutomaticallyCreateSymbolicLink" = "Symbolischen Link zur Xcode.app automatisch erstellen";
+"AutomaticallyCreateSymbolicLinkDescription" = "Beim Umstellen einer Xcode-Version auf Aktiv/Ausgewählt versuchen einen symbolischen Link namens Xcode.app im Installationsverzeichnis zu erstellen.";
+"OnSelectRenameXcode" = "Immer in Xcode.app umbenennen";
+"OnSelectRenameXcodeDescription" = "Bei Auswahl wird versucht das aktive Xcode in Xcode.app umzubenennen. Die vorherige Xcode.app wird dazu in den Versionsnamen umbenannt.";
+
+"PrivilegedHelper" = "Privilegierter Helfer";
+"PrivilegedHelperDescription" = "Xcodes verwendet einen separaten privilegierten Helfer, um Aufgaben als root zu erledigen. Das sind Dinge, die sudo in der Kommandozeile erfordern würden, einschließlich Post-Installationsschritte sowie das Umstellen von Xcode-Versionen mit xcode-select.\n\nUm ihn zu installieren, erfolgt eine Aufforderung zur Eingabe des Passworts für Dein macOS-Benutzerkonto.";
+"HelperInstalled" = "Helfer ist installiert";
+"HelperNotInstalled" = "Helfer ist nicht installiert";
+"InstallHelper" = "Helfer installieren";
+
+// Experiment Preference Pane
+"Experiments" = "Experimente";
+"FasterUnxip" = "Schnellerer Unxip";
+"UseUnxipExperiment" = "Beim Unxipping, Experiment verwenden";
+"FasterUnxipDescription" = "Dank an @_saagarjha, dieses Experiment kann die Unxipping-Geschwindigkeit bis zu 70% auf einigen Systemen beschleunigen.\n\nMehr Informationen wie dies erreicht wird sind über das Unxip-Repo erhältlich - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Zugriff erlaubt. Du empfängst jetzt Benachrichtigungen von Xcodes.";
+"AccessDenied" = "⚠️ Zugriff verweigert ⚠️\n\nBitte öffne Deine Benachrichtigungs-Einstellungen und wähle Xcodes aus, wenn Du den Zugriff erlauben möchtest.";
+"NotificationSettings" = "Benachrichtigungs-Einstellungen";
+"EnableNotifications" = "Benachrichtigungen einschalten";
+
+// SignIn
+"SignInWithApple" = "Mit Deiner Apple-ID anmelden.";
+"AppleID" = "Apple-ID:";
+"Password" = "Passwort:";
+"Required" = "Erforderlich";
+"SignOut" = "Abmelden";
+
+// SMS/2FA
+"DigitCodeDescription" = "Gib den %d-stelligen Code von einem Deiner Vertrauensgeräte ein:";
+"SendSMS" = "SMS senden";
+"EnterDigitCodeDescription" = "Gib den %d-stelligen Code ein der an '%@' gesendet wurde.";
+"SelectTrustedPhone" = "Wähle eine vertrauenswürdige Telefonnummer aus, um einen %d-stelligen Code via SMS zum empfangen:";
+"NoTrustedPhones" = "Dein Account verfügt über keine vertrauenswürdigen Telefonnummern, diese sind aber für Zwei-Faktor-Authentifizierung erforderlich.\n\nInformationen dazu unter https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Aktualisiert am";
+
+// ToolBar
+"Login" = "Login";
+"LoginDescription" = "Login öffnen";
+"Refresh" = "Aktualisieren";
+"RefreshDescription" = "Xcode-Liste aktualisieren";
+"All" = "Alle";
+"Release" = "Release";
+"ReleaseOnly" = "Nur Release";
+"Beta" = "Beta";
+"BetaOnly" = "Nur Beta";
+"Filter" = "Filter";
+"FilterAvailableDescription" = "Verfügbare Versionen filtern";
+"FilterInstalledDescription" = "Installierte Versionen filtern";
+"Info" = "Info";
+"InfoDescription" = "Info-Panel anzeigen oder verbergen";
+"Preferences" = "Einstellungen";
+"PreferencesDescription" = "Einstellungen öffnen";
+"Search" = "Suchen ...";
+"SearchDescription" = "Suchliste";
+
+// List
+"ActiveVersionDescription" = "Dies ist die aktive Version";
+"MakeActiveVersionDescription" = "Dies zur aktiven Version machen";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Xcode %@ deinstallieren?";
+"Alert.Uninstall.Message" = "Die Anwendung wird in den Papierkorb verschoben, dieser wird aber nicht geleert.";
+"Alert.Uninstall.Error.Title" = "Die Deinstallation von Xcode ist nicht möglich";
+"Alert.Uninstall.Error.Message.FileNotFound" = "Datei \"%@\" konnte nicht gefunden werden.";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Bist du sicher, dass Du die installation von Xcode %@ anhalten möchtest?";
+"Alert.CancelInstall.Message" = "Jeglicher Fortschritt wird verworfen.";
+"Alert.CancelInstall.PrimaryButton" = "Installation anhalten";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Privilegierter Helfer";
+"Alert.PrivilegedHelper.Message" = "Xcodes verwendet einen separaten privilegierten Helfer, um Aufgaben als root zu erledigen. Das sind Dinge, die sudo in der Kommandozeile erfordern würden, einschließlich Post-Installationsschritte sowie das Umstellen von Xcode-Versionen mit xcode-select.\n\nUm ihn zu installieren, erfolgt eine Aufforderung zur Eingabe des Passworts für Dein macOS-Benutzerkonto.";
+"Alert.PrivilegedHelper.Error.Title" = "Installation des Helfers nicht möglich";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Minimalanforderungen nicht erfülltt";
+"Alert.MinSupported.Message" = "Xcode %@ erfordert MacOS %@ aber es läuft nur MacOS %@. Möchtest Du es trotzdem installieren?";
+
+// Install
+"Alert.Install.Error.Title" = "Installation von Xcode nicht möglich";
+"Alert.InstallArchive.Error.Title" = "Installation des archivierten Xcodes nicht möglich";
+
+// Update
+"Alert.Update.Error.Title" = "Update des ausgewählten Xcodes nicht möglich";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Auswahl von Xcode nicht möglich";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Erstellung des symbolischen Links nicht möglich";
+"Alert.SymLink.Message" = "Xcode.app existiert und ist kein symbolischer Link";
+
+// Post install
+"Alert.PostInstall.Title" = "Ausführung von Post-Installationsschritten nicht möglich";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "Das Archiv \"%@\" ist beschädigt und kann nicht entpackt werden.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "Das Archiv \"%@\" kann nicht entpackt werden, da auf dem aktuellen Volume nicht genügend freier Speicherplatz verfügbar ist.\n\nBitte stelle mehr Platz zur Verfügung, um das Archiv zu entpacken und installiere danach Xcode %@ erneut, um die Installation von dort zu beginnen wo sie beendet wurde.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Das Bewegen von Xcode in das %@-Verzeichnis ist nicht möglich.";
+"InstallationError.FailedSecurityAssessment" = "Die Sicherheitsprüfung für Xcode %@ ist mit folgender Meldung gescheitert:\n%@\nXcode bleibt unter %@ installiert, für den Fall, dass es dennoch verwendet werden soll.";
+"InstallationError.CodesignVerifyFailed" = "Die heruntergeladene Version von Xcode hat die Code-Signing-Prüfung nicht bestanden, mit folgendem Hinweis:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "Die heruntergeladene Version von Xcode hat nicht die erwartete Code-Signing-Identity.\nErhalten:\n%@\n%@\nErwartet:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes unterstützt (bislang) nicht die Installation von Xcode per %@-Dateiformat.";
+"InstallationError.MissingSudoerPassword" = "Passwort fehlt. Bitte erneut versuchen.";
+"InstallationError.UnavailableVersion" = "Kann Version %@ nicht finden.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Keine Nicht-Prerelease-Versionen verfügbar.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Keine Prerelease-Versions verfügbar.";
+"InstallationError.MissingUsernameOrPassword" = "Benutzername oder ein Passwort fehlt. Bitte erneut versuchen.";
+"InstallationError.VersionAlreadyInstalled" = "%@ ist bereits installiert unter %@";
+"InstallationError.InvalidVersion" = "%@ ist keine gültige Versionsnummer.";
+"InstallationError.VersionNotInstalled" = "%@ ist nicht installiert.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "Die Installation ist abgeschlossen, allerdings wurden einige Post-Installationsschritte nicht automatisch ausgeführt. Diese werden beim ersten Start von Xcode %@ ausgeführt.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "Die Installation ist abgeschlossen, allerdings wurden einige Post-Installationsschritte nicht automatisch ausgeführt. Xcodes führt diese Schritte mittels eines privilegierten Helfers aus, welcher aber nicht installiert zu sein scheint. Er kann über Einstellungen > Erweitert installiert werden. Diese Schritte werden beim ersten Start von Xcode %@ ausgeführt.";
+
+// Installation Steps
+"Downloading" = "Herunterladen";
+"Unarchiving" = "Entpacken (Dies kann etwas dauern)";
+"Moving" = "In %@ bewegen";
+"TrashingArchive" = "Archiv in den Papierkorb bewegen";
+"CheckingSecurity" = "Sicherheitsprüfung";
+"Finishing" = "Abschließen";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Neue Version verfügbar";
+"Notification.FinishedInstalling" = "Installation beendet";
+
+
+"HelperClient.error" = "Unable to communicate with privileged helper.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Neue Xcode-Versionen";
+"Notification.NewXcodeVersion.Body" = "Neue Xcode-Versionen stehen zum Download bereit.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/en.lproj/Localizable.strings b/Xcodes/Resources/en.lproj/Localizable.strings
new file mode 100644
index 0000000..59a8676
--- /dev/null
+++ b/Xcodes/Resources/en.lproj/Localizable.strings
@@ -0,0 +1,238 @@
+// Menu
+"Menu.About" = "About Xcodes";
+"Menu.CheckForUpdates" = "Check for Updates...";
+"Menu.Acknowledgements" = "Xcodes Acknowledgements";
+"Menu.GitHubRepo" = "Xcodes GitHub Repo";
+"Menu.ReportABug" = "Report a Bug";
+"Menu.RequestNewFeature" = "Request a New Feature";
+
+// Common
+"Install" = "Install";
+"InstallDescription" = "Install this version";
+"RevealInFinder" = "Reveal in Finder";
+"Active" = "Active";
+"MakeActive" = "Make active";
+"Open" = "Open";
+"OpenDescription" = "Open this version";
+"CopyPath" = "Copy Path";
+"CreateSymLink" = "Create Symlink as Xcode.app";
+"CreateSymLinkBeta" = "Create Symlink as Xcode-Beta.app";
+"Uninstall" = "Uninstall";
+"Selected" = "Selected";
+"Select" = "Select";
+"Cancel" = "Cancel";
+"Next" = "Next";
+"Continue" = "Continue";
+"Close" = "Close";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Identical Builds";
+"IdenticalBuilds.help" = "Sometimes a prerelease and release version are the exact same build. Xcodes will automatically display these versions together.";
+
+"ReleaseDate" = "Release Date";
+"ReleaseNotes" = "Release Notes";
+"ReleaseNotes.help" = "View Release Notes";
+"CopyReleaseNoteURL" = "Copy URL";
+"Compatibility" = "Compatibility";
+"MacOSRequirement" = "Requires macOS %@ or later";
+"SDKs" = "SDKs";
+"Compilers" = "Compilers";
+"DownloadSize" = "Download Size";
+"NoXcodeSelected" = "No Xcode Selected";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Step %1$d of %2$d: %3$@";
+"DownloadingPercentDescription" = "Downloading: %d%% complete";
+"StopInstallation" = "Stop installation";
+"DownloadingError" = "No download information found";
+
+// About
+"VersionWithBuild" = "Version %@ (%@)";
+"GithubRepo" = "GitHub Repo";
+"Acknowledgements" = "Acknowledgements";
+"UnxipExperiment" = "Unxip Experiment";
+"License" = "License";
+
+// General Preference Pane
+"General" = "General";
+"AppleID" = "Apple ID";
+"SignIn" = "Sign In";
+"Notifications" = "Notifications";
+
+// Updates Preference Pane
+"Updates" = "Updates";
+"Versions" = "Versions";
+"AutomaticInstallNewVersion" = "Automatically install new versions of Xcode";
+"IncludePreRelease" = "Include prerelease/beta versions";
+"AppUpdates" = "Xcodes.app Updates";
+"CheckForAppUpdates" = "Automatically check for app updates";
+"CheckNow" = "Check Now";
+"LastChecked" = "Last checked: %@";
+"Never" = "Never";
+
+// Download Preference Pane
+"Downloads" = "Downloads";
+"DataSource" = "Data Source";
+"DataSourceDescription" = "The Apple data source scrapes the Apple Developer website. It will always show the latest releases that are available, but is more fragile.\n\nXcode Releases is an unofficial list of Xcode releases. It's provided as well-formed data, contains extra information that is not readily available from Apple, and is less likely to break if Apple redesigns their developer website.";
+"Downloader" = "Downloader";
+"DownloaderDescription" = "aria2 uses up to 16 connections to download Xcode 3-5x faster than URLSession. It's bundled as an executable along with its source code within Xcodes to comply with its GPLv2 license.\n\nURLSession is the default Apple API for making URL requests.";
+
+// Advanced Preference Pane
+"Advanced" = "Advanced";
+"LocalCachePath" = "Local Cache Path";
+"LocalCachePathDescription" = "Xcodes caches available Xcode versions and temporary downloads new versions to a directory";
+"Change" = "Change";
+"Active/Select" = "Active/Select";
+"InstallDirectory" = "Install Directory";
+"InstallPathDescription" = "Xcodes searches and installs to a single directory. By default (and recommended) is to keep this /Applications. Any changes to where Xcode is stored may result in other apps/services to stop working. ";
+
+"OnSelectDoNothing" = "Keep name as Xcode-X.X.X.app";
+"OnSelectDoNothingDescription" = "On select, will keep the name as the version eg. Xcode-13.4.1.app";
+"AutomaticallyCreateSymbolicLink" = "Automatically create symbolic link to Xcode.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "When making an Xcode version Active/Selected, try and create a symbolic link named Xcode.app in the installation directory";
+"OnSelectRenameXcode" = "Always rename to Xcode.app";
+"OnSelectRenameXcodeDescription" = "On select, will automatically try and rename the active Xcode to Xcode.app, renaming the previous Xcode.app to the version name.";
+
+"PrivilegedHelper" = "Privileged Helper";
+"PrivilegedHelperDescription" = "Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it.";
+"HelperInstalled" = "Helper is installed";
+"HelperNotInstalled" = "Helper is not installed";
+"InstallHelper" = "Install helper";
+
+"ShowOpenInRosetta" = "Show Open In Rosetta option";
+"ShowOpenInRosettaDescription" = "Open in Rosetta option will show where other \"Open\" functions are available. Note: This will only show for Apple Silicon machines.";
+
+// Experiment Preference Pane
+"Experiments" = "Experiments";
+"FasterUnxip" = "Faster Unxip";
+"UseUnxipExperiment" = "When unxipping, use experiment";
+"FasterUnxipDescription" = "Thanks to @_saagarjha, this experiment can increase unxipping speed by up to 70% for some systems.\n\nMore information on how this is accomplished can be seen on the unxip repo - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Access Granted. You will receive notifications from Xcodes.";
+"AccessDenied" = "⚠️ Access Denied ⚠️\n\nPlease open your Notification Settings and select Xcodes if you wish to allow access.";
+"NotificationSettings" = "Notification Settings";
+"EnableNotifications" = "Enable Notifications";
+
+// SignIn
+"SignInWithApple" = "Sign in with your Apple ID.";
+"AppleID" = "AppleID:";
+"Password" = "Password:";
+"Required" = "Required";
+"SignOut" = "Sign Out";
+
+// SMS/2FA
+"DigitCodeDescription" = "Enter the %d digit code from one of your trusted devices:";
+"SendSMS" = "Send SMS";
+"EnterDigitCodeDescription" = "Enter the %d digit code sent to %@: ";
+"SelectTrustedPhone" = "Select a trusted phone number to receive a %d digit code via SMS:";
+"NoTrustedPhones" = "Your account doesn't have any trusted phone numbers, but they're required for two-factor authentication.\n\nSee https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Updated at";
+
+// ToolBar
+"Login" = "Login";
+"LoginDescription" = "Open Login";
+"Refresh" = "Refresh";
+"RefreshDescription" = "Refresh Xcode List";
+"All" = "All";
+"Release" = "Release";
+"ReleaseOnly" = "Release only";
+"Beta" = "Beta";
+"BetaOnly" = "Beta only";
+"Filter" = "Filter";
+"FilterAvailableDescription" = "Filter available versions";
+"FilterInstalledDescription" = "Filter installed versions";
+"Info" = "Info";
+"InfoDescription" = "Show or hide the info pane";
+"Preferences" = "Preferences";
+"PreferencesDescription" = "Open Preferences";
+"Search" = "Search...";
+"SearchDescription" = "Search list";
+
+// List
+"ActiveVersionDescription" = "This is the active version";
+"MakeActiveVersionDescription" = "Make this the active version";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Uninstall Xcode %@?";
+"Alert.Uninstall.Message" = "It will be moved to the Trash, but won't be emptied.";
+"Alert.Uninstall.Error.Title" = "Unable to uninstall Xcode";
+"Alert.Uninstall.Error.Message.FileNotFound" = "Could not find file \"%@\".";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Are you sure you want to stop the installation of Xcode %@?";
+"Alert.CancelInstall.Message" = "Any progress will be discarded.";
+"Alert.CancelInstall.PrimaryButton" = "Stop Installation";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Privileged Helper";
+"Alert.PrivilegedHelper.Message" = "Xcodes uses a separate privileged helper to perform tasks as root. These are things that would require sudo on the command line, including post-install steps and switching Xcode versions with xcode-select.\n\nYou'll be prompted for your macOS account password to install it.";
+"Alert.PrivilegedHelper.Error.Title" = "Unable to install helper";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Minimum requirements not met";
+"Alert.MinSupported.Message" = "Xcode %@ requires MacOS %@, but you are running MacOS %@, do you still want to install it?";
+
+// Install
+"Alert.Install.Error.Title" = "Unable to install Xcode";
+"Alert.InstallArchive.Error.Title" = "Unable to install archived Xcode";
+
+// Update
+"Alert.Update.Error.Title" = "Unable to update selected Xcode";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Unable to select Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Unable to create symbolic Link";
+"Alert.SymLink.Message" = "Xcode.app exists and is not a symbolic link";
+
+// Post install
+"Alert.PostInstall.Title" = "Unable to perform post install steps";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "The archive \"%@\" is damaged and can't be expanded.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "The archive \"%@\" can’t be expanded because the current volume doesn’t have enough free space.\n\nMake more space available to expand the archive and then install Xcode %@ again to start installation from where it left off.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Failed to move Xcode to the %@ directory.";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ failed its security assessment with the following output:\n%@\nIt remains installed at %@ if you wish to use it anyways.";
+"InstallationError.CodesignVerifyFailed" = "The downloaded Xcode failed code signing verification with the following output:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "The downloaded Xcode doesn't have the expected code signing identity.\nGot:\n%@\n%@\nExpected:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes doesn't (yet) support installing Xcode from the %@ file format.";
+"InstallationError.MissingSudoerPassword" = "Missing password. Please try again.";
+"InstallationError.UnavailableVersion" = "Could not find version %@.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "No non-prerelease versions available.";
+"InstallationError.NoPrereleaseVersionAvailable" = "No prerelease versions available.";
+"InstallationError.MissingUsernameOrPassword" = "Missing username or a password. Please try again.";
+"InstallationError.VersionAlreadyInstalled" = "%@ is already installed at %@";
+"InstallationError.InvalidVersion" = "%@ is not a valid version number.";
+"InstallationError.VersionNotInstalled" = "%@ is not installed.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "Installation was completed, but some post-install steps weren't performed automatically. These will be performed when you first launch Xcode %@.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "Installation was completed, but some post-install steps weren't performed automatically. Xcodes performs these steps with a privileged helper, which appears to not be installed. You can install it from Preferences > Advanced.\n\nThese steps will be performed when you first launch Xcode %@.";
+
+// Installation Steps
+"Downloading" = "Downloading";
+"Unarchiving" = "Unarchiving (This can take a while)";
+"Moving" = "Moving to %@";
+"TrashingArchive" = "Moving archive to the Trash";
+"CheckingSecurity" = "Security verification";
+"Finishing" = "Finishing";
+
+// Notifications
+"Notification.NewVersionAvailable" = "New version is available";
+"Notification.FinishedInstalling" = "Finished installing";
+
+
+"HelperClient.error" = "Unable to communicate with privileged helper.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "New Xcode versions";
+"Notification.NewXcodeVersion.Body" = "New Xcode versions are available to download.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/es.lproj/Localizable.strings b/Xcodes/Resources/es.lproj/Localizable.strings
new file mode 100644
index 0000000..a910bc7
--- /dev/null
+++ b/Xcodes/Resources/es.lproj/Localizable.strings
@@ -0,0 +1,230 @@
+// Menu
+"Menu.About" = "Acerca de Xcodes";
+"Menu.CheckForUpdates" = "Buscar Actualizaciones...";
+"Menu.Acknowledgements" = "Xcodes Agradecimientos";
+"Menu.GitHubRepo" = "Xcodes GitHub Repositorio";
+"Menu.ReportABug" = "Reportar un fallo";
+"Menu.RequestNewFeature" = "Solicitar una nueva función";
+
+// Common
+"Install" = "Instalar";
+"InstallDescription" = "Instalar esta versión";
+"RevealInFinder" = "Mostrar en el Finder";
+"Active" = "Activo";
+"MakeActive" = "Activarlo";
+"Open" = "Abrir";
+"OpenDescription" = "Abrir esta versión";
+"CopyPath" = "Copiar Ruta";
+"CreateSymLink" = "Crear Symlink como Xcode.app";
+"CreateSymLinkBeta" = "Crear Symlink como Xcode-Beta.app";
+"Uninstall" = "Desinstalar";
+"Selected" = "Seleccionado";
+"Select" = "Seleccionar";
+"Cancel" = "Cancelar";
+"Next" = "Siguiente";
+"Continue" = "Continuar";
+"Close" = "Cerrar";
+"OK" = "Aceptar";
+
+// Info Pane
+"IdenticalBuilds" = "Compilaciones Idénticas";
+"IdenticalBuilds.help" = "A veces, una versión preliminar y una versión de lanzamiento tienen exactamente la misma compilación. Xcodes mostrará automáticamente estas versiones juntas.";
+
+"ReleaseDate" = "Fecha de lanzamiento";
+"ReleaseNotes" = "Notas del lanzamiento";
+"ReleaseNotes.help" = "Ver Notas del Lanzamiento";
+"CopyReleaseNoteURL" = "Copiar URL";
+"Compatibility" = "Compatibilidad";
+"MacOSRequirement" = "Requiere macOS %@ o posterior";
+"SDKs" = "SDKs";
+"Compilers" = "Compiladores";
+"DownloadSize" = "Tamaño de descarga";
+"NoXcodeSelected" = "No se ha seleccionado un Xcode";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Paso %1$d de %2$d: %3$@";
+"DownloadingPercentDescription" = "Descargando: %d%% completado";
+"StopInstallation" = "Detener Instalación";
+"DownloadingError" = "No se encontró información de descarga";
+
+// About
+"VersionWithBuild" = "Versión %@ (%@)";
+"GithubRepo" = "GitHub Repositorio";
+"Acknowledgements" = "Agradecimientos";
+"UnxipExperiment" = "Experimento Unxip";
+"License" = "Licencia";
+
+// General Preference Pane
+"General" = "General";
+"AppleID" = "Apple ID";
+"SignIn" = "Iniciar Sesión";
+"Notifications" = "Notificaciones";
+
+// Updates Preference Pane
+"Updates" = "Actualizaciones";
+"Versions" = "Versiones";
+"AutomaticInstallNewVersion" = "Instalar automáticamente versions de Xcode";
+"IncludePreRelease" = "Incluir versiones preliminares/beta";
+"AppUpdates" = "Xcodes.app Actualizaciones";
+"CheckForAppUpdates" = "Buscar automáticamente actualizaciones";
+"CheckNow" = "Revisarlo Ahora";
+"LastChecked" = "Última comprobación: %@";
+"Never" = "Nunca";
+
+// Download Preference Pane
+"Downloads" = "Descargas";
+"DataSource" = "Fuente de datos";
+"DataSourceDescription" = "La fuente de datos de Apple la extrae de el sitio web de Apple Developer. Siempre mostrará los últimos lanzamientos disponibles, pero es más frágil.\n\nXcode Releases es una lista no oficial de lanzamientos de Xcode. Se proporciona como datos bien estructurados, contiene información adicional que no está disponible fácilmente en Apple y es menos probable que se rompa si Apple rediseña su sitio web para desarrolladores.";
+"Downloader" = "Descargador";
+"DownloaderDescription" = "aria2 usa hasta 16 conexiones para descargar Xcode de 3 a 5 veces más rápido que URLSession. Se incluye como un ejecutable junto con su código fuente dentro de Xcodes para cumplir con su licencia GPLv2.\n\nURLSession es la API predeterminada de Apple para realizar solicitudes de URL.";
+
+// Advanced Preference Pane
+"Advanced" = "Avanzado";
+"LocalCachePath" = "Ruta de caché local";
+"LocalCachePathDescription" = "Xcodes almacena en caché versiones de Xcode disponibles y descargas temporalmente las nuevas versiones en un directorio";
+"Change" = "Cambiar";
+"Active/Select" = "Activar/Seleccionar";
+"InstallDirectory" = "Directorio de instalación";
+
+"OnSelectDoNothing" = "Mantener el nombre como Xcode-X.X.X.app";
+"OnSelectDoNothingDescription" = "Al seleccionar, mantener el nombre como la versión p.ej. Xcode-13.4.1.app";
+"AutomaticallyCreateSymbolicLink" = "Crear automáticamente enlace simbólico a Xcode.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "Al activar/seleccionar una versión de Xcode, intentará crear un enlace simbólico llamado Xcode.app en el directorio de instalación.";
+"PrivilegedHelper" = "Asistente privilegiado";
+"PrivilegedHelperDescription" = "Xcodes utiliza un asistente privilegiado independiente para realizar tareas como root. Estas son cosas que requerirían sudo en la línea de comandos, incluidos los pasos posteriores a la instalación y el cambio de versiones de Xcode con xcode-select.\n\nSe le pedirá la contraseña de su cuenta de macOS para instalarlo.";
+"HelperInstalled" = "El Asistente está instalado";
+"HelperNotInstalled" = "El Asistente no está instalado.";
+"InstallHelper" = "Instalar Asistente";
+
+// Experiment Preference Pane
+"Experiments" = "Experimentos";
+"FasterUnxip" = "Unxip más rápido";
+"UseUnxipExperiment" = "Al descomprimir, usar experimento";
+"FasterUnxipDescription" = "Gracias a @_saagarjha, este experimento puede aumentar la velocidad de liberación hasta en un 70 % para algunos sistemas.\n\nPuede ver más información sobre cómo se logra esto en el repositorio de unxip: https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Acceso Permitido. Recibirás notificaciones de Xcodes.";
+"AccessDenied" = "⚠️ Acceso Denegado ⚠️\n\nPor favor abra su Configuración de notificaciones y seleccione Xcodes si desea permitir el acceso.";
+"NotificationSettings" = "Configuración de las notificaciones";
+"EnableNotifications" = "Permitir notificaciones";
+
+// SignIn
+"SignInWithApple" = "Inicia sesión con tu ID de Apple.";
+"AppleID" = "AppleID:";
+"Password" = "Contraseña:";
+"Required" = "Requerido";
+"SignOut" = "Cerrar Sesión";
+
+// SMS/2FA
+"DigitCodeDescription" = "Ingrese el código de %d dígitos de uno de sus dispositivos de confianza:";
+"SendSMS" = "Enviar SMS";
+"EnterDigitCodeDescription" = "Ingrese el código de dígito %d enviado a %@: ";
+"SelectTrustedPhone" = "Selecciona un número de teléfono de confianza para recibir un código de %d dígitos por SMS:";
+"NoTrustedPhones" = "Su cuenta no tiene números de teléfono de confianza, pero son necesarios para la autenticación de dos factores.\n\nVer https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Actualizado en";
+
+// ToolBar
+"Login" = "Iniciar Sesión";
+"LoginDescription" = "Abrir Inicio de Sesión";
+"Refresh" = "Refrescar";
+"RefreshDescription" = "Refrescar lista de Xcodes";
+"All" = "Todos";
+"Release" = "Liberado";
+"ReleaseOnly" = "Solo Liberados";
+"Beta" = "Beta";
+"BetaOnly" = "Solo Beta";
+"Filter" = "Filtro";
+"FilterAvailableDescription" = "Filtrar versiones disponibles";
+"FilterInstalledDescription" = "Filtrar versiones instaladas";
+"Info" = "Info";
+"InfoDescription" = "Mostrar u ocultar el panel de información";
+"Preferences" = "Preferencias";
+"PreferencesDescription" = "Abrir Preferencias";
+"Search" = "Buscar...";
+"SearchDescription" = "Lista de búsqueda";
+
+// List
+"ActiveVersionDescription" = "Esta es la versión activa.";
+"MakeActiveVersionDescription" = "Haz que esta sea la versión activa";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Desinstalar Xcode %@?";
+"Alert.Uninstall.Message" = "Se moverá a la Papelera, pero no se vaciará.";
+"Alert.Uninstall.Error.Title" = "No se puede desinstalar Xcode";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "¿Está seguro de que desea detener la instalación de Xcode %@?";
+"Alert.CancelInstall.Message" = "Cualquier progreso será descartado.";
+"Alert.CancelInstall.PrimaryButton" = "Detener Instalación";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Asistente Privilegiado";
+"Alert.PrivilegedHelper.Message" = "Xcodes utiliza un Asistente Privilegiado independiente para realizar tareas como root. Estas son cosas que requerirían sudo en la línea de comando, incluidos los pasos posteriores a la instalación y el cambio de versiones de Xcode con xcode-select.\n\nSe le pedirá la contraseña de su cuenta de macOS para instalarlo.";
+"Alert.PrivilegedHelper.Error.Title" = "No se puede instalar el asistente";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Requisitos mínimos no cumplidos";
+"Alert.MinSupported.Message" = "Xcode %@ requiere MacOS %@, pero está ejecutando MacOS %@, ¿aún desea instalarlo?";
+
+// Install
+"Alert.Install.Error.Title" = "No se puede instalar Xcode";
+"Alert.InstallArchive.Error.Title" = "No se puede instalar el Xcode archivado";
+
+// Update
+"Alert.Update.Error.Title" = "No se puede actualizar el Xcode seleccionado";
+
+// Active/Select
+"Alert.Select.Error.Title" = "No se puede seleccionar el Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "No se puede crear un enlace simbólico";
+"Alert.SymLink.Message" = "Xcode.app existe y no es un enlace simbólico";
+
+// Post install
+"Alert.PostInstall.Title" = "No se pueden realizar los pasos posteriores a la instalación";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "El archivo \"%@\" está dañado y no se puede expandir.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "El archivo \"%@\" no se puede expandir porque el volumen/disco actual no tiene suficiente espacio libre.\n\nHaga más espacio disponible para expandir el archivo y luego instale Xcode %@ nuevamente para comenzar la instalación desde donde lo dejó.";
+"InstallationError.FailedToMoveXcodeToApplications" = "No se pudo mover Xcode al directorio %@.";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ falló su evaluación de seguridad con el siguiente resultado:\n%@\nPermanece instalado en %@ si desea usarlo de todos modos.";
+"InstallationError.CodesignVerifyFailed" = "El Xcode descargado falló en la verificación de firma de código con el siguiente resultado:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "El Xcode descargado no tiene la identidad de firma de código esperada.\nObtuvo:\n%@\n%@\nEsperado:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes no admite (todavía) la instalación de Xcode desde el formato de archivo %@.";
+"InstallationError.MissingSudoerPassword" = "Falta la contraseña. Inténtalo de nuevo.";
+"InstallationError.UnavailableVersion" = "No se pudo encontrar la versión %@.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "No hay versiones que no sean preliminares disponibles.";
+"InstallationError.NoPrereleaseVersionAvailable" = "No hay versiones preliminares disponibles.";
+"InstallationError.MissingUsernameOrPassword" = "Falta el nombre de usuario o una contraseña. Inténtalo de nuevo.";
+"InstallationError.VersionAlreadyInstalled" = "%@ ya está instalado en %@";
+"InstallationError.InvalidVersion" = "%@ no es un número de versión válido.";
+"InstallationError.VersionNotInstalled" = "%@ no esta instalada.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "La instalación se completó, pero algunos pasos posteriores a la instalación no se realizaron automáticamente. Estos se realizarán cuando inicie Xcode por primera vez %@.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "La instalación se completó, pero algunos pasos posteriores a la instalación no se realizaron automáticamente. Xcodes realiza estos pasos con un asistente privilegiado, que parece no estar instalado. Puede instalarlo desde Preferencias > Avanzado.\n\nEstos pasos se realizarán cuando inicie Xcode por primera vez %@.";
+
+// Installation Steps
+"Downloading" = "Descargando";
+"Unarchiving" = "Desarchivando (esto puede llevar un tiempo)";
+"Moving" = "Moviendo a %@";
+"TrashingArchive" = "Mover archivo a la Papelera";
+"CheckingSecurity" = "Verificación de seguridad";
+"Finishing" = "Finalizando";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Una nueva versión está disponible";
+"Notification.FinishedInstalling" = "Terminado de instalar";
+
+
+"HelperClient.error" = "No se puede comunicar con el Asistente privilegiado.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Nuevas versiones de Xcode";
+"Notification.NewXcodeVersion.Body" = "Nuevas versiones de Xcode están disponibles para descargar.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/fi.lproj/Localizable.strings b/Xcodes/Resources/fi.lproj/Localizable.strings
new file mode 100644
index 0000000..905a1e0
--- /dev/null
+++ b/Xcodes/Resources/fi.lproj/Localizable.strings
@@ -0,0 +1,225 @@
+// Menu
+"Menu.About" = "Tietoa Xcodesista";
+"Menu.CheckForUpdates" = "Tarkasta uusia päivityksiä...";
+"Menu.Acknowledgements" = "Xcodes kunnianosoitukset";
+"Menu.GitHubRepo" = "Xcodes GitHub Repo";
+"Menu.ReportABug" = "Ilmoita bugi";
+"Menu.RequestNewFeature" = "Pyydä uutta ominaisuutta";
+
+// Common
+"Install" = "Asenna";
+"InstallDescription" = "Asenna tämä versio";
+"RevealInFinder" = "Näytä finderissa";
+"Active" = "Aktiivinen";
+"MakeActive" = "Aktivoi";
+"Open" = "Avaa";
+"OpenDescription" = "Avaa tämä versio";
+"CopyPath" = "Kopioi polku";
+"CreateSymLink" = "Luo Symlink nimellä Xcode.app";
+"CreateSymLinkBeta" = "Luo Symlink nimellä Xcode-Beta.app";
+"Uninstall" = "Poista";
+"Selected" = "Valittu";
+"Select" = "Valitse";
+"Cancel" = "Peruuta";
+"Next" = "Seuraava";
+"Continue" = "Jatka";
+"Close" = "Sulje";
+"OK" = "Ok";
+
+// Info Pane
+"IdenticalBuilds" = "Identtiset rakenteet";
+"IdenticalBuilds.help" = "Joskus esijulkaisu ja julkaisuversio ovat täsmälleen sama rakenne. Xcodes näyttää nämä versiot automaattisesti yhdessä.";
+
+"ReleaseDate" = "Julkaisupäivä";
+"ReleaseNotes" = "Julkaisutiedot";
+"ReleaseNotes.help" = "Lue julkaisutiedot";
+"Compatibility" = "Yhteensopivuus";
+"MacOSRequirement" = "Vaatii macOS version %@ tai myöhemmän";
+"SDKs" = "SDKs";
+"Compilers" = "Kääntäjät";
+"DownloadSize" = "Latauskoko";
+"NoXcodeSelected" = "Xcode versiota ei ole valittuna";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Vaihe %1$d / %2$d: %3$@";
+"DownloadingPercentDescription" = "Lataa: %d%% valmis";
+"StopInstallation" = "Lopeta asennus";
+"DownloadingError" = "Lataustietoja ei löytynyt";
+
+// About
+"VersionWithBuild" = "Versio %@ (%@)";
+"GithubRepo" = "GitHub Repo";
+"Acknowledgements" = "Kunnianosoitukset";
+"UnxipExperiment" = "Unxip Kokeilu";
+"License" = "Lisenssi";
+
+// General Preference Pane
+"General" = "Yleisasetukset";
+"AppleID" = "Apple ID";
+"SignIn" = "Kirjaudu sisään";
+"Notifications" = "Ilmoitukset";
+
+// Updates Preference Pane
+"Updates" = "Päivitysasetukset";
+"Versions" = "Versiot";
+"AutomaticInstallNewVersion" = "Asenna uudet Xcode versiot automaattisesti";
+"IncludePreRelease" = "Sisällytä esijulkaisu/beta versiot";
+"AppUpdates" = "Xcodes.app päivitykset";
+"CheckForAppUpdates" = "Tarkista päivitykset automaattisesti";
+"CheckNow" = "Tarkista nyt";
+"LastChecked" = "Viimeksi tarkistettu: %@";
+"Never" = "Ei koskaan";
+
+// Download Preference Pane
+"Downloads" = "Lataukset";
+"DataSource" = "Tietolähde";
+"DataSourceDescription" = "Applen tietolähde kaappaa Apple Developer -sivuston. Se näyttää aina uusimmat saatavilla olevat julkaisut, mutta se on herkempi hajoamiselle.\n\nXcode Releases on epävirallinen luettelo Xcode-julkaisuista. Se toimitetaan hyvin muotoiltuina tietoina, sisältää lisätietoa, jota ei ole helposti saatavilla Applelta, ja se ei todennäköisesti hajoa, jos Apple suunnittelee uudelleen kehittäjäsivustonsa.";
+"Downloader" = "Downloader";
+"DownloaderDescription" = "aria2 käyttää jopa 16 yhteyttä ladatakseen Xcoden 3–5 kertaa nopeammin kuin URLSession. Se on niputettu suoritettavaksi tiedostoksi ja sen lähdekoodiin Xcodesissa GPLv2-lisenssin noudattamiseksi.\n\nURLSession on Applen oletussovellusliittymä URL-pyyntöjen tekemiseen..";
+
+// Advanced Preference Pane
+"Advanced" = "Lisäasetukset";
+"LocalCachePath" = "Paikallisen välimuistin polku";
+"LocalCachePathDescription" = "Xcodes tallentaa saatavilla olevat Xcode-versiot välimuistiin ja lataa tilapäisesti uudet versiot hakemistoon";
+"Change" = "Vaihda";
+"Active/Select" = "Aktiivinen/Valitse";
+"AutomaticallyCreateSymbolicLink" = "Luo automaattisesti symbolinen linkki Xcode.appiin";
+"AutomaticallyCreateSymbolicLinkDescription" = "Kun teet Xcode-versiosta aktiivisen/valitun, yritä luoda symbolinen linkki nimeltä Xcode.app asennushakemistoon";
+"PrivilegedHelper" = "Etuoikeutettu auttaja";
+"PrivilegedHelperDescription" = "Xcodes käyttää erillistä etuoikeutettua avustajaa tehtävien suorittamiseen pääkäyttäjänä. Nämä ovat asioita, jotka edellyttävät sudo komentoa komentorivillä, mukaan lukien asennuksen jälkeiset vaiheet ja Xcode-versioiden vaihtaminen xcode-selectillä.\n\nSinua pyydetään antamaan macOS-tilisi salasana sen asentamiseksi.";
+"HelperInstalled" = "Apulainen on asennettu";
+"HelperNotInstalled" = "Apulaista ei ole asennettu";
+"InstallHelper" = "Asenna apulainen";
+
+// Experiment Preference Pane
+"Experiments" = "Kokeilut";
+"FasterUnxip" = "Nopeampi Unxip";
+"UseUnxipExperiment" = "Kun purat xip-tiedoston, käytä kokeiluversiota";
+"FasterUnxipDescription" = "@_saagarjhan ansiosta tämä kokeilu voi nostaa purkamisnopeutta jopa 70 % joissakin järjestelmissä.\n\nLisätietoja siitä, miten tämä tehdään, on unxip-repossa - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Pääsy myönnetty. Saat ilmoituksia Xcodesilta.";
+"AccessDenied" = "⚠️ Pääsy kielletty ⚠️\n\nAvaa ilmoitusasetukset ja valitse Xcodes, jos haluat sallia pääsyn.";
+"NotificationSettings" = "Ilmoitusasetukset";
+"EnableNotifications" = "Salli ilmoitukset";
+
+// SignIn
+"SignInWithApple" = "Kirjaudu sisään Apple ID:lläsi.";
+"AppleID" = "AppleID:";
+"Password" = "Salasana:";
+"Required" = "Vaaditaan";
+"SignOut" = "Kirjaudu ulos";
+
+// SMS/2FA
+"DigitCodeDescription" = "Anna %d numeroinen koodi jostakin luotetusta laitteesta:";
+"SendSMS" = "Lähetä tekstiviesti";
+"EnterDigitCodeDescription" = "Anna %d numeroinen koodi, joka lähetettiin osoitteeseen %@: ";
+"SelectTrustedPhone" = "Valitse luotettu puhelinnumero saadaksesi %d numeroisen koodin tekstiviestinä:";
+"NoTrustedPhones" = "Tililläsi ei ole luotettuja puhelinnumeroita, mutta ne vaaditaan kaksivaiheiseen todentamiseen.\n\nKatso https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Päivitetty ajankohtana";
+
+// ToolBar
+"Login" = "Kirjaudu sisään";
+"LoginDescription" = "Avaa kirjautuminen";
+"Refresh" = "Päivitä";
+"RefreshDescription" = "Päivitä Xcode lista";
+"All" = "Kaikki";
+"Release" = "Julkaisuversio";
+"ReleaseOnly" = "Vain julkaisuversiot";
+"Beta" = "Beta";
+"BetaOnly" = "Vain beta versiot";
+"Filter" = "Suodata";
+"FilterAvailableDescription" = "Suodata saatavilla olevat versiot";
+"FilterInstalledDescription" = "Suodata asennetut versiot";
+"Info" = "Tietoja";
+"InfoDescription" = "Näytä tai piilota tietoruutu";
+"Preferences" = "Asetukset";
+"PreferencesDescription" = "Avaa asetukset";
+"Search" = "Etsi...";
+"SearchDescription" = "Hakulista";
+
+// List
+"ActiveVersionDescription" = "Tämä on aktiivinen versio";
+"MakeActiveVersionDescription" = "Tee tästä aktiivinen versio";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Poista Xcode %@?";
+"Alert.Uninstall.Message" = "Se siirretään roskakoriin, mutta sitä ei tyhjennetä.";
+"Alert.Uninstall.Error.Title" = "Xcode-asennuksen poistaminen ei onnistu";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Haluatko varmasti lopettaa Xcode versio %@ asennuksen?";
+"Alert.CancelInstall.Message" = "Kaikki edistyminen hylätään.";
+"Alert.CancelInstall.PrimaryButton" = "Lopeta asennus";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Etuoikeutettu auttaja";
+"Alert.PrivilegedHelper.Message" = "Xcodes käyttää erillistä etuoikeutettua avustajaa tehtävien suorittamiseen pääkäyttäjänä. Nämä ovat asioita, jotka edellyttävät sudo komentoa komentorivillä, mukaan lukien asennuksen jälkeiset vaiheet ja Xcode-versioiden vaihtaminen xcode-selectillä.\n\nSinua pyydetään antamaan macOS-tilisi salasana sen asentamista varten.";
+"Alert.PrivilegedHelper.Error.Title" = "Apuohjelmaa ei voi asentaa";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Vähimmäisvaatimukset eivät täyty";
+"Alert.MinSupported.Message" = "Xcode %@ vaatii macOS version %@, mutta käytät macOS versiota %@, haluatko silti asentaa sen?";
+
+// Install
+"Alert.Install.Error.Title" = "Xcoden asentaminen ei onnistu";
+"Alert.InstallArchive.Error.Title" = "Arkistoitua Xcodea ei voi asentaa";
+
+// Update
+"Alert.Update.Error.Title" = "Valittua Xcodea ei voi päivittää";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Xcodea ei voi valita";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Symbolista linkkiä ei voi luoda";
+"Alert.SymLink.Message" = "Xcode.app on olemassa, eikä se ole symbolinen linkki";
+
+// Post install
+"Alert.PostInstall.Title" = "Asennuksen jälkeisiä vaiheita ei voi suorittaa";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "Arkisto \"%@\" on vaurioitunut, eikä sitä voi laajentaa.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "Arkistoa \"%@\" ei voi laajentaa, koska nykyisessä taltiossa ei ole tarpeeksi vapaata tilaa.\n\nAnna lisää tilaa arkiston laajentamiseksi ja asenna sitten Xcode versio %@ uudelleen aloittaaksesi asennuksen siitä, mihin se jäi.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Xcoden siirtäminen hakemistoon %@ epäonnistui.";
+"InstallationError.FailedSecurityAssessment" = "Xcode versio %@ epäonnistui turvallisuusarvioinnissaan seuraavalla lähdöllä:\n%@\nSe pysyy asennettuna osoitteeseen %@, jos haluat käyttää sitä joka tapauksessa.";
+"InstallationError.CodesignVerifyFailed" = "Ladattu Xcode versio epäonnistui koodin allekirjoitusvahvistuksessa seuraavalla lähdöllä:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "Ladatulla Xcodella ei ole odotettua koodin allekirjoitusidentiteettiä.\nSaatu:\n%@\n%@\nOletettu:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes ei (vielä) tue Xcoden asentamista %@ tiedostomuodosta.";
+"InstallationError.MissingSudoerPassword" = "Salasana puuttuu. Yritä uudelleen.";
+"InstallationError.UnavailableVersion" = "Versiota %@ ei löytynyt.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Julkaisua edeltäviä versioita ei ole saatavilla.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Esijulkaisuversioita ei saatavilla.";
+"InstallationError.MissingUsernameOrPassword" = "Käyttäjätunnus tai salasana puuttuu. Yritä uudelleen.";
+"InstallationError.VersionAlreadyInstalled" = "%@ on jo asennettu osoitteeseen %@";
+"InstallationError.InvalidVersion" = "%@ ei ole kelvollinen versionumero.";
+"InstallationError.VersionNotInstalled" = "%@ ei ole asennettu.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "Asennus valmistui, mutta joitain asennuksen jälkeisiä vaiheita ei suoritettu automaattisesti. Nämä suoritetaan, kun käynnistät Xcoden version %@.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "Asennus valmistui, mutta joitain asennuksen jälkeisiä vaiheita ei suoritettu automaattisesti. Xcodes suorittaa nämä vaiheet etuoikeutetulla avustajalla, jota ei näytä olevan asennettuna. Voit asentaa sen kohdasta Asetukset > Lisäasetukset.\n\nNämä vaiheet suoritetaan, kun käynnistät Xcode version %@ ensimmäisen kerran..";
+
+// Installation Steps
+"Downloading" = "Ladataan";
+"Unarchiving" = "Poistetaan arkistointi (tämä voi kestää hetken)";
+"Moving" = "Siirretään kohteeseen %@";
+"TrashingArchive" = "Siirretään arkistoa roskakoriin";
+"CheckingSecurity" = "Turvallisuustarkastus";
+"Finishing" = "Viimeistellään";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Uusi versio on saatavilla";
+"Notification.FinishedInstalling" = "Asennus valmis";
+
+
+"HelperClient.error" = "Ei pysty kommunikoimaan etuoikeutetun avustajan kanssa.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Uudet Xcode-versiot";
+"Notification.NewXcodeVersion.Body" = "Uusia Xcode-versioita on ladattavissa.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Hyvää WWDC tapahtumaa %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/fr.lproj/Localizable.strings b/Xcodes/Resources/fr.lproj/Localizable.strings
new file mode 100644
index 0000000..0e619ef
--- /dev/null
+++ b/Xcodes/Resources/fr.lproj/Localizable.strings
@@ -0,0 +1,230 @@
+// Menu
+"Menu.About" = "À propos de Xcodes";
+"Menu.CheckForUpdates" = "Vérifier les mises à jour...";
+"Menu.Acknowledgements" = "Remerciements Xcodes";
+"Menu.GitHubRepo" = "Dépôt GitHub Xcodes";
+"Menu.ReportABug" = "Signaler un Bogue";
+"Menu.RequestNewFeature" = "Demander une Nouvelle Fonctionnalité";
+
+// Common
+"Install" = "Installer";
+"InstallDescription" = "Installer cette version";
+"RevealInFinder" = "Ouvrir dans le Finder";
+"CopyReleaseNoteURL" = "Copier l'URL";
+"Active" = "Version Active";
+"MakeActive" = "Activer";
+"Open" = "Ouvrir";
+"OpenDescription" = "Ouvrir cette version";
+"CopyPath" = "Copier le chemin d'accès";
+"CreateSymLink" = "Créer un Symlink pour Xcode.app";
+"CreateSymLink" = "Créer un Symlink pour Xcode-Beta.app";
+"Uninstall" = "Désinstaller";
+"Selected" = "Sélectionné";
+"Select" = "Sélectionner";
+"Cancel" = "Annuler";
+"Next" = "Suivant";
+"Continue" = "Continuer";
+"Close" = "Fermer";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Versions identiques";
+"IdenticalBuilds.help" = "Parfois, la version préliminaire et la version finale sont exactement la même version. Xcodes affichera automatiquement ces versions ensemble.";
+
+"ReleaseDate" = "Date de Sortie";
+"ReleaseNotes" = "Notes de Mise à Jour";
+"ReleaseNotes.help" = "Consulter les Notes de Mise á Jour";
+"Compatibility" = "Compatibilité";
+"MacOSRequirement" = "Nécessite macOS %@ ou une version ultérieure";
+"SDKs" = "SDKs";
+"Compilers" = "Compilateurs";
+"DownloadSize" = "Taille de téléchargement";
+"NoXcodeSelected" = "Aucun Xcode Sélectionné";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Étape %1$d sur %2$d : %3$@";
+"DownloadingPercentDescription" = "Téléchargement : %d%% téléchargé";
+"StopInstallation" = "Arrêter l'installation";
+"DownloadingError" = "Aucune information de téléchargement trouvée";
+
+// About
+"VersionWithBuild" = "Version %@ (%@)";
+"GithubRepo" = "Dépôt GitHub";
+"Acknowledgements" = "Remerciements";
+"UnxipExperiment" = "Expérimentation Unxip";
+"License" = "Licence";
+
+// General Preference Pane
+"General" = "Général";
+"AppleID" = "Identifiant Apple";
+"SignIn" = "Connecter";
+"Notifications" = "Notifications";
+
+// Updates Preference Pane
+"Updates" = "Mise à jour";
+"Versions" = "Versions";
+"AutomaticInstallNewVersion" = "Installer automatiquement les nouvelles versions de Xcode";
+"IncludePreRelease" = "Inclure les versions préliminaires/bêta";
+"AppUpdates" = "Mises à Jour de Xcodes.app";
+"CheckForAppUpdates" = "Vérifier automatiquement les mises à jour de l'application";
+"CheckNow" = "Vérifier Maintenant";
+"LastChecked" = "Dernière vérification : %@";
+"Never" = "Jamais";
+
+// Download Preference Pane
+"Downloads" = "Téléchargements";
+"DataSource" = "Source de Données";
+"DataSourceDescription" = "La source de données Apple analyse le site Web de développeurs d'Apple. Elle contient les dernières versions disponibles, mais est plus fragile.\n\nXcode Releases est une liste non officielle des versions de Xcode. Elle contient des informations supplémentaires qui ne sont pas facilement disponibles auprès d'Apple et est moins susceptible de se briser si Apple refait son site Web de développeurs.";
+"Downloader" = "Téléchargeur";
+"DownloaderDescription" = "aria2 utilise jusqu'à 16 connexions pour télécharger Xcode de 3 à 5 fois plus rapidement que URLSession. aria2 est fourni sous forme d'exécutable avec son code source dans Xcodes pour se conformer à sa licence GPLv2.\n\nURLSession est l'API d'Apple utilisée par défaut pour effectuer des requêtes d'URL.";
+
+// Advanced Preference Pane
+"Advanced" = "Avancé";
+"LocalCachePath" = "Cache Local";
+"LocalCachePathDescription" = "Xcodes met en cache les versions de Xcode disponibles et télécharge temporairement les nouvelles versions dans le répertoire de cache local";
+"Change" = "Changer";
+"Active/Select" = "Activer/Sélectionner";
+"AutomaticallyCreateSymbolicLink" = "Créer automatiquement un lien symbolique vers Xcode.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "Lorsque vous activez/sélectionnez une version de Xcode, essayez de créer un lien symbolique nommé Xcode.app dans le répertoire d'installation";
+"OnSelectDoNothing" = "Ne rien faire";
+"OnSelectRenameXcode" = "Toujours renommer en Xcode.app";
+"OnSelectRenameXcodeDescription" = "À la sélection, toujours essayer de renommer la version active de Xcode en Xcode.app, en renommant l'ancienne Xcode.app avec le nom de version.";
+
+"PrivilegedHelper" = "Assistant Privilégié";
+"PrivilegedHelperDescription" = "Xcodes utilise un assistant privilégié distinct pour effectuer des tâches en tant que root. Ce sont des tâches qui nécessiteraient sudo sur la ligne de commande, y compris les étapes de post-installation et le changement de version de Xcode avec xcode-select.\n\nVous serez invité à saisir le mot de passe de votre compte macOS pour l'installer.";
+"HelperInstalled" = "L'assistant est installé";
+"HelperNotInstalled" = "L'assistant n'est pas installé";
+"InstallHelper" = "Installer l'assistant'";
+
+// Experiment Preference Pane
+"Experiments" = "Expérimentations";
+"FasterUnxip" = "Unxip Plus Rapide";
+"UseUnxipExperiment" = "Lors de la décompression Unxip, utiliser l'expérimentation";
+"FasterUnxipDescription" = "Grâce à @_saagarjha, cette expérimentation peut augmenter jusqu'à 70% la vitesse de décompression pour certains systèmes.\n\nPour plus d'informations sur la façon dont cela est accompli, consultez le dépôt GitHub unxip - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Accès autorisé. Vous recevrez des notifications de Xcodes.";
+"AccessDenied" = "⚠️ Accès refusé ⚠️\n\nVeuillez ouvrir vos paramètres de notifications et sélectionnez Xcodes si vous souhaitez autoriser l'accès.";
+"NotificationSettings" = "Paramètres de Notification";
+"EnableNotifications" = "Activer les Notifications";
+
+// SignIn
+"SignInWithApple" = "Connectez-vous avec votre identifiant Apple.";
+"AppleID" = "Identifiant Apple :";
+"Password" = "Mot de passe :";
+"Required" = "Requis";
+"SignOut" = "Déconnecter";
+
+// SMS/2FA
+"DigitCodeDescription" = "Saisissez le code à %d chiffres de l'un de vos appareils de confiance :";
+"SendSMS" = "Envoyer un SMS";
+"EnterDigitCodeDescription" = "Entrez le code à %d chiffres envoyé à %@ : ";
+"SelectTrustedPhone" = "Sélectionnez le numéro de téléphone de confiance pour recevoir un code à %d chiffres par SMS :";
+"NoTrustedPhones" = "Votre compte n'a aucun numéro de téléphone de confiance, mais ils sont requis pour l'authentification à deux facteurs.\n\nVoir https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Mis à jour le ";
+
+// ToolBar
+"Login" = "Connexion";
+"LoginDescription" = "Ouvrir une connexion";
+"Refresh" = "Actualiser";
+"RefreshDescription" = "Actualiser la liste des Xcode";
+"All" = "Tout";
+"Release" = "Version";
+"ReleaseOnly" = "Seulement le versions finales";
+"Beta" = "Beta";
+"BetaOnly" = "Seulement les versions bêtas";
+"Filter" = "Filtre";
+"FilterAvailableDescription" = "Filtrer les versions disponibles";
+"FilterInstalledDescription" = "Filtrer les versions installées";
+"Info" = "Informations";
+"InfoDescription" = "Afficher ou masquer le volet d'informations";
+"Preferences" = "Préférences";
+"PreferencesDescription" = "Ouvrir les Préférences";
+"Search" = "Rechercher...";
+"SearchDescription" = "Liste de recherche";
+
+// List
+"ActiveVersionDescription" = "Ceci est la version active";
+"MakeActiveVersionDescription" = "Activer";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Désinstaller Xcode %@ ?";
+"Alert.Uninstall.Message" = "Xcodes sera déplacé vers la corbeille, mais la corbeille ne sera pas vidée.";
+"Alert.Uninstall.Error.Title" = "Impossible de désinstaller Xcode";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Êtes-vous sûr de vouloir arrêter l'installation de Xcode %@ ?";
+"Alert.CancelInstall.Message" = "Toute progression sera supprimée.";
+"Alert.CancelInstall.PrimaryButton" = "Arrêter l'installation";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Assistant Privilégié";
+"Alert.PrivilegedHelper.Message" = "Xcodes utilise un assistant privilégié distinct pour effectuer des tâches en tant que root. Ce sont des tâches qui nécessiteraient sudo sur la ligne de commande, y compris les étapes de post-installation et le changement de version de Xcode avec xcode-select.\n\nVous serez invité à saisir le mot de passe de votre compte macOS pour l'installer.";
+"Alert.PrivilegedHelper.Error.Title" = "Impossible d'installer l'assistant";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Pré-requis minimum non satisfaits";
+"Alert.MinSupported.Message" = "Xcode %@ nécessite MacOS %@, mais vous utilisez MacOS %@, voulez-vous toujours l'installer ?";
+
+// Install
+"Alert.Install.Error.Title" = "Impossible d'installer Xcode";
+"Alert.InstallArchive.Error.Title" = "Impossible d'installer l'archive de Xcode";
+
+// Update
+"Alert.Update.Error.Title" = "Impossible de mettre à jour la version de Xcode sélectionnée";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Impossible de sélectionner Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Impossible de créer un lien symbolique";
+"Alert.SymLink.Message" = "Xcode.app existe et n'est pas un lien symbolique";
+
+// Post install
+"Alert.PostInstall.Title" = "Impossible d'effectuer les étapes de post-installation";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "L'archive \"%@\" est endommagée et ne peut pas être décompressée.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "L'archive \"%@\" ne peut pas être décompressée car le volume actuel n'a pas assez d'espace libre.\n\nLibérez plus d'espace pour décompresser l'archive, puis réinstallez Xcode %@ pour démarrer l'installation là où elle s'est arrêtée.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Impossible de déplacer Xcode vers le répertoire %@.";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ a échoué à son évaluation de sécurité avec le résultat suivant :\n%@\nXcode reste installé à %@ si vous souhaitez l'utiliser quand même.";
+"InstallationError.CodesignVerifyFailed" = "La vérification de signature de code du Xcode téléchargé a échoué avec le résultat suivant :\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "Le Xcode téléchargé n'a pas l'identité de signature de code attendue.\nReçue :\n%@\n%@\nAttendue :\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes ne supporte pas (encore) l'installation de Xcode à partir du format de fichier %@.";
+"InstallationError.MissingSudoerPassword" = "Mot de passe manquant. Veuillez réessayer.";
+"InstallationError.UnavailableVersion" = "Impossible de trouver la version %@.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Aucune version non préliminaire disponible.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Aucune version préliminaire disponible.";
+"InstallationError.MissingUsernameOrPassword" = "Nom d'utilisateur ou mot de passe manquant. Veuillez réessayer.";
+"InstallationError.VersionAlreadyInstalled" = "%@ est déjà installé à %@";
+"InstallationError.InvalidVersion" = "%@ n'est pas un numéro de version valide.";
+"InstallationError.VersionNotInstalled" = "%@ n'est pas installé.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "L'installation est terminée, mais certaines étapes de post-installation n'ont pas été effectuées automatiquement. Celles-ci seront exécutées lors du premier lancement de Xcode %@.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "L'installation est terminée, mais certaines étapes de post-installation n'ont pas été effectuées automatiquement. Xcodes effectue ces étapes avec un assistant privilégié, qui ne semble pas être installé. Vous pouvez l'installer depuis Préférences > Avancé.\n\nCes étapes seront effectuées lors du premier lancement de Xcode %@.";
+
+// Installation Steps
+"Downloading" = "Téléchargement";
+"Unarchiving" = "Décompression (cela peut prendre un certain temps)";
+"Moving" = "Déplacement vers %@";
+"TrashingArchive" = "Déplacement de l'archive vers la corbeille";
+"CheckingSecurity" = "Vérification de sécurité";
+"Finishing" = "Finition";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Une nouvelle version est disponible";
+"Notification.FinishedInstalling" = "Installation terminée";
+
+
+"HelperClient.error" = "Impossible de communiquer avec l'assistant privilégié.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Nouvelles versions de Xcode";
+"Notification.NewXcodeVersion.Body" = "De nouvelles versions de Xcode sont disponibles au téléchargement.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/hi.lproj/Localizable.strings b/Xcodes/Resources/hi.lproj/Localizable.strings
new file mode 100644
index 0000000..22a672d
--- /dev/null
+++ b/Xcodes/Resources/hi.lproj/Localizable.strings
@@ -0,0 +1,225 @@
+// Menu
+"Menu.About" = "एक्सकोड के बारे में";
+"Menu.CheckForUpdates" = "अद्यतनों के लिए जाँच करें...";
+"Menu.Acknowledgements" = "Xcodes स्वीकृति";
+"Menu.GitHubRepo" = "Xcodes GitHub रेपो";
+"Menu.ReportABug" = "किसी बग की रिपोर्ट करें";
+"Menu.RequestNewFeature" = "किसी नई सुविधा का अनुरोध करें";
+
+// Common
+"Install" = "स्थापित करे";
+"InstallDescription" = "इस संस्करण को स्थापित करें";
+"RevealInFinder" = "खोजक में प्रकट करें";
+"Active" = "सक्रिय";
+"MakeActive" = "सक्रिय बनाएँ";
+"Open" = "खोलना";
+"OpenDescription" = "इस संस्करण को खोलें";
+"CopyPath" = "पथ की कॉपी करे";
+"CreateSymLink" = "Xcode.app के रूप में सिमलिंक बनाएं";
+"CreateSymLinkBeta" = "Xcode-Beta.app के रूप में सिमलिंक बनाएं";
+"Uninstall" = "असंस्थापित करे";
+"Selected" = "चयनित";
+"Select" = "चयन करे";
+"Cancel" = "रद्द करे";
+"Next" = "अगला";
+"Continue" = "जारी रखें";
+"Close" = "बंद करे";
+
+// Info Pane
+"IdenticalBuilds" = "Identical Builds";
+"IdenticalBuilds.help" = "कभी-कभी एक पूर्व-रिलीज़ और रिलीज़ संस्करण एक ही बिल्ड होते हैं। Xcodes स्वचालित रूप से इन संस्करणों को एक साथ प्रदर्शित करेगा।";
+
+"ReleaseDate" = "रिलीज़ दिनांक";
+"ReleaseNotes" = "रिलीज नोट्स";
+"ReleaseNotes.help" = "रिलीज़ नोट्स देखें";
+"CopyReleaseNoteURL" = "URL कॉपी करें";
+"Compatibility" = "अनुकूलता";
+"MacOSRequirement" = "macOS %@ या बाद के संस्करण की आवश्यकता है";
+"SDKs" = "SDKs";
+"Compilers" = "संकलनकर्ता";
+"DownloadSize" = "डाउनलोड आकार";
+"NoXcodeSelected" = "कोई Xcode चयनित नहीं";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "%2$d का स्टेप %1$d: %3$@";
+"DownloadingPercentDescription" = "डाउनलोड: %d%% पूर्ण";
+"StopInstallation" = "स्थापना रोकें";
+"DownloadingError" = "कोई डाउनलोड जानकारी नहीं मिली";
+
+// About
+"VersionWithBuild" = "संस्करण %@ (%@)";
+"GithubRepo" = "GitHub रेपो";
+"Acknowledgements" = "स्वीकृति";
+"UnxipExperiment" = "अनज़िप प्रयोग";
+"License" = "लाइसेंस";
+
+// General Preference Pane
+"General" = "सामान्य";
+"AppleID" = "Apple ID";
+"SignIn" = "साइन इन करें";
+"Notifications" = "सूचनाएँ";
+
+// Updates Preference Pane
+"Updates" = "अपडेट";
+"Versions" = "संस्करण";
+"AutomaticInstallNewVersion" = "Xcode के नए संस्करण स्वचालित रूप से स्थापित करें";
+"IncludePreRelease" = "पूर्व-रिलीज़/बीटा संस्करण शामिल करें";
+"AppUpdates" = "Xcodes.app अपडेट";
+"CheckForAppUpdates" = "ऐप अपडेट के लिए स्वचालित रूप से जांचें";
+"CheckNow" = "अभी जाँचें";
+"LastChecked" = "पिछली बार चेक किया गया: %@";
+"Never" = "कभी नहीं";
+
+// Download Preference Pane
+"Downloads" = "डाउनलोड";
+"DataSource" = "डेटा स्रोत";
+"DataSourceDescription" = "Apple डेटा स्रोत Apple डेवलपर वेबसाइट को स्क्रैप करता है। यह हमेशा नवीनतम रिलीज दिखाएगा जो उपलब्ध हैं, लेकिन अधिक नाजुक है।\n\nXcode Releases Xcode रिलीज़ की एक अनौपचारिक सूची है। यह सुव्यवस्थित डेटा के रूप में प्रदान किया जाता है, इसमें अतिरिक्त जानकारी होती है जो Apple से आसानी से उपलब्ध नहीं होती है, और यदि Apple अपनी डेवलपर वेबसाइट को फिर से डिज़ाइन करता है तो इसके टूटने की संभावना कम होती है।";
+"Downloader" = "डाउनलोडर";
+"DownloaderDescription" = "aria2 URLSession की तुलना में Xcode 3-5x तेजी से डाउनलोड करने के लिए 16 कनेक्शन तक का उपयोग करता है। इसे अपने GPLv2 लाइसेंस का अनुपालन करने के लिए Xcodes के भीतर अपने स्रोत कोड के साथ एक निष्पादन योग्य के रूप में बंडल किया गया है।\n\nURL अनुरोध करने के लिए URLSession डिफ़ॉल्ट Apple API है।";
+
+// Advanced Preference Pane
+"Advanced" = "उन्नत";
+"LocalCachePath" = "स्थानीय कैश पथ";
+"LocalCachePathDescription" = "Xcodes उपलब्ध Xcode संस्करणों को कैश करता है और एक निर्देशिका में अस्थायी रूप से नए संस्करण डाउनलोड करता है";
+"Change" = "परिवर्तन";
+"Active/Select" = "सक्रिय/चयन";
+"AutomaticallyCreateSymbolicLink" = "Xcode.app के लिए स्वचालित रूप से प्रतीकात्मक लिंक बनाएं";
+"AutomaticallyCreateSymbolicLinkDescription" = "Xcode संस्करण को सक्रिय/चयनित बनाते समय, स्थापना निर्देशिका में Xcode.app नामक एक प्रतीकात्मक लिंक बनाने का प्रयास करें";
+"PrivilegedHelper" = "विशेषाधिकार प्राप्त सहायक";
+"PrivilegedHelperDescription" = "Xcodes कार्यों को रूट के रूप में करने के लिए एक अलग विशेषाधिकार प्राप्त सहायक का उपयोग करता है। ये ऐसी चीजें हैं जिनके लिए कमांड लाइन पर sudo की आवश्यकता होगी, जिसमें पोस्ट-इंस्टॉल चरण और xcode-select के साथ Xcode संस्करण स्विच करना शामिल है।\n\nइसे इंस्टॉल करने के लिए आपको अपने macOS अकाउंट पासवर्ड के लिए कहा जाएगा।";
+"HelperInstalled" = "सहायक स्थापित है";
+"HelperNotInstalled" = "सहायक स्थापित नहीं है";
+"InstallHelper" = "सहायक स्थापित करें";
+
+// Experiment Preference Pane
+"Experiments" = "परीक्षण";
+"FasterUnxip" = "तेज़ अनज़िप";
+"UseUnxipExperiment" = "अनज़िपिंग करते समय, प्रयोग का उपयोग करें";
+"FasterUnxipDescription" = "@_saagarjha के लिए धन्यवाद, यह प्रयोग कुछ प्रणालियों के लिए 70% तक की गति को बढ़ा सकता है।\n\nइसे कैसे पूरा किया जाता है, इस बारे में अधिक जानकारी अनएक्सआईपी रेपो पर देखी जा सकती है - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "प्रवेश करने की अनुमति है। आपको Xcodes से सूचनाएं प्राप्त होंगी।";
+"AccessDenied" = "⚠️ प्रवेश निषेध ⚠️\n\nकृपया अपनी सूचना सेटिंग्स खोलें और यदि आप प्रवेश की अनुमति देना चाहते हैं तो Xcodes का चयन करें.";
+"NotificationSettings" = "सूचना सेटिंग्स";
+"EnableNotifications" = "सूचनाएँ सक्षम करें";
+
+// SignIn
+"SignInWithApple" = "अपनी Apple ID से साइन इन करें.";
+"AppleID" = "AppleID:";
+"Password" = "पासवर्ड:";
+"Required" = "आवश्यक";
+"SignOut" = "साइन आउट करें";
+
+// SMS/2FA
+"DigitCodeDescription" = "अपने किसी विश्वसनीय डिवाइस से %d अंक कोड दर्ज करें:";
+"SendSMS" = "SMS भेजें";
+"EnterDigitCodeDescription" = "%@ को भेजा गया %d अंक कोड दर्ज करें: ";
+"SelectTrustedPhone" = "SMS के द्वारा %d अंक कोड प्राप्त करने के लिए विश्वसनीय फ़ोन नंबर चुनें:";
+"NoTrustedPhones" = "आपके खाते में कोई विश्वसनीय फ़ोन नंबर नहीं है, लेकिन वे दो-कारक प्रमाणीकरण के लिए आवश्यक हैं।\n\nhttps://support.apple.com/HT204915 देखें।";
+
+// MainWindow
+"UpdatedAt" = "पर अपडेट किया गया";
+
+// ToolBar
+"Login" = "लॉग इन करें";
+"LoginDescription" = "लॉगिन खोलें";
+"Refresh" = "ताज़ा करें";
+"RefreshDescription" = "Xcode सूची ताज़ा करें";
+"All" = "सभी";
+"Release" = "रिलीज़";
+"ReleaseOnly" = "केवल रिलीज़ करें";
+"Beta" = "बीटा";
+"BetaOnly" = "केवल बीटा";
+"Filter" = "फ़िल्टर";
+"FilterAvailableDescription" = "उपलब्ध संस्करणों को फ़िल्टर करें";
+"FilterInstalledDescription" = "स्थापित संस्करणों को फ़िल्टर करें";
+"Info" = "जानकारी";
+"InfoDescription" = "जानकारी फलक दिखाएँ या छिपाएँ";
+"Preferences" = "प्राथमिकताएँ";
+"PreferencesDescription" = "प्राथमिकताएँ खोलें";
+"Search" = "खोज...";
+"SearchDescription" = "खोज सूची";
+
+// List
+"ActiveVersionDescription" = "यह सक्रिय संस्करण है";
+"MakeActiveVersionDescription" = "इसे सक्रिय संस्करण बनाएं";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Xcode %@ की स्थापना रद्द करें?";
+"Alert.Uninstall.Message" = "इसे ट्रैश में ले जाया जाएगा, लेकिन खाली नहीं किया जाएगा।";
+"Alert.Uninstall.Error.Title" = "Xcode की स्थापना रद्द करने में असमर्थ";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "क्या आप वाकई Xcode %@ की स्थापना को रोकना चाहते हैं?";
+"Alert.CancelInstall.Message" = "किसी भी प्रगति को छोड़ दिया जाएगा।";
+"Alert.CancelInstall.PrimaryButton" = "स्थापना रोकें";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "विशेषाधिकार प्राप्त सहायक";
+"Alert.PrivilegedHelper.Message" = "Xcodes कार्यों को रूट के रूप में करने के लिए एक अलग विशेषाधिकार प्राप्त सहायक का उपयोग करता है। ये ऐसी चीजें हैं जिनके लिए कमांड लाइन पर sudo की आवश्यकता होगी, जिसमें पोस्ट-इंस्टॉल चरण और xcode-select के साथ Xcode संस्करण स्विच करना शामिल है।\n\nआपको इसे स्थापित करने के लिए अपने macOS खाता पासवर्ड के लिए संकेत दिया जाएगा।";
+"Alert.PrivilegedHelper.Error.Title" = "हेल्पर स्थापित करने में असमर्थ";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "न्यूनतम आवश्यकताएं पूरी नहीं हुई";
+"Alert.MinSupported.Message" = "Xcode %@ को MacOS %@ की आवश्यकता है, लेकिन आप MacOS %@ चला रहे हैं, क्या आप अभी भी इसे स्थापित करना चाहते हैं?";
+
+// Install
+"Alert.Install.Error.Title" = "एक्सकोड स्थापित करने में असमर्थ";
+"Alert.InstallArchive.Error.Title" = "संग्रहीत Xcode स्थापित करने में असमर्थ";
+
+// Update
+"Alert.Update.Error.Title" = "चयनित Xcode को अपडेट करने में असमर्थ";
+
+// Active/Select
+"Alert.Select.Error.Title" = "एक्सकोड का चयन करने में असमर्थ";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "प्रतीकात्मक लिंक बनाने में असमर्थ";
+"Alert.SymLink.Message" = "Xcode.app मौजूद है और एक प्रतीकात्मक लिंक नहीं है";
+
+// Post install
+"Alert.PostInstall.Title" = "स्थापना के बाद के चरण निष्पादित करने में असमर्थ";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "संग्रह \"%@\" क्षतिग्रस्त है और इसे विस्तारित नहीं किया जा सकता है।";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "संग्रह \"%@\" का विस्तार नहीं किया जा सकता क्योंकि वर्तमान वॉल्यूम में पर्याप्त रिक्त स्थान नहीं है.\n\nसंग्रह का विस्तार करने के लिए अधिक स्थान उपलब्ध कराएं और फिर Xcode %@ को फिर से स्थापित करें ताकि स्थापना वहीं से शुरू की जा सके जहां से इसे छोड़ा था।";
+"InstallationError.FailedToMoveXcodeToApplications" = "Xcode को %@ निर्देशिका में ले जाने में विफल।";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ निम्नलिखित आउटपुट के साथ अपने सुरक्षा मूल्यांकन में विफल रहा:\n%@\nयदि आप किसी भी तरह इसका उपयोग करना चाहते हैं तो यह %@ पर स्थापित रहता है।";
+"InstallationError.CodesignVerifyFailed" = "डाउनलोड किया गया Xcode निम्नलिखित आउटपुट के साथ कोड हस्ताक्षर सत्यापन में विफल रहा:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "डाउनलोड किए गए Xcode में अपेक्षित कोड हस्ताक्षर पहचान नहीं है।\nमिला:\n%@\n%@\अपेक्षित:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes (अभी तक) % @ फ़ाइल स्वरूप से Xcode स्थापित करने का समर्थन नहीं करता है।";
+"InstallationError.MissingSudoerPassword" = "पासवर्ड अनुपलब्ध है। कृपया पुन: प्रयास करें।";
+"InstallationError.UnavailableVersion" = "संस्करण %@ नहीं मिल सका।";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "कोई गैर-पूर्व-रिलीज़ संस्करण उपलब्ध नहीं है।";
+"InstallationError.NoPrereleaseVersionAvailable" = "कोई पूर्व-रिलीज़ संस्करण उपलब्ध नहीं है।";
+"InstallationError.MissingUsernameOrPassword" = "उपयोगकर्ता नाम या पासवर्ड गुम है। कृपया पुन: प्रयास करें।";
+"InstallationError.VersionAlreadyInstalled" = "%@ पहले से ही %@ पर संस्थापित है";
+"InstallationError.InvalidVersion" = "%@ मान्य संस्करण संख्या नहीं है।";
+"InstallationError.VersionNotInstalled" = "%@ स्थापित नहीं किया गया।";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "स्थापना पूर्ण हो गई थी, लेकिन कुछ पोस्ट-इंस्टॉलेशन चरण स्वचालित रूप से निष्पादित नहीं किए गए थे। ये तब किए जाएंगे जब आप पहली बार Xcode %@ लॉन्च करेंगे।";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "स्थापना पूर्ण हो गई थी, लेकिन कुछ पोस्ट-इंस्टॉलेशन चरण स्वचालित रूप से निष्पादित नहीं किए गए थे। Xcodes इन चरणों को एक विशेषाधिकार प्राप्त सहायक के साथ निष्पादित करता है, जो स्थापित नहीं प्रतीत होता है। आप इसे Preferences > Advanced(वरीयताएँ> उन्नत) से स्थापित कर सकते हैं।\n\nजब आप पहली बार Xcode %@ लॉन्च करेंगे तो ये चरण निष्पादित किए जाएंगे।";
+
+// Installation Steps
+"Downloading" = "डाउनलोड";
+"Unarchiving" = "Unarchiving (इसमें कुछ समय लग सकता है)";
+"Moving" = "%@ पर जा रहा है";
+"TrashingArchive" = "संग्रह को ट्रैश में ले जाया जा रहा है";
+"CheckingSecurity" = "सुरक्षा सत्यापन";
+"Finishing" = "परिष्करण";
+
+// Notifications
+"Notification.NewVersionAvailable" = "नया संस्करण उपलब्ध है";
+"Notification.FinishedInstalling" = "स्थापना समाप्त हो गई";
+
+
+"HelperClient.error" = "विशेषाधिकार प्राप्त सहायक के साथ संवाद करने में असमर्थ।";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "नए Xcode संस्करण";
+"Notification.NewXcodeVersion.Body" = "नए Xcode संस्करण डाउनलोड करने के लिए उपलब्ध हैं।";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/it.lproj/Localizable.strings b/Xcodes/Resources/it.lproj/Localizable.strings
new file mode 100644
index 0000000..e8af7a2
--- /dev/null
+++ b/Xcodes/Resources/it.lproj/Localizable.strings
@@ -0,0 +1,226 @@
+// Menu
+"Menu.About" = "Informazioni di Xcodes";
+"Menu.CheckForUpdates" = "Controllo aggiornamenti...";
+"Menu.Acknowledgements" = "Ringraziamenti di Xcodes";
+"Menu.GitHubRepo" = "Xcodes Repository di Github";
+"Menu.ReportABug" = "Segnala un Bug";
+"Menu.RequestNewFeature" = "Richiedi una nuova Feature";
+
+// Common
+"Install" = "Installa";
+"InstallDescription" = "Installa questa versione";
+"RevealInFinder" = "Mostra in Finder";
+"Active" = "Attiva";
+"MakeActive" = "Rendi attiva";
+"Open" = "Apri";
+"OpenDescription" = "Apri questa versione";
+"CopyPath" = "Copia Percorso";
+"CreateSymLink" = "Crea Symlink come Xcode.app";
+"CreateSymLinkBeta" = "Crea Symlink come Xcode-Beta.app";
+"Uninstall" = "Disinstalla";
+"Selected" = "Selezionato";
+"Select" = "Seleziona";
+"Cancel" = "Annulla";
+"Next" = "Avanti";
+"Continue" = "Continua";
+"Close" = "Chiudi";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Build Identiche";
+"IdenticalBuilds.help" = "A volte una versione di prerelease e release sono esattamente la stessa build. Xcodes mostrerà automatiamente queste versioni assieme.";
+
+"ReleaseDate" = "Data di Rilascio";
+"ReleaseNotes" = "Note di Rilascio";
+"ReleaseNotes.help" = "Vedi Note di Rilascio";
+"CopyReleaseNoteURL" = "Copia URL";
+"Compatibility" = "Compatibilità";
+"MacOSRequirement" = "Richiede macOS %@ o successivo";
+"SDKs" = "SDKs";
+"Compilers" = "Compilatore";
+"DownloadSize" = "Dimensione Download";
+"NoXcodeSelected" = "Nessun Xcode selezionato";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Passo %1$d di %2$d: %3$@";
+"DownloadingPercentDescription" = "Download: %d%% completato";
+"StopInstallation" = "Ferma istallazione";
+"DownloadingError" = "Non trovo informazioni sul download";
+
+// About
+"VersionWithBuild" = "Versione %@ (%@)";
+"GithubRepo" = "GitHub Repo";
+"Acknowledgements" = "Ringraziamenti";
+"UnxipExperiment" = "Unxip Sperimentale";
+"License" = "Licenza";
+
+// General Preference Pane
+"General" = "Generale";
+"AppleID" = "Apple ID";
+"SignIn" = "Entra";
+"Notifications" = "Notifiche";
+
+// Updates Preference Pane
+"Updates" = "Aggiornamenti";
+"Versions" = "Versioni";
+"AutomaticInstallNewVersion" = "Installa automaticamente nuove versioni di Xcode";
+"IncludePreRelease" = "Includi versioni prerelease/beta";
+"AppUpdates" = "Xcodes.app Aggiornamenti";
+"CheckForAppUpdates" = "Controlla automaticamente per aggiornamenti";
+"CheckNow" = "Controlla Ora";
+"LastChecked" = "Ultimo controllo: %@";
+"Never" = "Mai";
+
+// Download Preference Pane
+"Downloads" = "Downloads";
+"DataSource" = "Sorgente Dati";
+"DataSourceDescription" = "La sorgente dati Apple controlla il sito sviluppatori Apple. Mostra sempre le ultime versioni disponibili, ma è più fragile. Xcode Releases è una lista non ufficiale di versioni di Xcode. E' disponibile con un formato ben strutturato, contiene più in formazioni che non vengono rilasciate da Apple ed è più difficile che smetta di funzionare se Apple ristruttura il suo sito sviluppatori.";
+"Downloader" = "Scaricatore";
+"DownloaderDescription" = "aria2 usa fino a 16 connessioni per scaricar Xcode da 3 a 5 volte più veloce di URLSession. E' incluso come eseguibile assieme ai suoi sogenti dentro Xcodes per rispettare la sua licenza GPLv2. \n\nURLSession è l'API di default di Apple per fare richieste a URL remote.";
+
+// Advanced Preference Pane
+"Advanced" = "Avanzate";
+"LocalCachePath" = "Percorso Cache Locale";
+"LocalCachePathDescription" = "Xcodes fa la cache delle versioni di Xcode disponibili e download temporanei in una directory.";
+"Change" = "Cambia";
+"Active/Select" = "Attivo/Seleziona";
+"AutomaticallyCreateSymbolicLink" = "Crea Automaticamente link simbolico a Xcodes.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "Quando rendi una versione di Xcode Attiva/Selezionata, prova a fare un link simbolico chiamato nella directory di installazione.";
+"PrivilegedHelper" = "Aiutante Privilegiato";
+"PrivilegedHelperDescription" = "Xcodes usa un aiutante privilegiato per svolgere dei compiti come root. Si tratta di comandi che normalmente richiederebbero sudo da linea di comando, incluse fasi di post-install e modificare la versione di Xcode con xcode-select.\n\nTi verrà richiesta la password del tuo account di macOS per installarlo.";
+"HelperInstalled" = "Aiutante è installato";
+"HelperNotInstalled" = "Aiutante non è installato";
+"InstallHelper" = "Installa aiutante";
+
+// Experiment Preference Pane
+"Experiments" = "Sperimentale";
+"FasterUnxip" = "Unxip veloce";
+"UseUnxipExperiment" = "Quando fai unxipping, usa sperimentale";
+"FasterUnxipDescription" = "Grazie a @_saagarjha, questa versione sperimentale può incrementare la velocità di unxipping fino al 70% in certi sistemi.\n\nMaggiori informazioni su come questo è possibile sono disponibili nel repository unxip - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Accesso Garantito. Riceverai notifiche da Xcodes.";
+"AccessDenied" = "⚠️ Accesso Negato ⚠️\n\nApri le Impostazioni delle Notifiche e seleziona Xcodes se desideri garantirgli accesso.";
+"NotificationSettings" = "Impostazioni delle Notifiche";
+"EnableNotifications" = "Abilita Notifiche";
+
+// SignIn
+"SignInWithApple" = "Entra con il tuo Apple ID.";
+"AppleID" = "AppleID:";
+"Password" = "Password:";
+"Required" = "Obbligatorio";
+"SignOut" = "Esci";
+
+// SMS/2FA
+"DigitCodeDescription" = "Inserisci il codice di %d cifre in uno dei tuo dispositivi registrati:";
+"SendSMS" = "Manda SMS";
+"EnterDigitCodeDescription" = "Inserisci il codice di %d cifre inviato a %@: ";
+"SelectTrustedPhone" = "Seleziona un numero di telefono registrato per ricevere il codice di %d cifre via SMS:";
+"NoTrustedPhones" = "Il tuo account non ha dispositivi registrati, ma è richiesto dall'autenticazione a due fattori.\n\Vedi https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Aggiorna a";
+
+// ToolBar
+"Login" = "Login";
+"LoginDescription" = "Apri Login";
+"Refresh" = "Ricarica";
+"RefreshDescription" = "Ricarica Lista Xcode";
+"All" = "Tutti";
+"Release" = "Rilasci";
+"ReleaseOnly" = "Solo Rilasci";
+"Beta" = "Beta";
+"BetaOnly" = "Solo Beta";
+"Filter" = "Filtra";
+"FilterAvailableDescription" = "Filtra versioni disponibili";
+"FilterInstalledDescription" = "Filtra versioni installate";
+"Info" = "Info";
+"InfoDescription" = "Mostra o nascondi pannello informazioni";
+"Preferences" = "Preferenze";
+"PreferencesDescription" = "Apri Preferenze";
+"Search" = "Cerca...";
+"SearchDescription" = "Lista Ricerca";
+
+// List
+"ActiveVersionDescription" = "Questa è la versione attiva";
+"MakeActiveVersionDescription" = "Rendi questa versione attiva";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Disinstalla Xcode %@?";
+"Alert.Uninstall.Message" = "Sarà spostato nel cestino, ma non svuotato.";
+"Alert.Uninstall.Error.Title" = "Non riesco a disinstallare Xcode";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Sei sicuro di voler interrompere l'installazione di Xcode %@?";
+"Alert.CancelInstall.Message" = "Perderai quanto fatto finora.";
+"Alert.CancelInstall.PrimaryButton" = "Ferma Installazione";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Aiutante Privilegiato";
+"Alert.PrivilegedHelper.Message" = "Xcodes usa un aiutante privilegiato per svolgere dei compiti come root. Si tratta di comandi che normalmente richiederebbero sudo da linea di comando, incluse fasi di post-install e modificare la versione di Xcode con xcode-select.\n\nTi verrà richiesta la password del tuo account di macOS per installarlo.";
+"Alert.PrivilegedHelper.Error.Title" = "Non riesco a installare aiutante";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Requisiti minimi non presenti";
+"Alert.MinSupported.Message" = "Xcode %@ richiede MacOS %@, ma la tua versione di MacOS è %@, vuoi installarlo comunque?";
+
+// Install
+"Alert.Install.Error.Title" = "Non riesco a installare Xcode";
+"Alert.InstallArchive.Error.Title" = "Non riesco a installare Xcode archiviato";
+
+// Update
+"Alert.Update.Error.Title" = "Non riesco ad aggiornare Xcode selezionato";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Non riesco a selezionare Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Non riesco a creare il link simbolico";
+"Alert.SymLink.Message" = "Xcode.app esiste e non è un link simbolico";
+
+// Post install
+"Alert.PostInstall.Title" = "Non riesco ad eseguire i comandi post installazione";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "L'archivio \"%@\" è danneggiato e non può essere espanso.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "L'archivio \"%@\" non può essere espanso perché non c'è abbastanza spazio libero nel volume.\n\nAumenta lo spazio disponibile per poter espandere l'archivio e poi installa Xcode %@ nuovamente per riprendere da dove era arrivato.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Non sono riuscito a spostare Xcode nella directory %@ .";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ ha fallito i check di sicurezza con il seguente errore:\n%@\nRimane installato in %@ se vuoi usarlo comunque.";
+"InstallationError.CodesignVerifyFailed" = "Xcode scaricato ha fallito le verifiche di firma con il seguente errore:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "Xcode scaricato non ha l'identità di firma corretta.\nGot:\n%@\n%@\Ci aspettiamo:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes non supporta (per ora) l'installazione di Xcode dal formato %@ .";
+"InstallationError.MissingSudoerPassword" = "Manca la password. Prova di nuovo.";
+"InstallationError.UnavailableVersion" = "Non trovo la versione %@.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Non sono disponibili versioni non-prerelease.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Non sono disponibili versioni prerelease.";
+"InstallationError.MissingUsernameOrPassword" = "Mancano username o password. Prova di nuovo";
+"InstallationError.VersionAlreadyInstalled" = "%@ è già installato in %@";
+"InstallationError.InvalidVersion" = "%@ non è una versione valida.";
+"InstallationError.VersionNotInstalled" = "%@ non è installato.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "L'installazione è completata, ma alcuni comandi di post installazione non sono partiti automaticamente. Verranno lanciati quando aprirai Xcode %@ per la prima volta.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "L'installazione è completata, ma alcuni comandi di post installazione non sono partiti automaticamente. Xcodes lancia questi comandi con un aiutante privilegiato, che sembra non essere installato. Puoi instlalarlo da Preferenze > Avanzate.\n\nVerranno lanciati quando aprirai Xcode %@ per la prima volta.";
+
+// Installation Steps
+"Downloading" = "Scaricando";
+"Unarchiving" = "Espandendo (Potrà metterci un po')";
+"Moving" = "Sposto in %@";
+"TrashingArchive" = "Sposto archivio nel Cestino";
+"CheckingSecurity" = "Verifica di sicurezza";
+"Finishing" = "Completando";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Una nuova versione è disponibile";
+"Notification.FinishedInstalling" = "Ho terminato l'installazione";
+
+
+"HelperClient.error" = "Non riesco a comunicare con l'aiutante privilegiato.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Nuove versioni di Xcode";
+"Notification.NewXcodeVersion.Body" = "Nuove versioni di Xcode disponibili per il download.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/ja.lproj/Localizable.strings b/Xcodes/Resources/ja.lproj/Localizable.strings
new file mode 100644
index 0000000..90aa86f
--- /dev/null
+++ b/Xcodes/Resources/ja.lproj/Localizable.strings
@@ -0,0 +1,238 @@
+// Menu
+"Menu.About" = "Xcodesについて";
+"Menu.CheckForUpdates" = "アップデートを確認…";
+"Menu.Acknowledgements" = "謝辞";
+"Menu.GitHubRepo" = "GitHub";
+"Menu.ReportABug" = "不具合の報告";
+"Menu.RequestNewFeature" = "機能のリクエスト";
+
+// Common
+"Install" = "インストール";
+"InstallDescription" = "このバージョンをインストール";
+"RevealInFinder" = "Finderに表示";
+"Active" = "アクティブ";
+"MakeActive" = "アクティブにする";
+"Open" = "開く";
+"OpenDescription" = "このバージョンを開く";
+"CopyPath" = "パスをコピー";
+"CreateSymLink" = "Xcode.appとしてシンボリックリンクを作成";
+"CreateSymLinkBeta" = "Xcode-Beta.appとしてシンボリックリンクを作成";
+"Uninstall" = "アンインストール";
+"Selected" = "アクティブ";
+"Select" = "アクティブにする";
+"Cancel" = "キャンセル";
+"Next" = "次へ";
+"Continue" = "続ける";
+"Close" = "閉じる";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "ビルドの同一性";
+"IdenticalBuilds.help" = "プレリリース版とリリース版が全く同じビルドであることがあります。Xcodesはこれらのバージョンを自動的に一緒に表示します。";
+
+"ReleaseDate" = "リリース日";
+"ReleaseNotes" = "リリースノート";
+"ReleaseNotes.help" = "リリースノートを表示";
+"CopyReleaseNoteURL" = "URLをコピー";
+"Compatibility" = "互換性";
+"MacOSRequirement" = "macOS %@ 以降";
+"SDKs" = "SDK";
+"Compilers" = "コンパイラ";
+"DownloadSize" = "サイズ";
+"NoXcodeSelected" = "Xcodeが選択されていません";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "ステップ %1$d / %2$d: %3$@";
+"DownloadingPercentDescription" = "ダウンロード中: %d%%";
+"StopInstallation" = "インストールを中止";
+"DownloadingError" = "ダウンロードに関する情報が見つかりませんでした。";
+
+// About
+"VersionWithBuild" = "バージョン %@ (%@)";
+"GithubRepo" = "GitHub";
+"Acknowledgements" = "謝辞";
+"UnxipExperiment" = "Unxip Experiment";
+"License" = "ライセンス";
+
+// General Preference Pane
+"General" = "一般";
+"AppleID" = "Apple ID";
+"SignIn" = "サインイン";
+"Notifications" = "通知";
+
+// Updates Preference Pane
+"Updates" = "アップデート";
+"Versions" = "バージョン";
+"AutomaticInstallNewVersion" = "新しい Xcode バージョンを自動的にインストールする";
+"IncludePreRelease" = "プレリリース版 / ベータ版を含む";
+"AppUpdates" = "Xcodes.appの更新";
+"CheckForAppUpdates" = "アプリのアップデートを自動で確認する";
+"CheckNow" = "今すぐ確認";
+"LastChecked" = "前回の確認: %@";
+"Never" = "なし";
+
+// Download Preference Pane
+"Downloads" = "ダウンロード";
+"DataSource" = "データソース";
+"DataSourceDescription" = "Appleのデータソースは、Apple Developerのウェブサイトをスクレイピングしています。常に最新のリリースが表示されますが、比較的壊れやすくなっています。\n\nXcode Releasesは、非公式なXcodeのリリース一覧です。この一覧は整形されたデータとして提供されます。Appleからは簡単に入手できない特別な情報を含んでおり、AppleがDeveloper ウェブサイトを再設計しても壊れにくくなっています。";
+"Downloader" = "ダウンローダ";
+"DownloaderDescription" = "aria2 は、最大16個の接続を使用して、Xcode を URLSession の3-5 倍のスピードでダウンロードします。GPLv2 ライセンスに準拠するため、Xcodes 内にソースコードと一緒に実行ファイルとしてバンドルされています。\n\nURLSession は、URLリクエストを行うための Apple のデフォルト API です。";
+
+// Advanced Preference Pane
+"Advanced" = "上級者向け";
+"LocalCachePath" = "ローカルキャッシュパス";
+"LocalCachePathDescription" = "Xcodesは、利用可能なXcodeのバージョンをキャッシュし、新しいバージョンをディレクトリに一時的にダウンロードします。";
+"Change" = "変更";
+"Active/Select" = "アクティブ";
+"InstallDirectory" = "インストール先";
+"InstallPathDescription" = "Xcodesは1つのディレクトリを検索し、そこにインストールします。デフォルト (推奨) は /Applications です。Xcodeの格納場所を変更すると、他のアプリケーションやサービスが動作しなくなる可能性があります。";
+
+"OnSelectDoNothing" = "名前をXcode-X.X.X.appのまま維持する";
+"OnSelectDoNothingDescription" = "選択時、バージョン付きの名前を維持します。例) Xcode-13.4.1.app";
+"AutomaticallyCreateSymbolicLink" = "Xcode.appへのシンボリックリンクの自動生成";
+"AutomaticallyCreateSymbolicLinkDescription" = "Xcodeのバージョンをアクティブにする時、インストール先でXcode.appのシンボリックリンクの作成を試みます。";
+"OnSelectRenameXcode" = "名前を常にXcode.appに変更する";
+"OnSelectRenameXcodeDescription" = "選択時、自動的にアクティブなXcodeをXcode.appに、以前のXcode.appをバージョン付きの名前に変更します。";
+
+"PrivilegedHelper" = "権限のあるヘルパー";
+"PrivilegedHelperDescription" = "Xcodesは、rootとしてタスクを実行するために、別の権限のあるヘルパーを使用します。インストール後の手順や xcode-select による Xcode のバージョン切り替えなど、コマンドラインで sudo を必要とするものです。\n\nインストールには、macOS アカウントのパスワードの入力が必要です。";
+"HelperInstalled" = "インストール済み";
+"HelperNotInstalled" = "未インストール";
+"InstallHelper" = "ヘルパーのインストール";
+
+"ShowOpenInRosetta" = "Rosettaで開くオプションを表示する";
+"ShowOpenInRosettaDescription" = "Rosettaで開くオプションは \"開く\" 機能が利用できる場所に表示されます。注意: これはApple Siliconマシンにのみ表示されます。";
+
+// Experiment Preference Pane
+"Experiments" = "試験的な機能";
+"FasterUnxip" = "高速な Unxip";
+"UseUnxipExperiment" = "試験的な Unxip の実装を使う";
+"FasterUnxipDescription" = "@_saagarjha さんの方法で、一部のシステムで Unxip の速度が最大70%向上します。\n\n方法の詳細については、 unxipのリポジトリ (https://github.com/saagarjha/unxip)をご覧ください。";
+
+// Notifications
+"AccessGranted" = "アクセスが許可されました。Xcodesから通知が届きます。";
+"AccessDenied" = "⚠️ アクセス拒否 ⚠️\n\nアクセス許可する場合は、通知設定を開き、Xcodesを選択してください。";
+"NotificationSettings" = "通知設定";
+"EnableNotifications" = "通知を有効にする";
+
+// SignIn
+"SignInWithApple" = "Apple IDでサインインしてください。";
+"AppleID" = "Apple ID:";
+"Password" = "パスワード:";
+"Required" = "必須";
+"SignOut" = "サインアウト";
+
+// SMS/2FA
+"DigitCodeDescription" = "信頼できるデバイスから、%d桁のコードを入力してください。";
+"SendSMS" = "SMS を送信";
+"EnterDigitCodeDescription" = "%d桁のコードを%@に送信したので入力してください。";
+"SelectTrustedPhone" = "信頼できる電話番号を選択し、%d桁のコードをSMSで受信します。";
+"NoTrustedPhones" = "このアカウントに2要素認証に利用する、信頼できる電話番号がありません。\n\n詳しくはこちらをご確認ください。https://support.apple.com/HT204915";
+
+// MainWindow
+"UpdatedAt" = "前回の更新:";
+
+// ToolBar
+"Login" = "ログイン";
+"LoginDescription" = "ログイン画面を開く";
+"Refresh" = "再読み込み";
+"RefreshDescription" = "Xcodeの一覧を再度取得する";
+"All" = "すべて";
+"Release" = "リリース";
+"ReleaseOnly" = "リリース版のみ";
+"Beta" = "ベータ";
+"BetaOnly" = "ベータ版のみ";
+"Filter" = "フィルタ";
+"FilterAvailableDescription" = "利用可能なバージョンのフィルタを適用する";
+"FilterInstalledDescription" = "インストール済みバージョンのフィルタを適用する";
+"Info" = "情報";
+"InfoDescription" = "情報パネルの切り替え";
+"Preferences" = "環境設定";
+"PreferencesDescription" = "環境設定を開く";
+"Search" = "検索…";
+"SearchDescription" = "一覧の検索";
+
+// List
+"ActiveVersionDescription" = "アクティブなバージョン";
+"MakeActiveVersionDescription" = "アクティブなバージョンにする";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Xcode %@ をアンインストールしてもよろしいですか?";
+"Alert.Uninstall.Message" = "ゴミ箱に移動されますが、ゴミ箱を自動的に空にすることはありません。";
+"Alert.Uninstall.Error.Title" = "Xcode をアンインストールできませんでした";
+"Alert.Uninstall.Error.Message.FileNotFound" = "ファイル \"%@\" が見つかりませんでした。";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Xcode %@ のインストールを中止してもよろしいですか?";
+"Alert.CancelInstall.Message" = "インストール前の状態に戻します。";
+"Alert.CancelInstall.PrimaryButton" = "インストールを中止";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "権限のあるヘルパー";
+"Alert.PrivilegedHelper.Message" = "Xcodesは、Rootとしてタスクを実行するために、別の権限のあるヘルパーを使用します。インストール後の手順や xcode-select による Xcode のバージョン切り替えなど、コマンドラインで sudo を必要とするものです。\n\nインストールには、macOS アカウントのパスワードの入力が必要です。";
+"Alert.PrivilegedHelper.Error.Title" = "ヘルパーをインストールできませんでした";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "システム要件を満たしていません";
+"Alert.MinSupported.Message" = "Xcode %@ を使うには macOS %@ 以降が必要です。このシステムは macOS %@ ですが、インストールを続行しますか?";
+
+// Install
+"Alert.Install.Error.Title" = "Xcode をインストールできませんでした";
+"Alert.InstallArchive.Error.Title" = "Xcode アーカイブをインストールできませんでした";
+
+// Update
+"Alert.Update.Error.Title" = "Xcode の一覧をアップデートできませんでした";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Xcode をアクティブにすることができませんでした";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "シンボリックリンクを作成できませんでした";
+"Alert.SymLink.Message" = "Xcode.appが存在し、シンボリックリンクではありません。";
+
+// Post install
+"Alert.PostInstall.Title" = "インストール後の作業を実行できませんでした";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "このアーカイブ \"%@\" は破損していて展開できません。";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "このボリュームの空き容量が不足しているため、アーカイブ \"%@\" を展開できません。\n\n空き容量を増やしてから Xcode %@ のインストールをもう一度お試しください。";
+"InstallationError.FailedToMoveXcodeToApplications" = "Xcode を %@ に移動できませんでした。";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ は、以下の出力でセキュリティ評価に失敗しました。\n%@\nそれでも使用したい場合は、%@にインストールされたままになっています。";
+"InstallationError.CodesignVerifyFailed" = "ダウンロードしたXcodeは、以下の出力で署名の検証に失敗しました。\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "ダウンロードしたXcodeに想定された署名IDがありません。\nGot:\n%@\n%@\nExpected:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes は %@ 形式でのインストールには(まだ)対応していません。";
+"InstallationError.MissingSudoerPassword" = "パスワードが見つかりません。もう一度お試しください。";
+"InstallationError.UnavailableVersion" = "バージョン %@ が見つかりません。";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "プレリリース以外の利用可能なバージョンがありません。";
+"InstallationError.NoPrereleaseVersionAvailable" = "利用可能なプレリリースバージョンがありません。";
+"InstallationError.MissingUsernameOrPassword" = "ユーザ名またはパスワードが見つかりません。もう一度お試しください。";
+"InstallationError.VersionAlreadyInstalled" = "%@ は既に %@ にインストールされています。";
+"InstallationError.InvalidVersion" = "%@ は有効なバージョン番号ではありません。";
+"InstallationError.VersionNotInstalled" = "%@ がインストールされていません。";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "インストールは完了しましたが、いくつかのインストール後のタスクが自動的に実行されませんでした。このタスクは Xcode %@ を初回起動した際に実行されます。";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "インストールは完了しましたが、いくつかのインストール後作業が自動的に実行されませんでした。Xcodes は権限のあるヘルパーを使ってタスクを実行しますが、ヘルパーがインストールされていません。環境設定 > 上級者向け でインストールできます。\n\nこのタスクは Xcode %@ を初回起動した際に実行されます。";
+
+// Installation Steps
+"Downloading" = "ダウンロードしています";
+"Unarchiving" = "アーカイブを展開しています (時間がかかることがあります)";
+"Moving" = "%@ に移動しています";
+"TrashingArchive" = "アーカイブをゴミ箱に移動しています";
+"CheckingSecurity" = "セキュリティを検証しています";
+"Finishing" = "終了しています";
+
+// Notifications
+"Notification.NewVersionAvailable" = "新しいバージョンが利用可能です";
+"Notification.FinishedInstalling" = "インストールを完了しました";
+
+
+"HelperClient.error" = "権限のあるヘルパーと通信できません。";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "新しいXcodeのバージョン";
+"Notification.NewXcodeVersion.Body" = "新しいバージョンがダウンロードできます。";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/ko.lproj/Localizable.strings b/Xcodes/Resources/ko.lproj/Localizable.strings
new file mode 100644
index 0000000..24e0b6d
--- /dev/null
+++ b/Xcodes/Resources/ko.lproj/Localizable.strings
@@ -0,0 +1,226 @@
+// Menu
+"Menu.About" = "Xcodes 정보";
+"Menu.CheckForUpdates" = "업데이트 확인...";
+"Menu.Acknowledgements" = "Xcodes 법적고지";
+"Menu.GitHubRepo" = "Xcodes GitHub 저장소";
+"Menu.ReportABug" = "버그 제보하기";
+"Menu.RequestNewFeature" = "새로운 기능 요청하기";
+
+// Common
+"Install" = "설치";
+"InstallDescription" = "이 버전 설치하기";
+"RevealInFinder" = "Finder에서 보기";
+"Active" = "기본 버전으로 사용 중";
+"MakeActive" = "기본 버전으로 만들기";
+"Open" = "열기";
+"OpenDescription" = "이 버전 열기";
+"CopyPath" = "경로 복사하기";
+"CreateSymLink" = "Xcode.app과 같은 Symlink 만들기";
+"CreateSymLinkBeta" = "Xcode-Beta.app과 같은 Symlink 만들기";
+"Uninstall" = "제거";
+"Selected" = "선택됨";
+"Select" = "선택";
+"Cancel" = "취소";
+"Next" = "다음";
+"Continue" = "계속하기";
+"Close" = "닫기";
+"OK" = "확인";
+
+// Info Pane
+"IdenticalBuilds" = "동일한 빌드";
+"IdenticalBuilds.help" = "가끔 사전 출시 (prerelease) 버전과 출시 버전이 완전히 동일한 빌드인 경우가 있습니다. Xcodes는 자동으로 이 버전들을 함께 표시합니다.";
+
+"ReleaseDate" = "출시일";
+"ReleaseNotes" = "릴리즈 노트";
+"ReleaseNotes.help" = "릴리즈 노트 보기";
+"CopyReleaseNoteURL" = "URL 복사";
+"Compatibility" = "호환성";
+"MacOSRequirement" = "macOS %@ 또는 이후 버전";
+"SDKs" = "SDKs";
+"Compilers" = "컴파일러";
+"DownloadSize" = "다운로드 크기";
+"NoXcodeSelected" = "선택된 Xcode 없음";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "%1$d / %2$d 단계: %3$@";
+"DownloadingPercentDescription" = "다운로드 중: %d%% 완료";
+"StopInstallation" = "설치 중지";
+"DownloadingError" = "다운로드 정보를 찾을 수 없음";
+
+// About
+"VersionWithBuild" = "버전 %@ (%@)";
+"GithubRepo" = "GitHub 저장소";
+"Acknowledgements" = "Acknowledgements";
+"UnxipExperiment" = "Unxip 실험";
+"License" = "라이센스";
+
+// General Preference Pane
+"General" = "일반";
+"AppleID" = "Apple ID";
+"SignIn" = "로그인하기";
+"Notifications" = "알림";
+
+// Updates Preference Pane
+"Updates" = "업데이트";
+"Versions" = "버전";
+"AutomaticInstallNewVersion" = "새 Xcode 버전 자동으로 설치";
+"IncludePreRelease" = "사전 출시/베타 버전 포함";
+"AppUpdates" = "Xcodes.app 업데이트";
+"CheckForAppUpdates" = "업데이트 자동 확인";
+"CheckNow" = "지금 확인하기";
+"LastChecked" = "마지막 확인 시점: %@";
+"Never" = "확인하지 않음";
+
+// Download Preference Pane
+"Downloads" = "다운로드";
+"DataSource" = "데이터 소스";
+"DataSourceDescription" = "Apple 데이터 소스는 Apple Developer 웹사이트를 스크랩합니다. Apple Developer에는 항상 사용 가능한 최신 출시 버전이 표시되지만 취약한 면이 있습니다.\n\nXcode Releases는 비공식적인 Xcode 출시 버전 목록입니다. Xcode Releases에서는 정리된 데이터를 제공하며, Apple에서 쉽게 구할 수 없는 정보를 포함하며 Apple이 Apple Developer를 재설계할 경우에도 안전할 수 있습니다.";
+"Downloader" = "다운로더";
+"DownloaderDescription" = "aria2는 최대 16 개의 연결을 사용하여 URLSession보다 3~5배 더 빠르게 Xcode를 다운로드합니다. GPLv2 라이센스를 준수하기 위해 Xcodes 내 소스 코드와 함께 실행 파일 번들로 제공됩니다.\n\nURLSession은 URL 요청을 만들기 위한 기본 Apple API입니다.";
+
+// Advanced Preference Pane
+"Advanced" = "고급";
+"LocalCachePath" = "로컬 캐시 경로";
+"LocalCachePathDescription" = "Xcodes는 사용 가능한 Xcode 버전을 캐시하고 임시로 디렉토리에 새 버전을 다운로드 합니다.";
+"Change" = "변경";
+"Active/Select" = "기본 버전/선택";
+"AutomaticallyCreateSymbolicLink" = "Xcodes.app에 대한 심볼릭 링크 자동 생성";
+"AutomaticallyCreateSymbolicLinkDescription" = "Xcode 버전을 기본 버전(활성)/선택됨 상태로 만들 때 설치 디렉토리에 Xcode.app이라는 심볼릭 링크를 만들어 보세요.";
+"PrivilegedHelper" = "권한을 가진 도우미 (Privileged Helper)";
+"PrivilegedHelperDescription" = "Xcodes는 별도의 권한을 가진 도우미를 사용하여 작업을 루트로서 수행합니다. 설치 후 단계와 xcode-select로 Xcode 버전을 전환하는 것과 같이 커맨드 라인에서 sudo가 필요한 작업이 이에 해당합니다.\n\n설치하려면 macOS 계정 암호를 입력하라는 메시지가 표시됩니다.";
+"HelperInstalled" = "도우미 설치됨";
+"HelperNotInstalled" = "도우미 설치되지 않음";
+"InstallHelper" = "도우미 설치";
+
+// Experiment Preference Pane
+"Experiments" = "실험실";
+"FasterUnxip" = "더 빠른 Unxip";
+"UseUnxipExperiment" = "unxipping 시 실험 기능 사용";
+"FasterUnxipDescription" = "@_saagarjha님 덕분에 이 실험 기능을 이용하면 일부 시스템에서 압축 해제 속도를 최대 70%까지 향상시킬 수 있습니다.\n\n이를 수행하는 방법에 대한 자세한 내용은 unxip 저장소 (https://github.com/saagarjha/unxip)에서 확인할 수 있습니다.";
+
+// Notifications
+"AccessGranted" = "접근 권한이 부여되었습니다. Xcodes에서 알림을 받게 됩니다.";
+"AccessDenied" = "⚠️ 접근 권한이 부여되지 않았습니다 ⚠️\n\n 접근을 허용하려면 알림 설정을 열고 Xcodes를 선택하여 알림을 허용해주세요.";
+"NotificationSettings" = "알림 설정";
+"EnableNotifications" = "알림 활성화";
+
+// SignIn
+"SignInWithApple" = "Apple ID로 로그인하세요.";
+"AppleID" = "AppleID:";
+"Password" = "비밀번호:";
+"Required" = "필수 입력";
+"SignOut" = "로그아웃";
+
+// SMS/2FA
+"DigitCodeDescription" = "신뢰할 수 있는 장치 중 하나에서 %d 자리 코드를 입력하세요.";
+"SendSMS" = "SMS 보내기";
+"EnterDigitCodeDescription" = "%@(으)로 전송된 %d 자리 코드를 입력하세요.";
+"SelectTrustedPhone" = "SMS를 통해 %d 자리 코드를 수신하려면 신뢰할 수 있는 전화번호를 선택하세요.";
+"NoTrustedPhones" = "계정에 신뢰할 수 있는 전화번호가 없지만 이중 인증(2FA)에 필요합니다.\n\nhttps://support.apple.com/HT204915를 참조하세요.";
+
+// MainWindow
+"UpdatedAt" = "마지막 업데이트 시점";
+
+// ToolBar
+"Login" = "로그인";
+"LoginDescription" = "로그인 창 열기";
+"Refresh" = "갱신";
+"RefreshDescription" = "Xcode 목록 갱신";
+"All" = "전체";
+"Release" = "출시 버전";
+"ReleaseOnly" = "출시 버전만";
+"Beta" = "베타";
+"BetaOnly" = "베타만";
+"Filter" = "필터";
+"FilterAvailableDescription" = "이용 가능한 버전만 보기";
+"FilterInstalledDescription" = "설치한 버전만 보기";
+"Info" = "정보";
+"InfoDescription" = "정보창 표시 또는 숨기기";
+"Preferences" = "환경설정";
+"PreferencesDescription" = "환경설정 열기";
+"Search" = "검색하기...";
+"SearchDescription" = "목록 검색하기";
+
+// List
+"ActiveVersionDescription" = "기본 버전으로 사용 중";
+"MakeActiveVersionDescription" = "기본 버전으로 만들기";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Xcode %@을 제거하시겠습니까?";
+"Alert.Uninstall.Message" = "휴지통으로 이동되지만 비워지지는 않습니다.";
+"Alert.Uninstall.Error.Title" = "Xcode를 제거할 수 없음";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Xcode %@ 설치를 중지하시겠습니까?";
+"Alert.CancelInstall.Message" = "진행된 모든 사항은 삭제됩니다";
+"Alert.CancelInstall.PrimaryButton" = "설치 중지";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "권한을 가진 도우미 (Privileged Helper)";
+"Alert.PrivilegedHelper.Message" = "Xcodes는 별도의 권한을 가진 도우미를 사용하여 작업을 루트로서 수행합니다. 설치 후 단계와 xcode-select로 Xcode 버전을 전환하는 것과 같이 커맨드 라인에서 sudo가 필요한 작업이 이에 해당합니다.\n\n설치하려면 macOS 계정 암호를 입력하라는 메시지가 표시됩니다.";
+"Alert.PrivilegedHelper.Error.Title" = "도우미를 설치할 수 없음";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "최소 요건 충족되지 않음";
+"Alert.MinSupported.Message" = "Xcode %@는 MacOS %@이(가) 필요하지만 MacOS %@을(를) 실행 중입니다. 그래도 설치하시겠습니까?";
+
+// Install
+"Alert.Install.Error.Title" = "Xcode를 설치할 수 없음";
+"Alert.InstallArchive.Error.Title" = "아카이브된 Xcode를 설치할 수 없음";
+
+// Update
+"Alert.Update.Error.Title" = "선택된 Xcode를 업데이트할 수 없음";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Xcode를 선택할 수 없음";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "심볼릭 링크를 생성할 수 없음";
+"Alert.SymLink.Message" = "Xcode.app이 존재하며 심볼릭 링크가 아닙니다.";
+
+// Post install
+"Alert.PostInstall.Title" = "설치 후 단계를 수행할 수 없음";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "아카이브 \"%@\"이(가) 손상되어 확장할 수 없습니다.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "현재 볼륨에 여유 공간이 충분하지 않기 때문에 아카이브 \"%@\"을(를) 확장할 수 없습니다.\n\n아카이브를 확장하기 위해 더 많은 공간을 확보한 다음 Xcode %@을(를) 다시 설치하여 중단된 위치에서 설치를 시작하세요.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Xcode를 %@ 디렉토리로 이동하지 못했습니다.";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@이(가) 다음과 같이 보안 평가에 실패했습니다.\n%@\n그래도 사용하려는 경우 %@에 설치된 상태로 유지됩니다.";
+"InstallationError.CodesignVerifyFailed" = "다운로드한 Xcode가 다음과 같이 코드 서명 확인에 실패했습니다.\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "다운로드한 Xcode에 예상된 코드 서명 ID가 없습니다.\n받은 값:\n%@\n%@\n예상된 값:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes는 (아직) %@ 파일 형식에서 Xcode 설치를 지원하지 않습니다.";
+"InstallationError.MissingSudoerPassword" = "비밀번호가 입력되지 않았습니다. 다시 시도해 주세요.";
+"InstallationError.UnavailableVersion" = "%@ 버전을 찾을 수 없습니다.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "사전 출시 버전 이외에 사용 가능한 버전이 없습니다.";
+"InstallationError.NoPrereleaseVersionAvailable" = "사용 가능한 사전 출시 버전이 없습니다.";
+"InstallationError.MissingUsernameOrPassword" = "사용자 이름 또는 비밀번호가 입력되지 않았습니다. 다시 시도해 주세요.";
+"InstallationError.VersionAlreadyInstalled" = "%@은(는) 이미 %@에 설치되어 있습니다.";
+"InstallationError.InvalidVersion" = "%@은(는) 유효한 버전 번호가 아닙니다.";
+"InstallationError.VersionNotInstalled" = "%@이(가) 설치되지 않았습니다.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "설치가 완료되었지만 일부 설치 후 단계가 자동으로 수행되지 않았습니다. 이는 Xcode %@를 처음 실행할 때 수행됩니다.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "설치가 완료되었지만 일부 설치 후 단계가 자동으로 수행되지 않았습니다. Xcodes는 설치되지 않은 것으로 보이는 권한을 가진 도우미 (Privileged helper)를 사용하여 이러한 단계를 수행합니다. 권한을 가진 도우미는 환경설정 > 고급에서 설치할 수 있습니다.\n\n이 단계는 Xcode %@를 처음 실행할 때 수행됩니다.";
+
+// Installation Steps
+"Downloading" = "다운로드 중";
+"Unarchiving" = "언아카이빙 중 (시간이 좀 걸릴 수 있습니다)";
+"Moving" = "%@(으)로 이동 중";
+"TrashingArchive" = "아카이브를 휴지통으로 이동";
+"CheckingSecurity" = "보안 확인";
+"Finishing" = "마무리하는 중";
+
+// Notifications
+"Notification.NewVersionAvailable" = "새 버전을 사용할 수 있습니다";
+"Notification.FinishedInstalling" = "설치 완료";
+
+
+"HelperClient.error" = "권한을 가진 도우미 (Privileged helper)와의 통신할 수 없습니다.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "새 Xcode 버전";
+"Notification.NewXcodeVersion.Body" = "새로운 Xcode 버전을 다운로드 할 수 있습니다.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/nl.lproj/Localizable.strings b/Xcodes/Resources/nl.lproj/Localizable.strings
new file mode 100644
index 0000000..2b52b12
--- /dev/null
+++ b/Xcodes/Resources/nl.lproj/Localizable.strings
@@ -0,0 +1,230 @@
+// Menu
+"Menu.About" = "Over Xcodes";
+"Menu.CheckForUpdates" = "Controleren op Updates...";
+"Menu.Acknowledgements" = "Xcodes Dankbetuigingen";
+"Menu.GitHubRepo" = "Xcodes GitHub Repo";
+"Menu.ReportABug" = "Bug Rapporteren";
+"Menu.RequestNewFeature" = "Nieuwe Feature Aanvragen";
+
+// Common
+"Install" = "Installeren";
+"InstallDescription" = "Installeer deze versie";
+"RevealInFinder" = "Toon in Finder";
+"Active" = "Actief";
+"MakeActive" = "Maak actief";
+"Open" = "Open";
+"OpenDescription" = "Open deze versie";
+"CopyPath" = "Kopieer Pad";
+"CreateSymLink" = "Creëer Symlink als Xcode.app";
+"Uninstall" = "Deinstalleren";
+"Selected" = "Geselecteerd";
+"Select" = "Selecteer";
+"Cancel" = "Annuleren";
+"Next" = "Volgende";
+"Continue" = "Doorgaan";
+"Close" = "Sluiten";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Identieke Builds";
+"IdenticalBuilds.help" = "Soms zijn een prerelease en release versie exact hetzelfde. Xcodes toont deze versies automatisch samen.";
+
+"ReleaseDate" = "Release Datum";
+"ReleaseNotes" = "Release Notes";
+"ReleaseNotes.help" = "Toon Release Notes";
+"CopyReleaseNoteURL" = "Kopieer URL";
+"Compatibility" = "Compatibiliteit";
+"MacOSRequirement" = "Vereist macOS %@ of hoger";
+"SDKs" = "SDKs";
+"Compilers" = "Compilers";
+"DownloadSize" = "Download Grootte";
+"NoXcodeSelected" = "Geen Xcode Geselecteerd";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Stap %1$d van %2$d: %3$@";
+"DownloadingPercentDescription" = "Downloaden: %d%% compleet";
+"StopInstallation" = "Stop installatie";
+"DownloadingError" = "Geen download informatie gevonden";
+
+// About
+"VersionWithBuild" = "Versie %@ (%@)";
+"GithubRepo" = "GitHub Repo";
+"Acknowledgements" = "Dankbetuigingen";
+"UnxipExperiment" = "Unxip Experiment";
+"License" = "Licentie";
+
+// General Preference Pane
+"General" = "Algemeen";
+"AppleID" = "Apple ID";
+"SignIn" = "Inloggen";
+"Notifications" = "Notificaties";
+
+// Updates Preference Pane
+"Updates" = "Updates";
+"Versions" = "Versies";
+"AutomaticInstallNewVersion" = "Installeer nieuwe versies van Xcode automatisch";
+"IncludePreRelease" = "Prerelease/beta versies toestaan";
+"AppUpdates" = "Xcodes.app Updates";
+"CheckForAppUpdates" = "Automatisch controleren op app updates";
+"CheckNow" = "Nu Controleren";
+"LastChecked" = "Laatste controle: %@";
+"Never" = "Nooit";
+
+// Advanced Preference Pane
+"Advanced" = "Geavanceerd";
+"LocalCachePath" = "Lokaal Cache Pad";
+"LocalCachePathDescription" = "Xcodes cached beschikbare Xcode versies en download tijdelijk nieuwe versies naar een map";
+"Change" = "Wijzig";
+"Active/Select" = "Active/Select";
+"InstallDirectory" = "Installatie Map";
+"InstallPathDescription" = "Xcodes zoekt en installeert naar een enkele map. Standaard (en aanbevolen) is om dit in te stellen op /Applications. Wijzigingen die gemaakt worden in waar Xcode is opgeslagen kan resulteren in niet werkende apps/services. ";
+
+"OnSelectDoNothing" = "Behoud naam als Xcode-X.X.X.app";
+"OnSelectDoNothingDescription" = "Wanneer ingeschakeld, zal de bestandsnaam altijd het versienummer bevatten. Bijvoorbeeld Xcode-13.4.1.app";
+"AutomaticallyCreateSymbolicLink" = "Creëer automatisch een symbolic link naar Xcode.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "Bij het actief maken/selecteren van een Xcode versie, probeer een symbolic link genaamd Xcode.app te maken in de installatie map";
+"OnSelectRenameXcode" = "Altijd hernoemen naar Xcode.app";
+"OnSelectRenameXcodeDescription" = "Wanneer ingeschakeld, zal automatisch de actieve Xcode hernoemd worden naar Xcode.app, de vorige Xcode.app zal hernoemd worden naar de desbetreffende versie.";
+
+"DataSource" = "Databron";
+"DataSourceDescription" = "De Apple databron scraped de Apple Developer website. Dit zal altijd de laatste nieuwe versies tonen die beschikbaar zijn, maar is meer foutgevoelig.\n\nXcode Releases is een onofficiële lijst van Xcode releases. Deze lijst wordt gepresenteerd als gevalideerde data en bevat extra informatie die niet beschikbaar is vanuit Apple. Deze databron is minder foutgevoelig en niet afhankelijk van wanneer Apple de developer website aanpast.";
+"Downloader" = "Downloader";
+"DownloaderDescription" = "aria2 gebruikt tot 16 verbindingen om Xcode 3-5x sneller te downloaden dan met URLSession. Het is gebundeld als een executable samen met de broncode binnen Xcodes om te voldoen aan de GPLv2 licentie.\n\nURLSession is de standaard Apple API voor het maken van URL verzoeken.";
+"PrivilegedHelper" = "Privileged Helper";
+"PrivilegedHelperDescription" = "Xcodes gebruikt een separate privilged helper om taken uit te voeren als root. Dit zijn operaties die een sudo vereisen op de command line, inclusief post-installatie stappen en het wijzigen van Xcode versies met xcode-select.\n\nJe zult worden gevraagd om je macOS account wachtwoord om deze te installeren.";
+"HelperInstalled" = "Helper is geïnstalleerd";
+"HelperNotInstalled" = "Helper is niet geïnstalleerd";
+"InstallHelper" = "Installeer helper";
+
+// Experiment Preference Pane
+"Experiments" = "Experimenten";
+"FasterUnxip" = "Snellere Unxip";
+"UseUnxipExperiment" = "Bij unxipping, gebruik expiriment";
+"FasterUnxipDescription" = "Dankzij @_saagarjha, kan met dit experiment het uitpakken van Xcode tot 70% sneller gaan op sommige systemen.\n\nMeer informatie over hoe dit mogelijk wordt gemaakt kan worden gevonden op de unxip repo - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Toegang Verleend. Je krijgt notificaties van Xcodes.";
+"AccessDenied" = "⚠️ Toegang Geweigerd ⚠️\n\nOpen je Notificatie Instellingen en selecteer Xcodes om toegang te verlenen.";
+"NotificationSettings" = "Notificatie Instellingen";
+"EnableNotifications" = "Notificaties Inschakelen";
+
+// SignIn
+"SignInWithApple" = "Log in met je Apple ID.";
+"AppleID" = "AppleID:";
+"Password" = "Wachtwoord:";
+"Required" = "Vereist";
+"SignOut" = "Uitloggen";
+
+// SMS/2FA
+"DigitCodeDescription" = "Voer de %d code in van een van je vertrouwde apparaten:";
+"SendSMS" = "Verstuur SMS";
+"EnterDigitCodeDescription" = "Voer de %d code in die is verstuurd naar %@: ";
+"SelectTrustedPhone" = "Selecteer een vertrouwd telefoonnumer on een %d code te ontvangen via SMS:";
+"NoTrustedPhones" = "Je account heeft geen vertrouwde telefoonnummers, dit is nodig voor two-factor authenticatie.\n\nZie https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Bijgewerkt op";
+
+// ToolBar
+"Login" = "Inloggen";
+"LoginDescription" = "Open Inloggen";
+"Refresh" = "Verversen";
+"RefreshDescription" = "Ververs Xcode Lijst";
+"All" = "Alles";
+"Release" = "Release";
+"ReleaseOnly" = "Alleen release";
+"Beta" = "Beta";
+"BetaOnly" = "Alleen beta";
+"Filter" = "Filter";
+"FilterAvailableDescription" = "Filter beschikbare versies";
+"FilterInstalledDescription" = "Filter geïnstalleerde versies";
+"Info" = "Info";
+"InfoDescription" = "Show or hide the info pane";
+"Preferences" = "Voorkeuren";
+"PreferencesDescription" = "Open Voorkeuren";
+"Search" = "Zoeken...";
+"SearchDescription" = "Zoek lijst";
+
+// List
+"ActiveVersionDescription" = "Dit is de actieve versie";
+"MakeActiveVersionDescription" = "Maak dit de actieve versie";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Xcode %@ deinstalleren?";
+"Alert.Uninstall.Message" = "Het zal worden verplaatst naar de Prullenbak, maar deze zal niet geleegd worden.";
+"Alert.Uninstall.Error.Title" = "Kan Xcode niet deinstalleren";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Weet je zeker dat je de installatie van Xcode %@ wilt stoppen?";
+"Alert.CancelInstall.Message" = "Elke voortgang zal worden weggegooid.";
+"Alert.CancelInstall.PrimaryButton" = "Stop Installatie";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Privileged Helper";
+"Alert.PrivilegedHelper.Message" = "Xcodes gebruikt een separate privilged helper om taken uit te voeren als root. Dit zijn operaties die een sudo vereisen op de command line, inclusief post-installatie stappen en het wijzigen van Xcode versies met xcode-select.\n\nJe zult worden gevraagd om je macOS account wachtwoord om deze te installeren.";
+"Alert.PrivilegedHelper.Error.Title" = "Kan helper niet installeren";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Minimale vereisten niet voldaan";
+"Alert.MinSupported.Message" = "Xcode %@ vereist MacOS %@, maar jouw MacOS versie is %@, wil je doorgaan met installeren?";
+
+// Install
+"Alert.Install.Error.Title" = "Kan Xcode niet installeren";
+"Alert.InstallArchive.Error.Title" = "Kan gearchiveerde Xcode niet installeren";
+
+// Update
+"Alert.Update.Error.Title" = "Kan geselecteerde Xcode niet updaten";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Kan Xcode niet selecteren";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Kan geen symbolic link maken";
+"Alert.SymLink.Message" = "Xcode.app bestaat en is geen symbolic link";
+
+// Post install
+"Alert.PostInstall.Title" = "Kan post-installatie stappen niet uitvoeren";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "Het archief \"%@\" is beschadigd en kan niet worden uitgepakt.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "Het archief \"%@\" kan niet worden uitgepakt omdat het huidige volume niet voldoende vrije schijf ruimte heeft.\n\nMaak meer ruimte beschikbaar om het archief uit te pakken en installeer dan Xcode %@ opnieuw om de installatie voort te zetten.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Het is mislukt om Xcode te verplaatsen naar de %@ map.";
+"InstallationError.FailedSecurityAssessment" = "De Xcode %@ veiligheidsbeoordeling was onsuccesvol en gaf de volgende melding:\n%@\nXcode blijft geïnstalleerd op %@ als je deze toch wilt gebruiken.";
+"InstallationError.CodesignVerifyFailed" = "De gedownloade Xcode is mislukt voor verificatie van code-ondertekening met de volgende melding:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "De gedownloade Xcode heeft niet de verwachte code-ondertekeningsidentiteit.\nVerkregen:\n%@\n%@\nVerwacht:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes heeft (nog) geen ondersteuning om Xcode te installeren vanaf het %@ bestandsformaat.";
+"InstallationError.MissingSudoerPassword" = "Wachtwoord ontbreekt. Probeer het opnieuw.";
+"InstallationError.UnavailableVersion" = "Kan versie %@ niet vinden.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Geen niet-prerelease versies beschikbaar.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Geen prerelease versies beschikbaar.";
+"InstallationError.MissingUsernameOrPassword" = "Ontbrekende gebruikersnaam of wachtwoord. Probeer het opnieuw.";
+"InstallationError.VersionAlreadyInstalled" = "%@ is al geïnstalleerd op %@";
+"InstallationError.InvalidVersion" = "%@ is geen geldig versie nummer.";
+"InstallationError.VersionNotInstalled" = "%@ is niet geïnstalleerd.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "De installatie is voltooid, maar sommige post-installatie taken zijn niet uitgevoerd. Deze taken zullen worden uitgevoerd wanneer je Xcode %@ voor het eerst start.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "De installatie is voltooid, maar sommige post-installatie taken zijn niet uitgevoerd. Xcodes voert deze taken uit door middel van de privileged helper, maar deze lijkt niet te zijn geïnstalleerd. Je kunt deze installeren vanaf Voorkeuren > Geavanceerd.\n\nDeze taken zullen worden uitgevoerd wanneer je Xcode %@ voor het eerst start.";
+
+// Installation Steps
+"Downloading" = "Downloaden";
+"Unarchiving" = "Dearchiveren (Dit kan even duren)";
+"Moving" = "Verplaatsen naar %@";
+"TrashingArchive" = "Bezig met archief naar Prullenbak verplaatsen";
+"CheckingSecurity" = "Beveiliging verificatie";
+"Finishing" = "Finishing";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Nieuwe versie beschikbaar";
+"Notification.FinishedInstalling" = "Installatie voltooid";
+
+
+"HelperClient.error" = "Kan niet communiceren met de privileged helper.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Nieuwe Xcode versies";
+"Notification.NewXcodeVersion.Body" = "Nieuwe Xcode versies zijn beschikbaar om te downloaden.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/pl.lproj/Localizable.strings b/Xcodes/Resources/pl.lproj/Localizable.strings
new file mode 100644
index 0000000..c505451
--- /dev/null
+++ b/Xcodes/Resources/pl.lproj/Localizable.strings
@@ -0,0 +1,239 @@
+// Menu
+"Menu.About" = "O Xcodes";
+"Menu.CheckForUpdates" = "Sprawdź aktualizacje...";
+"Menu.Acknowledgements" = "Podziękowania Xcodes";
+"Menu.GitHubRepo" = "Repozytorium Xcodes na GitHubie";
+"Menu.ReportABug" = "Zgłoś błąd";
+"Menu.RequestNewFeature" = "Poproś o nową funkcję";
+
+// Common
+"Install" = "Zainstaluj";
+"InstallDescription" = "Zainstaluj tę wersję";
+"RevealInFinder" = "Pokaż w Finderze";
+"Active" = "Aktywny";
+"MakeActive" = "Ustaw jako aktywny";
+"Open" = "Otwórz";
+"OpenDescription" = "Otwórz tę wersję";
+"CopyPath" = "Kopiuj ścieżkę";
+"CreateSymLink" = "Utwórz Symlink jako Xcode.app";
+"CreateSymLinkBeta" = "Utwórz Symlink jako Xcode-Beta.app";
+"Uninstall" = "Odinstaluj";
+"Selected" = "Wybrany";
+"Select" = "Wybierz";
+"Cancel" = "Anuluj";
+"Next" = "Dalej";
+"Continue" = "Kontynuuj";
+"Close" = "Zamknij";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Takie same wersje";
+"IdenticalBuilds.help" = "Czasami wersja przedpremierowa i wydana są dokładnie takie same. Xcodes automatycznie wyświetla te wersje razem.";
+
+"ReleaseDate" = "Data wydania";
+"ReleaseNotes" = "Notatki wydania";
+"ReleaseNotes.help" = "Wyświetl notatki wydania";
+"CopyReleaseNoteURL" = "Kopiuj adres URL";
+"Compatibility" = "Kompatybilność";
+"MacOSRequirement" = "Wymaga systemu macOS %@ lub nowszego";
+"SDKs" = "SDK";
+"Compilers" = "Kompilatory";
+"DownloadSize" = "Wielkość pobierania";
+"NoXcodeSelected" = "Nie wybrano Xcode";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Krok %1$d z %2$d: %3$@";
+"DownloadingPercentDescription" = "Pobieranie: %d%% ukończone";
+"StopInstallation" = "Przerwij instalację";
+"DownloadingError" = "Nie znaleziono informacji o pobieraniu";
+
+// About
+"VersionWithBuild" = "Wersja %@ (%@)";
+"GithubRepo" = "Repozytorium GitHub";
+"Acknowledgements" = "Podziękowania";
+"UnxipExperiment" = "Eksperyment Unxip";
+"License" = "Licencja";
+
+// General Preference Pane
+"General" = "Ogólne";
+"AppleID" = "Apple ID";
+"SignIn" = "Zaloguj się";
+"Notifications" = "Powiadomienia";
+
+// Updates Preference Pane
+"Updates" = "Aktualizacje";
+"Versions" = "Wersje";
+"AutomaticInstallNewVersion" = "Automatycznie instaluj nowe wersje Xcode";
+"IncludePreRelease" = "Dołącz wersje beta/wstępne";
+"AppUpdates" = "Aktualizacje Xcodes.app";
+"CheckForAppUpdates" = "Automatycznie sprawdzaj aktualizacje aplikacji";
+"CheckNow" = "Sprawdź teraz";
+"LastChecked" = "Ostatnio sprawdzono: %@";
+"Never" = "Nigdy";
+
+// Download Preference Pane
+"Downloads" = "Pobieranie";
+"DataSource" = "Źródło danych";
+"DataSourceDescription" = "Źródło danych Apple przeszukuje stronę internetową Apple Developer. Zawsze wyświetla najnowsze dostępne wydania, ale jest bardziej podatne na problemy.\n\nXcode Releases to nieoficjalna lista wydań Xcode. Udostępnia ona dobrze sformatowane dane, zawiera dodatkowe informacje, które nie są łatwo dostępne w Apple, i jest mniej podatna na problemy, gdy Apple zmienia swoją stronę dla deweloperów.";
+"Downloader" = "Pobieranie";
+"DownloaderDescription" = "aria2 używa do pobierania do 16 połączeń, co pozwala na pobieranie Xcode 3-5x szybciej niż URLSession. Jest on dostarczany jako plik wykonywalny wraz ze swoim kodem źródłowym w ramach Xcodes w celu przestrzegania jego licencji GPLv2.\n\nURLSession to domyślne API Apple do tworzenia żądań URL.";
+
+// Advanced Preference Pane
+"Advanced" = "Zaawansowane";
+"LocalCachePath" = "Katalog cache";
+"LocalCachePathDescription" = "Xcodes przechowuje w cache dostępne wersje Xcode oraz tymczasowo pobiera nowe pliki do tego katalogu.";
+"Change" = "Zmień";
+"Active/Select" = "Aktywny/wybrany";
+"InstallDirectory" = "Katalog instalacji";
+"InstallPathDescription" = "Xcodes wyszukuje i instaluje w jednym katalogu. Domyślnie (i zalecane) jest to /Applications. Wszelkie zmiany w miejscu przechowywania Xcode mogą spowodować, że inne aplikacje/usługi przestaną działać.";
+
+"OnSelectDoNothing" = "Zachowaj nazwę jako Xcode-X.X.X.app";
+"OnSelectDoNothingDescription" = "Po wybraniu, nazwa pozostanie w formacie z numerem wersji, np. Xcode-13.4.1.app";
+"AutomaticallyCreateSymbolicLink" = "Automatycznie twórz symboliczne połączenie do Xcode.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "Przy ustawianiu Xcode jako aktywny/wybrany, próbuj automatycznie stworzyć symboliczne połączenie o nazwie Xcode.app w katalogu instalacji.";
+"OnSelectRenameXcode" = "Zawsze zmieniaj nazwę na Xcode.app";
+"OnSelectRenameXcodeDescription" = "Po wybraniu, automatycznie zmienia aktywną wersję Xcode na Xcode.app, a poprzednie Xcode.app zostanie zmienione na nazwę wersji.";
+
+"PrivilegedHelper" = "Helper z uprawnieniami";
+"PrivilegedHelperDescription" = "Xcodes używa oddzielnego helpera z uprawnieniami do wykonywania zadań jako root. Są to rzeczy, które wymagałyby użycia komendy sudo w wierszu poleceń, w tym kroki po instalacji oraz przełączanie wersji Xcode za pomocą xcode-select.\n\nZostaniesz poproszony o podanie hasła do swojego konta macOS, aby go zainstalować.";
+"HelperInstalled" = "Helper jest zainstalowany";
+"HelperNotInstalled" = "Helper nie jest zainstalowany";
+"InstallHelper" = "Zainstaluj helpera";
+
+"ShowOpenInRosetta" = "Pokaż opcję Otwórz w Rosetta";
+"ShowOpenInRosettaDescription" = "Opcja Otwórz w Rosetta będzie wyświetlana, gdy dostępne są inne funkcje \"Otwórz\". Uwaga: Będzie to działać tylko na urządzeniach z Apple Silicon.";
+
+// Experiment Preference Pane
+"Experiments" = "Eksperymenty";
+"FasterUnxip" = "Szybsze Unxip";
+"UseUnxipExperiment" = "Podczas rozpakowywania, użyj wersji eksperymentalnej";
+"FasterUnxipDescription" = "Dzięki @_saagarjha, wersja eksperymentalna może zwiększyć prędkość rozpakowywania o nawet 70% dla niektórych systemów.\n\nWięcej informacji na temat tego, jak to jest osiągane, można znaleźć w repozytorium unxip - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Dostęp przyznany. Będziesz otrzymywać powiadomienia z Xcodes.";
+"AccessDenied" = "⚠️ Dostęp zabroniony ⚠️\n\nOtwórz ustawienia powiadomień i wybierz Xcodes, jeśli chcesz zezwolić na dostęp.";
+"NotificationSettings" = "Ustawienia powiadomień";
+"EnableNotifications" = "Włącz powiadomienia";
+
+// SignIn
+"SignInWithApple" = "Zaloguj się przy użyciu Apple ID.";
+"AppleID" = "AppleID:";
+"Password" = "Hasło:";
+"Required" = "Wymagane";
+"SignOut" = "Wyloguj się";
+
+// SMS/2FA
+"DigitCodeDescription" = "Wprowadź %d-cyfrowy kod z jednego ze swoich zaufanych urządzeń:";
+"SendSMS" = "Wyślij SMS";
+"EnterDigitCodeDescription" = "Wprowadź %d-cyfrowy kod wysłany do %@: ";
+"SelectTrustedPhone" = "Wybierz zaufany numer telefonu, aby otrzymać %d-cyfrowy kod przez SMS:";
+"NoTrustedPhones" = "Twoje konto nie ma żadnych zaufanych numerów telefonów, ale są one wymagane do autoryzacji dwuskładnikowej.\n\nZobacz https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Zaktualizowano o";
+
+// ToolBar
+"Login" = "Zaloguj się";
+"LoginDescription" = "Otwórz okno logowania";
+"Refresh" = "Odśwież";
+"RefreshDescription" = "Odśwież listę Xcode";
+"All" = "Wszystkie";
+"Release" = "Stabilne";
+"ReleaseOnly" = "Tylko stabilne";
+"Beta" = "Beta";
+"BetaOnly" = "Tylko beta";
+"Filter" = "Filtr";
+"FilterAvailableDescription" = "Filtruj dostępne wersje";
+"FilterInstalledDescription" = "Filtruj zainstalowane wersje";
+"Info" = "Informacje";
+"InfoDescription" = "Pokaż lub ukryj okno informacji";
+"Preferences" = "Ustawienia";
+"PreferencesDescription" = "Otwórz ustawienia";
+"Search" = "Szukaj...";
+"SearchDescription" = "Przeszukaj listę";
+
+// List
+"ActiveVersionDescription" = "To jest aktywna wersja";
+"MakeActiveVersionDescription" = "Ustaw jako aktywną wersję";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Odinstalować Xcode %@?";
+"Alert.Uninstall.Message" = "Zostanie przeniesiony do kosza, ale nie zostanie opróżniony.";
+"Alert.Uninstall.Error.Title" = "Nie można odinstalować Xcode";
+"Alert.Uninstall.Error.Message.FileNotFound" = "Nie znaleziono pliku \"%@\".";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Czy na pewno chcesz przerwać instalację Xcode %@?";
+"Alert.CancelInstall.Message" = "Postęp zostanie utracony.";
+"Alert.CancelInstall.PrimaryButton" = "Przerwij instalację";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Helper z uprawnieniami";
+"Alert.PrivilegedHelper.Message" = "Xcodes używa oddzielnego helpera z uprawnieniami, aby wykonywać zadania jako root. Są to rzeczy, które wymagałyby sudo w wierszu poleceń, w tym kroki po instalacji i przełączanie wersji Xcode z xcode-select.\n\nBędziesz proszony o podanie hasła do konta macOS, aby go zainstalować.";
+"Alert.PrivilegedHelper.Error.Title" = "Nie można zainstalować helpera";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Niezgodność z minimalnymi wymaganiami";
+"Alert.MinSupported.Message" = "Xcode %@ wymaga macOS %@, a ty masz macOS %@. Czy nadal chcesz go zainstalować?";
+
+// Install
+"Alert.Install.Error.Title" = "Nie można zainstalować Xcode";
+"Alert.InstallArchive.Error.Title" = "Nie można zainstalować zarchiwizowanego Xcode";
+
+// Update
+"Alert.Update.Error.Title" = "Nie można zaktualizować wybranej wersji Xcode";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Nie można wybrać Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Nie można utworzyć linku symbolicznego";
+"Alert.SymLink.Message" = "Xcode.app istnieje i nie jest linkiem symbolicznym";
+
+// Post install
+"Alert.PostInstall.Title" = "Nie można wykonać czynności po-instalacyjnych";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "Archiwum \"%@\" jest uszkodzone i nie może zostać rozpakowane.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "Nie można rozpakować archiwum \"%@\", ponieważ wolumin nie ma wystarczającej ilości wolnego miejsca.\n\nZrób więcej miejsca, aby rozpakować archiwum, a następnie zainstaluj ponownie Xcode %@, aby kontynuować instalację od miejsca, w którym została wstrzymana.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Nie udało się przenieść Xcode do katalogu %@.";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ nie przeszedł testu bezpieczeństwa z następującym wynikiem:\n%@\nPozostaje zainstalowany w %@, jeśli mimo to chcesz go używać.";
+"InstallationError.CodesignVerifyFailed" = "Pobrany Xcode nie przeszedł weryfikacji sygnatury kodu z wynikiem:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "Pobrany Xcode nie ma oczekiwanej sygnatury kodu.\nOtrzymano:\n%@\n%@\nOczekiwano:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes nie obsługuje (jeszcze) instalowania Xcode z formatu %@.";
+
+"InstallationError.MissingSudoerPassword" = "Brak hasła. Spróbuj ponownie.";
+"InstallationError.UnavailableVersion" = "Nie można znaleźć wersji %@.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Brak wersji nie-przedpremierowych.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Brak wersji przedpremierowych.";
+"InstallationError.MissingUsernameOrPassword" = "Brak nazwy użytkownika lub hasła. Spróbuj ponownie.";
+"InstallationError.VersionAlreadyInstalled" = "%@ jest już zainstalowany w %@.";
+"InstallationError.InvalidVersion" = "%@ nie jest prawidłowym numerem wersji.";
+"InstallationError.VersionNotInstalled" = "%@ nie jest zainstalowany.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "Instalacja została zakończona, ale niektóre kroki po instalacji nie zostały wykonane automatycznie. Zostaną one wykonane przy pierwszym uruchomieniu Xcode %@.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "Instalacja została zakończona, ale niektóre kroki po instalacji nie zostały wykonane automatycznie. Xcodes wykonuje te kroki z pomocą helpera z uprawnieniami, który nie jest zainstalowany. Możesz go zainstalować w Ustawienia > Zaawansowane.\n\nTe kroki zostaną wykonane przy pierwszym uruchomieniu Xcode %@.";
+
+// Installation Steps
+"Downloading" = "Pobieranie";
+"Unarchiving" = "Rozpakowywanie (To może chwilę potrwać)";
+"Moving" = "Przenoszenie do %@";
+"TrashingArchive" = "Przenoszenie archiwum do kosza";
+"CheckingSecurity" = "Weryfikacja zabezpieczeń";
+"Finishing" = "Kończenie";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Dostępna jest nowa wersja";
+"Notification.FinishedInstalling" = "Zakończono instalację";
+
+
+"HelperClient.error" = "Nie można komunikować się z helperem z uprawnieniami.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Nowe wersje Xcode";
+"Notification.NewXcodeVersion.Body" = "Dostępne są nowe wersje Xcode do pobrania.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Wesołego WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/pt-BR.lproj/Localizable.strings b/Xcodes/Resources/pt-BR.lproj/Localizable.strings
new file mode 100644
index 0000000..9eb73a3
--- /dev/null
+++ b/Xcodes/Resources/pt-BR.lproj/Localizable.strings
@@ -0,0 +1,227 @@
+// Menu
+"Menu.About" = "Sobre Xcodes";
+"Menu.CheckForUpdates" = "Verificar atualizações...";
+"Menu.Acknowledgements" = "Menções de Xcodes";
+"Menu.GitHubRepo" = "Repositório Xcodes no GitHub";
+"Menu.ReportABug" = "Reportar um bug";
+"Menu.RequestNewFeature" = "Requerir uma nova funcionalidade";
+
+// Common
+"Install" = "Instalar";
+"InstallDescription" = "Instalar esta versão";
+"RevealInFinder" = "Abrir no Finder";
+"Active" = "Ativo";
+"MakeActive" = "Ativar";
+"Open" = "Abrir";
+"OpenDescription" = "Abrir essa versão";
+"CopyPath" = "Copiar caminho";
+"CreateSymLink" = "Criar Symlink como Xcode.app";
+"Uninstall" = "Desinstalar";
+"Selected" = "Selecionado(s)";
+"Select" = "Selecionar";
+"Cancel" = "Cancelar";
+"Next" = "Próximo";
+"Continue" = "Continuar";
+"Close" = "Fechar";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Builds idênticos";
+"IdenticalBuilds.help" = "As vezes, uma versão pré-lançamento e uma versão de lançamento são exatemente o mesmo build. Xcodes mostrará essas versões juntas automaticamente.";
+
+"ReleaseDate" = "Data de lançamento";
+"ReleaseNotes" = "Notas de lançamento";
+"ReleaseNotes.help" = "Visualizar notas de lançamento";
+"Compatibility" = "Compatibilidade";
+"MacOSRequirement" = "Necessário macOS %@ ou mais recente";
+"SDKs" = "SDKs";
+"Compilers" = "Compiladores";
+"DownloadSize" = "Tamanho do download";
+"NoXcodeSelected" = "Nenhum Xcode selecionado";
+
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Passo %1$d de %2$d: %3$@";
+"DownloadingPercentDescription" = "Baixando: %d%% finalizado";
+"StopInstallation" = "Interromper instalação";
+"DownloadingError" = "Nenhuma informação de download encontrada";
+
+// About
+"VersionWithBuild" = "Versão %@ (%@)";
+"GithubRepo" = "Repositório GitHub";
+"Acknowledgements" = "Menções";
+"UnxipExperiment" = "Experimento Unxip";
+"License" = "Licensa";
+
+// General Preference Pane
+"General" = "Ajustes";
+"AppleID" = "Apple ID";
+"SignIn" = "Entrar";
+"Notifications" = "Notificações";
+
+// Updates Preference Pane
+"Updates" = "Atualizações";
+"Versions" = "Versões";
+"AutomaticInstallNewVersion" = "Instalar novas versões do Xcode automaticamente";
+"IncludePreRelease" = "Incluir versōes pré-lançamento/beta";
+"AppUpdates" = "Atualizações de Xcodes.app";
+"CheckForAppUpdates" = "Verificar atualizações do app automaticamente";
+"CheckNow" = "Verificar agora";
+"LastChecked" = "Ultima vez verificado: %@";
+"Never" = "Nunca";
+
+// Advanced Preference Pane
+"Advanced" = "Avançado";
+"LocalCachePath" = "Caminho de caches local";
+"LocalCachePathDescription" = "Xcodes faz caches de versões disponíveis do Xcode e baixa temporariamente estas novas versões para um diretório.";
+"Change" = "Alterar";
+"Active/Select" = "Ativo/Selecionar";
+"AutomaticallyCreateSymbolicLink" = "Criar link simbólico para o Xcode.app automaticamente";
+"AutomaticallyCreateSymbolicLinkDescription" = "Quando ativar/selecionar uma versão do Xcode, tentar criar um link simbólico chamado Xcode.app no diretório de instalação";
+"DataSource" = "Fonte de dados";
+"DataSourceDescription" = "A fonte de dados da Apple copia o site Apple Developer. Sempre mostrará os lançamentos mais recentes que estão disponíveis, porém é mais frágil.\n\nLançamentos do Xcode (Xcode Releases) é uma lista de lançamentos do Xcode não-oficial. É provido como dado formatado, contem informação extra que não está prontamente disponível pela Apple, e é menos provável que quebre se a Apple redesenhar o seu site de desenvolvedores.";
+"Downloader" = "Baixador";
+"DownloaderDescription" = "aria2 usa até 16 conexões para baixar o Xcode 3-5x mais rápido que URLSession. Está empacotado como um executável junto com o seu código fonte dentro do Xcodes para conformar com a licensa GPLv2.\n\nURLSession é a API padrão da Apple para performar requisições URL.";
+"PrivilegedHelper" = "Ajudante privilegiado";
+"PrivilegedHelperDescription" = "Xcodes usa um ajudante privilegiado separado para performar atividades como raiz. São atividades que iriam requerir sudo na linha de comando, incluindo passos pós-instalação e trocar versões do Xcode com xcode-select.\n\nVocê será pedido para instalá-lo na sua conta do macOS.";
+"HelperInstalled" = "Ajudante está instalado";
+"HelperNotInstalled" = "Ajudante não está instalado";
+"InstallHelper" = "Instalar ajudante";
+"OnSelectDoNothing" = "Manter nome como Xcode-X.X.X.app";
+"OnSelectRenameXcode" = "Sempre renomear para Xcode.app";
+"OnSelectRenameXcodeDescription" = "Ao selecionar, tentar renomear o Xcode ativo para Xcode.app, renomeando o Xcode.app anterior para um nome com a versão.";
+"ShowOpenInRosetta" = "Mostrar opçao abrir com Rosetta";
+"ShowOpenInRosettaDescription" = "Opção de abrir com o Rosetta será mostrar onde outras opções \"Abrir\" estão disponíveis. Nota: Isso só será mostrado pra máquinas Apple Silicon.";
+
+// Experiment Preference Pane
+"Experiments" = "Experiments";
+"FasterUnxip" = "Unxip mais rápido";
+"UseUnxipExperiment" = "Quando performar unxipping, use experiment";
+"FasterUnxipDescription" = "Graças à @_saagarjha, esse experimento pode acelerar o unxip em até 70% para algum sistemas.\n\nMais informações sobre como isso ocorre pode ser encontrada no repositório do unxip - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Acesso autorizado. Você receberá notificações de Xcodes.";
+"AccessDenied" = "⚠️ Acesso negado ⚠️\n\nPor favor, abra suas configurações de notificação e selecione Xcodes se você deseja autorizar o acesso.";
+"NotificationSettings" = "Configurações de notificação";
+"EnableNotifications" = "Ativar notificações";
+
+// SignIn
+"SignInWithApple" = "Entrar com o seu Apple ID.";
+"AppleID" = "AppleID:";
+"Password" = "Senha:";
+"Required" = "Obrigatório";
+"SignOut" = "Sair";
+
+// SMS/2FA
+"DigitCodeDescription" = "Insira o código de %d dígitos de um de seus dispositivos confiáveis:";
+"SendSMS" = "Enviar SMS";
+"EnterDigitCodeDescription" = "Insira o código de %d dígitos enviado para %@: ";
+"SelectTrustedPhone" = "Selecione um número de telefone confiável para receber um código de %d dígitos via SMS:";
+"NoTrustedPhones" = "Sua conta não possui nenhum telefone confiável, mas é necessário para autenticação de dois fatores.\n\nVer https://support.apple.com/en-ca/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Atualizado em";
+
+// ToolBar
+"Login" = "Login";
+"LoginDescription" = "Abrir login";
+"Refresh" = "Atualizar";
+"RefreshDescription" = "Atualizar lista de Xcode";
+"All" = "Todos";
+"Release" = "Release";
+"ReleaseOnly" = "Somente release";
+"Beta" = "Beta";
+"BetaOnly" = "Somente beta";
+"Filter" = "Filtrar";
+"FilterAvailableDescription" = "Filtrar versões disponíveis";
+"FilterInstalledDescription" = "Filtrar versões instaladas";
+"Info" = "Informação";
+"InfoDescription" = "Mostrar ou esconder o painel de informações";
+"Preferences" = "Preferências";
+"PreferencesDescription" = "Abrir preferências";
+"Search" = "Procurar...";
+"SearchDescription" = "Lista de procura";
+
+// List
+"ActiveVersionDescription" = "Essa é a versão ativa";
+"MakeActiveVersionDescription" = "Ativar esta versão";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Desinstalar Xcode %@?";
+"Alert.Uninstall.Message" = "Será movido para a lixeira, mas não será esvaziada.";
+"Alert.Uninstall.Error.Title" = "Não foi possível desinstalar o Xcode";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Tem certeza que deseja interromper a instalação do Xcode %@?";
+"Alert.CancelInstall.Message" = "Todo progresso será descartado.";
+"Alert.CancelInstall.PrimaryButton" = "Interromper instalação";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Ajudante privilegiado";
+"Alert.PrivilegedHelper.Message" = "Xcodes usa um ajudante privilegiado separado para realizar tarefas como root (raíz). São tarefas onde seria necessário permissão de super usuário (sudo) na linha de comando, incluindo comandos de pós-instalação e trocar versões de Xcode com xcode-select.\n\nVocê deverá inserir sua senha do macOS para instalá-lo.";
+"Alert.PrivilegedHelper.Error.Title" = "Não foi possível instalar o ajudante";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Requerimentos mínimos não satisfeitos.";
+"Alert.MinSupported.Message" = "Xcode %@ requere MacOS %@, mas você está rodando MacOS %@, você ainda quer instalá-lo?";
+
+// Install
+"Alert.Install.Error.Title" = "Não foi possível instalar o Xcode";
+"Alert.InstallArchive.Error.Title" = "Não foi possível instalar o Xcode arquivado";
+
+// Update
+"Alert.Update.Error.Title" = "Não foi possível atualizar o Xcode selecionado";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Não foi possível selecionar Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Não foi possível criar link simbólico";
+"Alert.SymLink.Message" = "Xcode.app existe e não é um link simbólico";
+
+// Post install
+"Alert.PostInstall.Title" = "Não foi possível realizar os comandos de pós-instalação";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "O arquivamento \"%@\" está danificado e não pode ser expandido.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "O arquivamento \"%@\" não pode ser expandido porquê o volume atual não possui espaço disponível o suficiente.\n\nLibere espaço para expandir o arquivamento e então instale o Xcode %@ novamente para iniciar uma instalação de onde você parou.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Falha ao mover Xcode para o diretório: %@";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ falhou suas checagens de segurança com a seguinte saída:\n%@\nAinda está instalado em %@ se você deseja usar ainda assim.";
+"InstallationError.CodesignVerifyFailed" = "O Xcode baixado falhou a verificação de assinatura de código (code signing) com a seguinte saída:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "O Xcode baixado não possui a identidade de assinatura de código esperada.\nPossui:\n%@\n%@\nEsperada:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes (ainda) não suporta instalação de Xcode no formato de arquivo %@.";
+"InstallationError.MissingSudoerPassword" = "Faltando senha. Por favor, tente novamente.";
+"InstallationError.UnavailableVersion" = "Não foi possível encontrar versão %@.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Nenhuma versão não-pré-lançamento disponível.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Nenhuma versão de pré-lançamento disponível.";
+"InstallationError.MissingUsernameOrPassword" = "Faltando usuário ou senha. Por favor, tente novamente.";
+"InstallationError.VersionAlreadyInstalled" = "%@ já está instalada em %@";
+"InstallationError.InvalidVersion" = "%@ não é uma versão válida.";
+"InstallationError.VersionNotInstalled" = "%@ não está instalada.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "A instalação foi completada, mas alguns passos de pós-instalação não puderam ser performados automaticamente. Estes serão performados quando você rodar o Xcode %@ pela primeira vez.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "A instalação foi completada, mas alguns passos de pós-instalação não puderam ser performados automaticamente. Xcodes performa estes passos com o ajudante privilegiado, que aparentemente não está instalado. Você pode instalá-lo em Preferências > Avançado.\n\nEstes passos serão performados quando você rodar o Xcode %@ pela primeira vez.";
+
+// Installation Steps
+"Downloading" = "Baixando";
+"Unarchiving" = "Desarquivando (Pode demorar um pouco)";
+"Moving" = "Movendo para %@";
+"TrashingArchive" = "Movendo arquivo para a lixeira";
+"CheckingSecurity" = "Verificação de segurança";
+"Finishing" = "Finalizando";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Nova versão disponível";
+"Notification.FinishedInstalling" = "Instalação finalizada";
+
+
+"HelperClient.error" = "Não foi possível se comunicar com o ajudante.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Novas versões do Xcode";
+"Notification.NewXcodeVersion.Body" = "Novas versões do Xcode estão disponíveis para baixar";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Feliz WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/ru.lproj/Localizable.strings b/Xcodes/Resources/ru.lproj/Localizable.strings
new file mode 100644
index 0000000..e785967
--- /dev/null
+++ b/Xcodes/Resources/ru.lproj/Localizable.strings
@@ -0,0 +1,234 @@
+// Menu
+"Menu.About" = "О Xcodes";
+"Menu.CheckForUpdates" = "Проверить наличие обновлений...";
+"Menu.Acknowledgements" = "Юридическая информация Xcodes";
+"Menu.GitHubRepo" = "GitHub-репозиторий Xcodes";
+"Menu.ReportABug" = "Сообщить об ошибке";
+"Menu.RequestNewFeature" = "Запросить новую функцию";
+
+// Common
+"Install" = "Установить";
+"InstallDescription" = "Установить эту версию";
+"RevealInFinder" = "Показать в Finder";
+"Active" = "Активно";
+"MakeActive" = "Активировать";
+"Open" = "Открыть";
+"OpenDescription" = "Открыть эту версию";
+"CopyPath" = "Копировать путь";
+"CreateSymLink" = "Создать символическую ссылку к Xcode.app";
+"CreateSymLinkBeta" = "Создать символическую ссылку к Xcode-Beta.app";
+"Uninstall" = "Удалить";
+"Selected" = "Выбрано";
+"Select" = "Выбрать";
+"Cancel" = "Отмена";
+"Next" = "Далее";
+"Continue" = "Продолжить";
+"Close" = "Закрыть";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Идентичные выпуски";
+"IdenticalBuilds.help" = "Иногда предварительная и релизная версии представляют собой один и тот же выпуск. Xcodes автоматически отображает эти версии вместе.";
+
+"ReleaseDate" = "Дата выпуска";
+"ReleaseNotes" = "Примечания к выпуску";
+"ReleaseNotes.help" = "Просмотреть примечания к выпуску";
+"CopyReleaseNoteURL" = "Копировать URL";
+"Compatibility" = "Совместимость";
+"MacOSRequirement" = "Требуется macOS %@ или новее";
+"SDKs" = "SDK";
+"Compilers" = "Компиляторы";
+"DownloadSize" = "Размер загрузки";
+"NoXcodeSelected" = "Xcode не выбран";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Шаг %1$d из %2$d: %3$@";
+"DownloadingPercentDescription" = "Скачивание: %d% завершено";
+"StopInstallation" = "Остановить установку";
+"DownloadingError" = "Информация о загрузке не найдена";
+
+// About
+"VersionWithBuild" = "Версия %@ (%@)";
+"GithubRepo" = "Репозиторий GitHub";
+"Acknowledgements" = "Юридическая информация";
+"UnxipExperiment" = "Экспериментальный Unxip";
+"License" = "Лицензия";
+
+// General Preference Pane
+"General" = "Общее";
+"AppleID" = "Apple ID";
+"SignIn" = "Войти";
+"Notifications" = "Уведомления";
+
+// Updates Preference Pane
+"Updates" = "Обновления";
+"Versions" = "Версии";
+"AutomaticInstallNewVersion" = "Автоматически устанавливать новые версии Xcode";
+"IncludePreRelease" = "Включить предварительные/бета-версии";
+"AppUpdates" = "Обновления Xcodes.app";
+"CheckForAppUpdates" = "Автоматически проверять наличие обновлений";
+"CheckNow" = "Проверить сейчас";
+"LastChecked" = "Последняя проверка: %@";
+"Never" = "Никогда";
+
+// Download Preference Pane
+"Downloads" = "Загрузки";
+"DataSource" = "Источник данных";
+"DataSourceDescription" = "Источник данных Apple применяет технологию \"веб-скрейпинга\" к веб-сайту Apple для разработчиков. Он всегда показывает последние доступные выпуски, но является менее стабильным источником данных.\n\nXcode Releases — это неофициальный список выпусков Xcode. Он предоставляется в виде удобно структурированных данных, содержит дополнительную информацию, которую не всегда можно получить от Apple и который с меньшей вероятностью перестанет работать, если Apple изменит дизайн своего веб-сайта для разработчиков.";
+"Downloader" = "Загрузчик";
+"DownloaderDescription" = "aria2 использует до 16 подключений для загрузки Xcode в 3-5 раз быстрее, чем URLSession. Он поставляется в виде исполняемого файла вместе с исходным кодом в Xcodes, чтобы соответствовать лицензии GPLv2.\n\nURLSession — это API Apple по умолчанию для выполнения запросов по сети.";
+
+// Advanced Preference Pane
+"Advanced" = "Дополнительно";
+"LocalCachePath" = "Путь к локальному кешу";
+"LocalCachePathDescription" = "Xcodes кеширует доступные версии Xcode и временно загружает новые версии в указанную директорию";
+"Change" = "Изменить";
+"Active/Select" = "Активировать/Выбрать";
+"InstallDirectory" = "Путь для установки";
+"InstallPathDescription" = "Xcodes использует единый путь для поиска и установки выпусков Xcode. По умолчанию, рекомендуется оставить /Applications. Любые изменения в то, где находится Xcode, могут привести к тому, что другие приложения/сервисы могут перестать работать. ";
+
+"OnSelectDoNothing" = "Сохранять формат имени Xcode-X.X.X.app";
+"OnSelectDoNothingDescription" = "Если выбрано, формат имени будет содержать версию, прим. Xcode-13.4.1.app";
+"AutomaticallyCreateSymbolicLink" = "Автоматически создавать символическую ссылку к Xcode.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "Когда вы делаете версию Xcode активной/выбранной, будет выполнена попытка создать символическую ссылку с именем Xcode.app в директории установки";
+"OnSelectRenameXcode" = "Всегда переименовывать в Xcode.app";
+"OnSelectRenameXcodeDescription" = "Если выбрано, будет выполнена попытка переименовать активный Xcode в Xcode.app, а предыдущий Xcode.app в формат имени с версией.";
+
+"PrivilegedHelper" = "Привилегированный помощник";
+"PrivilegedHelperDescription" = "Xcodes использует отдельный привилегированный помощник для выполнения задач от имени root-пользователя. Это команды, которые потребуют sudo в командной строке, включая шаги после установки и переключение версий Xcode с помощью xcode-select.\n\nВам будет предложено указать пароль от вашей учетной записи macOS для его установки.";
+"HelperInstalled" = "Помощник установлен";
+"HelperNotInstalled" = "Помощник не установлен";
+"InstallHelper" = "Установить помощника";
+
+// Experiment Preference Pane
+"Experiments" = "Эксперименты";
+"FasterUnxip" = "Быстрый Unxip";
+"UseUnxipExperiment" = "При выполнении unxip будет использован экспериментальный функционал";
+"FasterUnxipDescription" = "Благодаря @_saagarjha этот эксперимент может увеличить скорость распаковки до 70% на некоторых системах.\n\nДополнительную информацию о том, как достигается такой результат, можно прочесть в репозитории unxip — https://github.com/saagarjha/unxip. ";
+
+// Notifications
+"AccessGranted" = "Доступ разрешен. Вы будете получать уведомления от Xcodes.";
+"AccessDenied" = "⚠️ Отказано в доступе ⚠️\n\nПожалуйста, откройте настройки уведомлений и выберите Xcodes, чтобы разрешить доступ.";
+"NotificationSettings" = "Настройки уведомлений";
+"EnableNotifications" = "Включить уведомления";
+
+// SignIn
+"SignInWithApple" = "Войти со своим Apple ID.";
+"AppleID" = "Apple ID:";
+"Password" = "Пароль:";
+"Required" = "Обязательно";
+"SignOut" = "Выход";
+
+// SMS/2FA
+"DigitCodeDescription" = "Введите %d цифровой код с одного из ваших доверенных устройств:";
+"SendSMS" = "Отправить SMS";
+"EnterDigitCodeDescription" = "Введите %d цифровой код, отправленный на %@: ";
+"SelectTrustedPhone" = "Выберите доверенный номер телефона для получения %d цифрового кода по SMS:";
+"NoTrustedPhones" = "В вашем аккаунтe нет доверенных телефонных номеров, но они необходимы для двухфакторной аутентификации.\n\nСм. https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Обновлено в";
+
+// ToolBar
+"Login" = "Логин";
+"LoginDescription" = "Открыть окно логина";
+"Refresh" = "Обновить";
+"RefreshDescription" = "Обновить список Xcode";
+"All" = "Все";
+"Release" = "Pелиз";
+"ReleaseOnly" = "Только релиз";
+"Beta" = "Бета";
+"BetaOnly" = "Только бета";
+"Filter" = "Фильтр";
+"FilterAvailableDescription" = "Отфильтровать доступные версии";
+"FilterInstalledDescription" = "Отфильтровать установленные версии";
+"Info" = "Информация";
+"InfoDescription" = "Показать или скрыть информационную панель";
+"Preferences" = "Hастройки";
+"PreferencesDescription" = "Открыть настройки";
+"Search" = "Поиск...";
+"SearchDescription" = "Поиск по списку";
+
+// List
+"ActiveVersionDescription" = "Это активная версия";
+"MakeActiveVersionDescription" = "Сделать эту версию активной";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Удалить Xcode %@?";
+"Alert.Uninstall.Message" = "Файлы будут перемещены в корзину, но не будут очищены из неё автоматически.";
+"Alert.Uninstall.Error.Title" = "Невозможно удалить Xcode";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Вы уверены, что хотите остановить установку Xcode %@?";
+"Alert.CancelInstall.Message" = "Прогресс будет сброшен.";
+"Alert.CancelInstall.PrimaryButton" = "Остановить установку";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Привилегированный помощник";
+"Alert.PrivilegedHelper.Message" = "Xcodes использует отдельный привилегированный помощник для выполнения задач от имени root-пользователя. Это команды, которые потребуют sudo в командной строке, включая шаги после установки и переключение версий Xcode с помощью xcode-select.\n\nВам будет предложено указать пароль от вашей учетной записи macOS для его установки.";
+"Alert.PrivilegedHelper.Error.Title" = "Невозможно установить помощник";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Текущая macOS не соответствует минимальным требованиям";
+"Alert.MinSupported.Message" = "Xcode %@ требует macOS %@, но вы используете macOS %@, вы все еще хотите установить?";
+
+// Install
+"Alert.Install.Error.Title" = "Невозможно установить Xcode";
+"Alert.InstallArchive.Error.Title" = "Невозможно установить заархивированный Xcode";
+
+// Update
+"Alert.Update.Error.Title" = "Невозможно обновить выбранный Xcode";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Невозможно выбрать Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Невозможно создать символическую ссылку";
+"Alert.SymLink.Message" = "Xcode.app существует и не является символической ссылкой";
+
+// Post install
+"Alert.PostInstall.Title" = "Невозможно выполнить дополнительные шаги после установки";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "Архив \"%@\" поврежден и не может быть расширен.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "Архив \"%@\" не может быть расширен, поскольку в текущем жестком диске недостаточно свободного места.\n\nОсвободите больше места для расширения архива, а затем снова установите Xcode %@, чтобы начать установку с того места, где он остановился.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Не удалось переместить Xcode в директорию %@.";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ не прошел оценку безопасности со следующим выводом:\n%@\nОн остается установленным в %@, если вы все равно хотите его использовать.";
+"InstallationError.CodesignVerifyFailed" = "Загруженный Xcode не прошел проверку подписи сертификата со следующим выводом:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "Загруженный Xcode не имеет ожидаемого идентификатора подписи сертификата.\nПолучено:\n%@\n%@\nОжидается:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes (пока) не поддерживает установку Xcode из формата файла %@.";
+"InstallationError.MissingSudoerPassword" = "Отсутствует пароль. Повторите попытку.";
+"InstallationError.UnavailableVersion" = "Не удалось найти версию %@.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Нет доступных не предварительных версий.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Предварительные версии недоступны.";
+"InstallationError.MissingUsernameOrPassword" = "Отсутствует имя пользователя или пароль. Пожалуйста, попробуйте еще раз.";
+"InstallationError.VersionAlreadyInstalled" = "%@ уже установлен в %@";
+"InstallationError.InvalidVersion" = "%@ не является допустимым номером версии.";
+"InstallationError.VersionNotInstalled" = "%@ не установлен.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "Установка завершена, но некоторые шаги после установки не были выполнены автоматически. Они будут выполнены при первом запуске Xcode %@.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "Установка была завершена, но некоторые шаги после установки не были выполнены автоматически. Xcodes выполняет эти шаги с привилегированным помощником, который, похоже, не установлен. Вы можете установить его из Настройки > Дополнительно.\n\nЭти шаги будут выполнены при первом запуске Xcode %@.";
+
+// Installation Steps
+"Downloading" = "Скачивание";
+"Unarchiving" = "Разархивирование (это может занять некоторое время)";
+"Moving" = "Перемещение в %@";
+"TrashingArchive" = "Перемещение архива в корзину";
+"CheckingSecurity" = "Проверка безопасности";
+"Finishing" = "Завершение";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Доступна новая версия";
+"Notification.FinishedInstalling" = "Завершена установка";
+
+
+"HelperClient.error" = "Невозможно установить связь с привилегированным помощником.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Новые версии Xcode";
+"Notification.NewXcodeVersion.Body" = "Новые версии Xcode доступны для скачивания.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Счастливого WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/tr.lproj/Localizable.strings b/Xcodes/Resources/tr.lproj/Localizable.strings
new file mode 100644
index 0000000..1cbb514
--- /dev/null
+++ b/Xcodes/Resources/tr.lproj/Localizable.strings
@@ -0,0 +1,238 @@
+// Menu
+"Menu.About" = "Xcodes Hakkında";
+"Menu.CheckForUpdates" = "Güncellemeleri Denetle...";
+"Menu.Acknowledgements" = "Xcodes Teşekkür";
+"Menu.GitHubRepo" = "Xcodes GitHub Repo";
+"Menu.ReportABug" = "Bir Sorun Bildir";
+"Menu.RequestNewFeature" = "Yeni Özellik Talep Et";
+
+// Common
+"Install" = "Yükle";
+"InstallDescription" = "Bu sürümü indir";
+"RevealInFinder" = "Finder'da Görüntüle";
+"Active" = "Aktif";
+"MakeActive" = "Etkinleştir";
+"Open" = "Aç";
+"OpenDescription" = "Bu sürümü aç";
+"CopyPath" = "Yolu Kopyala";
+"CreateSymLink" = "Xcode.app olarak sembolik link yarat";
+"CreateSymLinkBeta" = "Xcode-Beta.app olarak sembolik link yarat";
+"Uninstall" = "Kaldır";
+"Selected" = "Seçili";
+"Select" = "Seç";
+"Cancel" = "İptal Et";
+"Next" = "Sonraki";
+"Continue" = "Devam Et";
+"Close" = "Kapat";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Benzer Yapılar";
+"IdenticalBuilds.help" = " Bazen prerelase ve release sürümleri aynı yapı üzerindedir. Xcodes bu sürümleri birarada gösterecektir.";
+
+"ReleaseDate" = "Yayınlanma Tarihi";
+"ReleaseNotes" = "Yayınlanma Notları";
+"ReleaseNotes.help" = "Yayınlanma Notlarını Görüntüle";
+"CopyReleaseNoteURL" = "URL'yi kopyala";
+"Compatibility" = "Uyumluluk";
+"MacOSRequirement" = "macOS %@ veya sonrasını gerektirir";
+"SDKs" = "SDKler";
+"Compilers" = "Derleyiciler";
+"DownloadSize" = "İndirme Boyutu";
+"NoXcodeSelected" = "Xcode Sürümü Seçilmedi";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Adım %1$d/%2$d: %3$@";
+"DownloadingPercentDescription" = "İndirme: %d%% tamamlandı";
+"StopInstallation" = "İndirmeyi durdur";
+"DownloadingError" = "İndirme bilgisi bulunamadı";
+
+// About
+"VersionWithBuild" = "Sürüm %@ (%@)";
+"GithubRepo" = "GitHub Repo";
+"Acknowledgements" = "Teşekkür";
+"UnxipExperiment" = "Tecrübeyi çıkart";
+"License" = "Lisans";
+
+// General Preference Pane
+"General" = "Genel";
+"AppleID" = "Apple ID";
+"SignIn" = "Giriş Yap";
+"Notifications" = "Bildirimler";
+
+// Updates Preference Pane
+"Updates" = "Güncellemeler";
+"Versions" = "Sürümler";
+"AutomaticInstallNewVersion" = "Xcode'un yeni versiyonlarını otomatik yükle";
+"IncludePreRelease" = "Prerelease/beta sürümlerini dahil et";
+"AppUpdates" = "Xcodes.app Güncellemeleri";
+"CheckForAppUpdates" = "Uygulama güncellemelerini otomatik kontrol et";
+"CheckNow" = "Kontrol Et";
+"LastChecked" = "Son kontrol: %@";
+"Never" = "Daha Yeni";
+
+// Download Preference Pane
+"Downloads" = "İndirilenler";
+"DataSource" = "Veri Kaynağı";
+"DataSourceDescription" = "Apple veri kaynağı Apple'ın geliştirici sitesini inceliyor. Uygun olan en son sürümleri gösterir, ama biraz daha hassastır.\n\nXcode Releases, Xcode sürümlerinin bulunduğu resmi olmayan bir listedir. Data düzenli bir formata sahip bilgileri barındırır, Apple tarafında olmayan ek bilgileri içerir ve Apple'ın ileride sitesini değiştireceği için bozabileceği bir kaynak değildir.";
+"Downloader" = "Yükleyici";
+"DownloaderDescription" = "aria2 16ya kadar bağlantı kullanarak Xcode'u URLSession'a göre 3-5 kat daha hızlı indirir. Çalıştırılabilir bir dosya olarak Bundle'a dahildir ve kaynak dosyaları GPLv2 lisansıyla uyumlu olması açısından Xcode içerisinde yer alır.\n\nURLSession Apple'ın URL istekleri oluşturmak için sunduğu varsayılan API'dır.";
+
+// Advanced Preference Pane
+"Advanced" = "Gelişmiş";
+"LocalCachePath" = "Lokal Önbellek Konumu";
+"LocalCachePathDescription" = "Xcodes, uygun Xcode sürümlerini ve geçici yeni indirilenleri bir klasörde önbelleğe alır";
+"Change" = "Değiştir";
+"Active/Select" = "Aktif/Seç";
+"InstallDirectory" = "Yükleme Klasörü";
+"InstallPathDescription" = "Xcodes bir klasörü arayıp oraya yükler. Varsayılan(ve önerilen) yöntem /Uygulamalar klasöründe tutmaktır. Xcode'un bulunduğu ortamdaki herhangi bir değişiklik başka bir uygulamanın/servisin çalışmasını durdurabilir.";
+
+"OnSelectDoNothing" = "Uygulama ismini Xcode-X.X.X.app gibi tut.";
+"OnSelectDoNothingDescription" = "Seçildiğinde, ismi Xcode-13.4.1.app örneğindeki gibi tutar.";
+"AutomaticallyCreateSymbolicLink" = "Xcodes.app'in sembolik linkini otomatik oluştur.";
+"AutomaticallyCreateSymbolicLinkDescription" = "Bir Xcode sürümünü Aktif/Seç yaparken Xcode.app ismindeki uygulamanın sembolik linkini yükleme klasörüne otomatik oluşturmayı dene";
+"OnSelectRenameXcode" = "Her zaman Xcode.app şeklinde ismi değiştir";
+"OnSelectRenameXcodeDescription" = "Seçildiğinde, aktif olan Xcode'u Xcode.app olarak isimlendirmeye çalışır ve eski Xcode ismine sürüm ismi ekler.";
+
+"PrivilegedHelper" = "Ayrıcalıklı Yardımcı";
+"PrivilegedHelperDescription" ="Xcodes, root görevlerini yerine getirmek için bir Ayrıcalıklı yardımcı aracı kullanır. Bunlar komut satırındaki sudo gerektiren, yükleme sonrası adımlarını sağlayan ve Xcode sürümü değiştiren xcode-select gibi komutlardan ibarettir.\n\nBunu yüklemek için macOS hesap şifrenizi girmeniz istenecektir.";
+"HelperInstalled" = "Yardımcı yüklendi";
+"HelperNotInstalled" = "Yardımcı yüklenmedi";
+"InstallHelper" = "Yardımcıyı yükle";
+
+"ShowOpenInRosetta" = "Rosetta ile açma seçeneğini göster";
+"ShowOpenInRosettaDescription" = "Rosetta ile açma opsiyonu diğer uygun \"Açma\" fonksiyonlarını gösterecektir. Not: Bu sadece Apple Silikon makinelerinde gözükecektir.";
+
+// Experiment Preference Pane
+"Experiments" = "Deneyler";
+"FasterUnxip" = "Daha Hızlı Unxip";
+"UseUnxipExperiment" = "Unxip yaparken dene";
+"FasterUnxipDescription" = "@_saagarjha sayesinde bu deney bazı sistemlerde unxip sürecini %70'e kadar hızlandırabilir.\n\nBu işlemin nasıl yapıldığına dair ayrıntılara unxip repo .- https://github.com/saagarjha/unxip adresinden bakabilirsiniz";
+
+// Notifications
+"AccessGranted" = "Erişim Tanımlandı. Xcodes'dan bildirimler alacaksınız.";
+"AccessDenied" = "⚠️ Erişim Reddedildi ⚠️\n\nEğer bildirimler için erişim vermek istiyorsanız lütfen bildirim ayarlarını açın ve Xcodes uygulamasını seçin.";
+"NotificationSettings" = "Bildirim Ayarları";
+"EnableNotifications" = "Bildirimleri Etkinleştir";
+
+// SignIn
+"SignInWithApple" = "Apple ID ile Giriş Yap.";
+"AppleID" = "AppleID:";
+"Password" = "Şifre:";
+"Required" = "Gerekli";
+"SignOut" = "Oturumu Kapat";
+
+// SMS/2FA
+"DigitCodeDescription" = "Güvenilir cihazından aldığın %d rakamlı kodu gir:";
+"SendSMS" = "SMS Gönder";
+"EnterDigitCodeDescription" = "%@ kaynağından gönderilen %d rakamlı kodu gir: ";
+"SelectTrustedPhone" = "%d rakamlı kodu SMS olarak almak için güvenilir telefon numarasını seç:";
+"NoTrustedPhones" = "Hesabına tanımlı güvenli bir telefon numarası yok, fakat iki aşamalı doğrulama için gerekmektedir.\n\nDaha fazlası için https://support.apple.com/HT204915 adresine bakın.";
+
+// MainWindow
+"UpdatedAt" = "Güncellenme Zamanı:";
+
+// ToolBar
+"Login" = "Giriş";
+"LoginDescription" = "Giriş Yap";
+"Refresh" = "Yenile";
+"RefreshDescription" = "Xcode Listesini Yenile";
+"All" = "Tümü";
+"Release" = "Kararlı Sürüm";
+"ReleaseOnly" = "Yalnızca Kararlı Sürüm";
+"Beta" = "Beta";
+"BetaOnly" = "Yalnızca Beta";
+"Filter" = "Filtre";
+"FilterAvailableDescription" = "Uygun sürümleri filtrele";
+"FilterInstalledDescription" = "Yüklü sürümleri filtrele";
+"Info" = "Bilgi";
+"InfoDescription" = "Bilgi panelini göster ya da gizle";
+"Preferences" = "Tercihler";
+"PreferencesDescription" = "Tercihleri Aç";
+"Search" = "Ara...";
+"SearchDescription" = "Arama listesi";
+
+// List
+"ActiveVersionDescription" = "Bu aktif sürümdür";
+"MakeActiveVersionDescription" = "Bu sürümü aktif et";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Xcode %@ sürümünü Kaldır?";
+"Alert.Uninstall.Message" = "Uygulama Çöp Kutusuna taşınacaktır, fakat silinmeyecektir.";
+"Alert.Uninstall.Error.Title" = "Xcode Kaldırılamadı";
+"Alert.Uninstall.Error.Message.FileNotFound" = "\"%@\" dosyası bulunamadı.";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Xcode %@ sürümünün yüklenmesini durdurmak istediğinize emin misiniz?";
+"Alert.CancelInstall.Message" = "Herhangi bir ilerleme göz ardı edilecektir.";
+"Alert.CancelInstall.PrimaryButton" = "Yüklemeyi Durdur";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Ayrıcalıklı Yardımcı";
+"Alert.PrivilegedHelper.Message" = "Xcodes, root görevlerini yerine getirmek için bir Ayrıcalıklı yardımcı aracı kullanır. Bunlar komut satırındaki sudo gerektiren, yükleme sonrası adımlarını sağlayan ve Xcode sürümü değiştiren xcode-select gibi komutlardan ibarettir.\n\nBunu yüklemek için macOS hesap şifrenizi girmeniz istenecektir.";
+"Alert.PrivilegedHelper.Error.Title" = "Yardımcı yüklenemedi";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Minimum gereksinimler karşılanamadı";
+"Alert.MinSupported.Message" = "Xcode %@ macOS %@ sürümünü gerektirir, fakat siz macOS %@ sürümünü kullanıyorsunuz, yine de indirmek istediğinize emin misiniz?";
+
+// Install
+"Alert.Install.Error.Title" = "Xcode yüklenemedi";
+"Alert.InstallArchive.Error.Title" = "Arşivlenmiş Xcode yüklenemedi.";
+
+// Update
+"Alert.Update.Error.Title" = "Seçili Xcode güncellenemedi";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Xcode seçilemedi";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Uygulama sembolik linki oluşturulamadı";
+"Alert.SymLink.Message" = "Xcode.app zaten var ve bir sembolik link değil";
+
+// Post install
+"Alert.PostInstall.Title" = "Yükleme aşamaları çalıştırılamadı";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "\"%@\" arşivi hasar görmüş ve açılamıyor.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "\"%@\" arşivi açılamıyor çünkü diskte yeterli alan bulunmuyor.\n\nArşivi açmak için daha fazla alan aç ve ondan sonra kaldığın yerden devam ederek Xcode %@ sürümünü tekrar kur.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Xcode %@ klasörüne taşınamadı.";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@ güvenlik görevlerini gerçekleştirirken şu hatayı verdi:\n%@\nYine de devam etmek isterseniz uygulama %@ konumunda bulunmaya devam ediyor.";
+"InstallationError.CodesignVerifyFailed" = "İndirilmiş Xcode imza onayı işleminde şu hatayı verdi:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "İndirilmiş Xcode beklenilen imza kimliğine sahip değil.\nEldeki:\n%@\n%@\nBeklenen:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes henüz Xcodu %@ dosya formatında yüklemeye (henüz) izin vermiyor.";
+"InstallationError.MissingSudoerPassword" = "Eksik şifre. Lütfen tekrar deneyin";
+"InstallationError.UnavailableVersion" = "%@ sürümü bulunamadı.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Prerelease olmayan sürümler yok.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Prerelease sürümleri yok.";
+"InstallationError.MissingUsernameOrPassword" = "Eksik kullanıcı adı veya şifre. Lütfen tekrar deneyin.";
+"InstallationError.VersionAlreadyInstalled" = "%@ zaten %@ konumunda yüklü.";
+"InstallationError.InvalidVersion" = "%@ geçerli bir sürüm numarası değil.";
+"InstallationError.VersionNotInstalled" = "%@ yüklenmedi.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "İndirme tamamlandı, fakat bazı indirme sonrası aşamaları tamamlanamadı. Bunlar Xcode %@ uygulamasını ilk açtığınızda gerçekleşecek.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "İndirme tamamlandı, fakat bazı indirme sonrası aşamaları otomatik tamamlanamadı. Xcodes bu aşamaları bir ayrıcalıklı yardımcı aracı ile gerçekleştirir, ama bu araç yüklenmemiş gözüküyor. Yüklemek için Tercihler > Gelişmiş yolunu kullanabilirsiniz. \n\nBunlar Xcode %@ uygulamasını ilk açtığınızda gerçekleşecek.";
+
+// Installation Steps
+"Downloading" = "İndiriliyor";
+"Unarchiving" = "Arşivden Çıkarılıyor (Bu biraz sürebilir)";
+"Moving" = "%@ Konumuna Taşınıyor";
+"TrashingArchive" = "Arşiv Çöp Kutusuna taşınıyor";
+"CheckingSecurity" = "Güvenlik doğrulaması";
+"Finishing" = "Tamamlanıyor";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Yeni sürüm mevcut";
+"Notification.FinishedInstalling" = "İndirme tamamlandı";
+
+
+"HelperClient.error" = "Yardımcı araç ile iletişim kurulamadı.";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Yeni Xcode sürümü";
+"Notification.NewXcodeVersion.Body" = "Yeni Xcode sürümleri indirmek için mevcut.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 WWDC %@ haftanız kutlu olsun! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/uk.lproj/Localizable.strings b/Xcodes/Resources/uk.lproj/Localizable.strings
new file mode 100644
index 0000000..8dd8df7
--- /dev/null
+++ b/Xcodes/Resources/uk.lproj/Localizable.strings
@@ -0,0 +1,225 @@
+// Menu
+"Menu.About" = "Про Xcodes";
+"Menu.CheckForUpdates" = "Перевірити Оновлення...";
+"Menu.Acknowledgements" = "Подяки";
+"Menu.GitHubRepo" = "Xcodes GitHub Repo";
+"Menu.ReportABug" = "Повідомити про помилку";
+"Menu.RequestNewFeature" = "Запит на нову Фічу";
+
+// Common
+"Install" = "Встановити";
+"InstallDescription" = "Встановити цю версію";
+"RevealInFinder" = "Показати у Finder";
+"Active" = "Активний";
+"MakeActive" = "Зробити активним";
+"Open" = "Запустити";
+"OpenDescription" = "Запустити цю версію";
+"CopyPath" = "Скопіювати шлях";
+"CreateSymLink" = "Створити символічну ссилку як Xcode.app";
+"CreateSymLinkBeta" = "Створити символічну ссилку як Xcode-Beta.app";
+"Uninstall" = "Видалити";
+"Selected" = "Обрано";
+"Select" = "Обрати";
+"Cancel" = "Відміна";
+"Next" = "Далі";
+"Continue" = "Продовжити";
+"Close" = "Закрити";
+"OK" = "OK";
+
+// Info Pane
+"IdenticalBuilds" = "Ідентичні Білди";
+"IdenticalBuilds.help" = "Буває що prerelease та release насправді ідентичні (то й же Build). У такому разі Xcodes будуть показувати обидві версії разом";
+
+"ReleaseDate" = "Дата релізу";
+"ReleaseNotes" = "Деталі релізу";
+"ReleaseNotes.help" = "Переглянути деталі релізу";
+"Compatibility" = "Сумісність";
+"MacOSRequirement" = "Потрібен macOS %@ чи новіший";
+"SDKs" = "SDKs";
+"Compilers" = "Компілятори";
+"DownloadSize" = "Розмір завантаження";
+"NoXcodeSelected" = "Не обрано Xcode";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "Крок %1$d з %2$d: %3$@";
+"DownloadingPercentDescription" = "Закачка: %d%% готово";
+"StopInstallation" = "Зупинити встановлення";
+"DownloadingError" = "Не знайдено інформації для закачування";
+
+// About
+"VersionWithBuild" = "Версія %@ (%@)";
+"GithubRepo" = "GitHub репозиторій";
+"Acknowledgements" = "Подяки";
+"UnxipExperiment" = "Експеримент Unxip";
+"License" = "Ліцензія";
+
+// General Preference Pane
+"General" = "Основне";
+"AppleID" = "Apple ID";
+"SignIn" = "Логін";
+"Notifications" = "Сповіщення";
+
+// Updates Preference Pane
+"Updates" = "Оновлення";
+"Versions" = "Версії";
+"AutomaticInstallNewVersion" = "Автоматично встановлювати нові версії Xcode";
+"IncludePreRelease" = "Також встановлювати prerelease/beta версії";
+"AppUpdates" = "Оновлення Xcodes.app";
+"CheckForAppUpdates" = "Автоматично перевіряти наявність оновлень.";
+"CheckNow" = "Перевірити зараз";
+"LastChecked" = "Перевірено в останнє: %@";
+"Never" = "Ніколи";
+
+// Download Preference Pane
+"Downloads" = "Завантаження";
+"DataSource" = "Джерело інформації";
+"DataSourceDescription" = "Apple – cканування порталу Apple Developer у пошуку доступних версій Xcode. Створюючи список усих нових релізів, але це не завжи спрацьовує.\n\nXcode Releases – це не офіційний список релізів Xcode. Він являє собою відформатований список, що також має додаткову інформацію не завжди доступну напряму з сайту Apple, і менш ймовірно що він зламається якщо Apple випустить редизайн Developer Portal";
+"Downloader" = "Завантажувач";
+"DownloaderDescription" = "aria2 може використовувати до 16 з'єднань, завантажуючи Xcode у 3-5 разів швидше ніж URLSession. Вона поставляється у вигляді бінарника та коду, відповідно до вимог її GPLv2 ліцензії.\n\nURLSession – це завантажувач по замовчуванню від Apple";
+
+// Advanced Preference Pane
+"Advanced" = "Розширені";
+"LocalCachePath" = "Локальний Кеш";
+"LocalCachePathDescription" = "Сюди зберігаються тимчасові закачки встановлюваних версій Xcode";
+"Change" = "Змінити";
+"Active/Select" = "Акивний/Обрати";
+"AutomaticallyCreateSymbolicLink" = "Автоматично створювати символічну ссилку Xcode.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "Обираючи Акивний Xcode, спробувати створити символічну ссилку Xcode.app що вказує на обрану версію. Ссилка буде розміщена у папці інсталяції Xcode";
+"PrivilegedHelper" = "Privileged Helper";
+"PrivilegedHelperDescription" = "Xcodes використовує спеціальний \"privilege helper\" щоб запускати задачі як суперюзер. Це включає наприклад sudo в терміналі, та кроки після інсталяції або перемикання версії Xcode за допомогою xcode-select.\n\nБуде запит на ваш пароль від Мак щоб встановити цей хелпер.";
+"HelperInstalled" = "Helper встановлено";
+"HelperNotInstalled" = "Helper не встановлено";
+"InstallHelper" = "Встановити Install helper";
+
+// Experiment Preference Pane
+"Experiments" = "Експерименти";
+"FasterUnxip" = "Швидкий Unxip";
+"UseUnxipExperiment" = "Під час розпаковки Unxip використовувати експериментальний метод.";
+"FasterUnxipDescription" = "Завдяки @_saagarjha, цей експеримент може пришвидшити розпаковку майже на 70%. Подробиці про unxip тут – https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "Сповіщення Дозволено. Ви будете отримувати сповіщення від Xcodes.";
+"AccessDenied" = "⚠️ Сповіщення Заборонено ⚠️\n\nВідкрийте будьласка Налаштування Сповіщень та надайте дозвіл Xcodes для отримання дозволу.";
+"NotificationSettings" = "Налаштування Сповіщень";
+"EnableNotifications" = "Увімкнути Сповіщення";
+
+// SignIn
+"SignInWithApple" = "Вхід з вашим Apple ID.";
+"AppleID" = "Apple ID:";
+"Password" = "Пароль:";
+"Required" = "Вимагається";
+"SignOut" = "Логаут";
+
+// SMS/2FA
+"DigitCodeDescription" = "Введіть %d-значний код з одного з довірених пристроїв:";
+"SendSMS" = "Надіслати СМС";
+"EnterDigitCodeDescription" = "Введіть %d-значний код відправлений на %@:";
+"SelectTrustedPhone" = "Виберіть довірений номер телефону щоб отримати %d-значний код в СМС:";
+"NoTrustedPhones" = "Ваш аккаунт не має перевіреного телефонного номеру, що вимагається для двофакторної авторизації.\n\nДивіться https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "Оновлено о";
+
+// ToolBar
+"Login" = "Apple ID";
+"LoginDescription" = "Показати Apple ID";
+"Refresh" = "Оновити";
+"RefreshDescription" = "Оновити список Xcode";
+"All" = "Всі";
+"Release" = "Release";
+"ReleaseOnly" = "лише Release";
+"Beta" = "Beta";
+"BetaOnly" = "лише Beta";
+"Filter" = "Фільтр";
+"FilterAvailableDescription" = "Фільтрувати доступні версії";
+"FilterInstalledDescription" = "Фільтрувати встановлені версії";
+"Info" = "Info";
+"InfoDescription" = "Показати або сховати панель інформації";
+"Preferences" = "Налаштування";
+"PreferencesDescription" = "Відкрити Налаштування";
+"Search" = "Пошук...";
+"SearchDescription" = "Список знайденого";
+
+// List
+"ActiveVersionDescription" = "Це активна версія";
+"MakeActiveVersionDescription" = "Зробити цю версію активною";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "Видалити Xcode %@?";
+"Alert.Uninstall.Message" = "Xcode буде просто переміщено до Кошика, без очищення.";
+"Alert.Uninstall.Error.Title" = "Не можливо видалити Xcode";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "Впевнені що хочете скасувати встановлення Xcode %@?";
+"Alert.CancelInstall.Message" = "Увесь прогрес буде скасовано.";
+"Alert.CancelInstall.PrimaryButton" = "Зупинити встановлення";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "Privileged Helper";
+"Alert.PrivilegedHelper.Message" = "Xcodes використовує спеціальний \"privilege helper\" щоб запускати задачі як суперюзер. Це включає наприклад sudo в терміналі, та кроки після інсталяції або перемикання версії Xcode за допомогою xcode-select.\n\nЗараз буде запит на ваш пароль від Мак щоб встановити цей хелпер.";
+"Alert.PrivilegedHelper.Error.Title" = "Не вдалося встановити \"privilege helper\"";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "Мінімальні вимоги не задоволені";
+"Alert.MinSupported.Message" = "Xcode %@ вимагає як мінімум MacOS %@, але у вас MacOS %@, всеодно продовжити?";
+
+// Install
+"Alert.Install.Error.Title" = "Не вдалося встановити Xcode";
+"Alert.InstallArchive.Error.Title" = "Помилка з архівом Xcode";
+
+// Update
+"Alert.Update.Error.Title" = "Не вдалося оновити обраний Xcode";
+
+// Active/Select
+"Alert.Select.Error.Title" = "Не вдалося вибрати Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "Не вдалося створити символічну ссилку";
+"Alert.SymLink.Message" = "Xcode.app вже існує, і це не символічна ссилка";
+
+// Post install
+"Alert.PostInstall.Title" = "Не можливо виконати пост-інсталяційні дії";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "Архів \"%@\" пошкожено і не можливо розпакувати.";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "Архів \"%@\" не можливо розпакувати, так як бракує місця.\n\nПочистіть файлове сховище щоб вистачило місця на розпаковку Архіву, та спробуйте встановити Xcode %@ знову.";
+"InstallationError.FailedToMoveXcodeToApplications" = "Помилка при переміщенні Xcode в %@.";
+"InstallationError.FailedSecurityAssessment" = "Xcode провалив перевірку безпеки (security assessment) з наступним повідомленням:\n%@\nВін залишиться установлений в %@ якщо ви все ж наважитесь його використовувати.";
+"InstallationError.CodesignVerifyFailed" = "Викачаний Xcode не зміг пройти перевірку коду підпису (code signing verification) з наступним повідомленням:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "Викачаний Xcode має неочікуваний код підпису (code signing identity).\nМає:\n%@\n%@\nОчікується:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcode (поки що) не підтримує свою установку у %@ форматі.";
+"InstallationError.MissingSudoerPassword" = "Не вистачає sudo паролю. Спробуйте ще раз.";
+"InstallationError.UnavailableVersion" = "Не можу знайти версію %@.";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "Немає доступних релізних версій.";
+"InstallationError.NoPrereleaseVersionAvailable" = "Немає доступного пререлізу.";
+"InstallationError.MissingUsernameOrPassword" = "Не вистачає юзернейму чи паролю. Спробуйте ще раз.";
+"InstallationError.VersionAlreadyInstalled" = "%@ вже встановлено в %@";
+"InstallationError.InvalidVersion" = "%@ не є вірним номером версії.";
+"InstallationError.VersionNotInstalled" = "%@ не встановлено.";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "Установку завершено. Але деякі post-install кроки не виконано автоматично. Ці кроки буде виконано коли ви вперше запустите Xcode %@.";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "Установку завершено. Але деякі post-install кроки не виконано автоматично. Xcodes виконує цю роботу за допомогою \"privileged helper\", який схоже що не встановлено. Ви можете встановити його за доромогою Налаштування > Додатково.\n\nЦі кроки буде виконано коли ви вперше запустите Xcode %@.";
+
+// Installation Steps
+"Downloading" = "Скачування";
+"Unarchiving" = "Розпаковка (Може бути довго)";
+"Moving" = "Переміщення до %@";
+"TrashingArchive" = "Видалення архіву";
+"CheckingSecurity" = "Перевірка безпеки";
+"Finishing" = "Завершення";
+
+// Notifications
+"Notification.NewVersionAvailable" = "Доступна нова версія";
+"Notification.FinishedInstalling" = "Встановлено";
+
+
+"HelperClient.error" = "Помилка з'єднання з \"privileged helper\".";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "Нові версії Xcode";
+"Notification.NewXcodeVersion.Body" = "Нові версії Xcode доступні до завантаження.";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Веселого WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/unxip b/Xcodes/Resources/unxip
new file mode 100755
index 0000000..46a7f31
Binary files /dev/null and b/Xcodes/Resources/unxip differ
diff --git a/Xcodes/Resources/zh-Hans.lproj/Localizable.strings b/Xcodes/Resources/zh-Hans.lproj/Localizable.strings
new file mode 100644
index 0000000..3bea058
--- /dev/null
+++ b/Xcodes/Resources/zh-Hans.lproj/Localizable.strings
@@ -0,0 +1,234 @@
+// Menu
+"Menu.About" = "关于Xcodes";
+"Menu.CheckForUpdates" = "检查更新…";
+"Menu.Acknowledgements" = "Xcodes版权声明";
+"Menu.GitHubRepo" = "Xcodes GitHub仓库";
+"Menu.ReportABug" = "反馈问题";
+"Menu.RequestNewFeature" = "建议新功能";
+
+// Common
+"Install" = "安装";
+"InstallDescription" = "安装此版本";
+"RevealInFinder" = "在访达中显示";
+"Active" = "已激活";
+"MakeActive" = "激活";
+"Open" = "打开";
+"OpenDescription" = "打开此版本";
+"CopyPath" = "复制文件位置";
+"CreateSymLink" = "以Xcode.app创建软链接";
+"CreateSymLinkBeta" = "以Xcode-Beta.app创建软链接";
+"Uninstall" = "卸载";
+"Selected" = "已选定";
+"Select" = "选定";
+"Cancel" = "取消";
+"Next" = "下一步";
+"Continue" = "继续";
+"Close" = "关闭";
+"OK" = "好";
+
+// Info Pane
+"IdenticalBuilds" = "重复版本";
+"IdenticalBuilds.help" = "有时预发布版本会与正式版使用相同的Build号。Xcodes会自动将这些版本显示为一个。";
+
+"ReleaseDate" = "发布日期";
+"ReleaseNotes" = "更新说明";
+"ReleaseNotes.help" = "查看更新说明";
+"CopyReleaseNoteURL" = "复制 URL";
+"Compatibility" = "兼容性";
+"MacOSRequirement" = "需要macOS %@及以上";
+"SDKs" = "SDK";
+"Compilers" = "编译器";
+"DownloadSize" = "下载文件大小";
+"NoXcodeSelected" = "没有选定的Xcode版本";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "第%1$d步,共%2$d步:%3$@";
+"DownloadingPercentDescription" = "正在下载:已完成%d%%";
+"StopInstallation" = "停止安装";
+"DownloadingError" = "没有找到下载信息";
+
+// About
+"VersionWithBuild" = "版本%@ (%@)";
+"GithubRepo" = "GitHub仓库";
+"Acknowledgements" = "版权声明";
+"UnxipExperiment" = "Unxip Experiment";
+"License" = "许可协议";
+
+// General Preference Pane
+"General" = "通用";
+"AppleID" = "Apple ID";
+"SignIn" = "登录";
+"Notifications" = "通知";
+
+// Updates Preference Pane
+"Updates" = "更新";
+"Versions" = "版本";
+"AutomaticInstallNewVersion" = "自动安装最新的Xcode版本";
+"IncludePreRelease" = "包含预发布/测试版本";
+"AppUpdates" = "Xcodes.app更新";
+"CheckForAppUpdates" = "自动检查更新";
+"CheckNow" = "现在检查";
+"LastChecked" = "上次检查时间:%@";
+"Never" = "从未检查";
+
+// Download Preference Pane
+"Downloads" = "下载";
+"DataSource" = "数据源";
+"DataSourceDescription" = "Apple数据源是从Apple开发者网站爬取的。它会及时反应最新版本,但较为脆弱。\n\nXcode Releases是一个非官方的Xcode版本列表。它提供了良好格式化的数据,包含了不便从Apple直接获得的附加信息,且更不容易在Apple开发者网站无法访问时出现问题。";
+"Downloader" = "下载器";
+"DownloaderDescription" = "aria2使用最高16线程,能提供3至5倍的下载速度。它与它的源代码被一同打包在Xcodes中以符合GPLv2授权。\n\nURLSession是用于发起URL请求的默认Apple API。";
+
+// Advanced Preference Pane
+"Advanced" = "高级";
+"LocalCachePath" = "本地缓存位置";
+"LocalCachePathDescription" = "Xcodes会缓存可用的Xcode版本并暂时下载新版本到一个目录。";
+"Change" = "变更";
+"Active/Select" = "激活/选定";
+"InstallDirectory" = "安装目录";
+"InstallPathDescription" = "Xcodes会在一个目录中检索及安装。默认(推荐)保持/Applications。任何对Xcode存储位置的变更都可能会导致其他App或服务停止工作。";
+
+"OnSelectDoNothing" = "保持名称为Xcode-X.X.X.app";
+"OnSelectDoNothingDescription" = "选中时,将会保持各版本的名称。例如Xcode-13.4.1.app。";
+"AutomaticallyCreateSymbolicLink" = "自动创建名为Xcode.app的软连接";
+"AutomaticallyCreateSymbolicLinkDescription" = "当激活/选定某个Xcode版本时,尝试在安装目录创建一个名为Xcode.app的软链接。";
+"OnSelectRenameXcode" = "总是重命名为Xcode.app";
+"OnSelectRenameXcodeDescription" = "选中时,会自动尝试重命名活跃的Xcode为Xcode.app,将之前的Xcode.app重命名为包含版本的名称。";
+
+"PrivilegedHelper" = "提权帮助程序";
+"PrivilegedHelperDescription" = "Xcodes使用一个独立的提权帮助程序来以root身份执行任务。就是那些需要在命令行中用sudo执行的命令。包括一些安装前置步骤以及用xcode-select切换Xcode版本。\n\n您需要提供当前用户的密码来安装它。";
+"HelperInstalled" = "帮助程序已安装";
+"HelperNotInstalled" = "帮助程序未安装";
+"InstallHelper" = "安装帮助程序";
+
+// Experiment Preference Pane
+"Experiments" = "实验性功能";
+"FasterUnxip" = "更快的xip解压";
+"UseUnxipExperiment" = "使用实验性功能解压xip";
+"FasterUnxipDescription" = "感谢@_saagarjha,此实验性功能可在部分系统上将xip解压速度提高70%。\n\n请从此仓库获取关于其实现细节的信息:https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "已授权。您将会收到Xcodes的通知。";
+"AccessDenied" = "⚠️ 没有权限 ⚠️\n\n请打开您的通知偏好设置并允许Xcodes发送通知。";
+"NotificationSettings" = "通知设置";
+"EnableNotifications" = "启用通知";
+
+// SignIn
+"SignInWithApple" = "使用您的Apple ID登录。";
+"AppleID" = "Apple ID:";
+"Password" = "密码:";
+"Required" = "必填";
+"SignOut" = "登出";
+
+// SMS/2FA
+"DigitCodeDescription" = "请输入从信任的设备接收到的%d位代码:";
+"SendSMS" = "发送短信";
+"EnterDigitCodeDescription" = "请输入%d位代码,已发送到%@:";
+"SelectTrustedPhone" = "请选择一个信任的手机号来从短信接收%d位代码:";
+"NoTrustedPhones" = "您的账户没有任何信任的手机号,但这是两步验证所必须的。\n\n请参阅 https://support.apple.com/HT204915。";
+
+// MainWindow
+"UpdatedAt" = "更新于";
+
+// ToolBar
+"Login" = "登录";
+"LoginDescription" = "打开登录窗口";
+"Refresh" = "刷新";
+"RefreshDescription" = "刷新Xcode列表";
+"All" = "全部";
+"Release" = "正式版";
+"ReleaseOnly" = "只显示正式版";
+"Beta" = "测试版";
+"BetaOnly" = "只显示测试版";
+"Filter" = "过滤";
+"FilterAvailableDescription" = "过滤可用版本";
+"FilterInstalledDescription" = "过滤已安装版本";
+"Info" = "信息";
+"InfoDescription" = "显示或隐藏信息面板";
+"Preferences" = "偏好设置";
+"PreferencesDescription" = "打开偏好设置";
+"Search" = "搜索…";
+"SearchDescription" = "搜索列表";
+
+// List
+"ActiveVersionDescription" = "这是一个已激活的版本";
+"MakeActiveVersionDescription" = "激活此版本";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "要卸载Xcode %@吗?";
+"Alert.Uninstall.Message" = "它会被移动到废纸篓,但废纸篓不会清空。";
+"Alert.Uninstall.Error.Title" = "无法卸载Xcode";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "您确定要终止Xcode %@的安装吗?";
+"Alert.CancelInstall.Message" = "所有进度都将会丢失。";
+"Alert.CancelInstall.PrimaryButton" = "终止安装";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "提权帮助程序";
+"Alert.PrivilegedHelper.Message" = "Xcodes使用一个独立的提权帮助程序来以root身份执行任务。就是那些需要在命令行中用sudo执行的命令。包括安装前置步骤以及用xcode-select切换Xcode版本。\n\n您需要提供当前用户的密码来安装它。";
+"Alert.PrivilegedHelper.Error.Title" = "无法安装帮助程序";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "没有达到安装所需的最低要求";
+"Alert.MinSupported.Message" = "Xcode %@需要macOS %@及以上,但您运行的是macOS %@,仍然要安装吗?";
+
+// Install
+"Alert.Install.Error.Title" = "无法安装Xcode";
+"Alert.InstallArchive.Error.Title" = "无法安装Xcode压缩文件";
+
+// Update
+"Alert.Update.Error.Title" = "无法更新选定的Xcode版本";
+
+// Active/Select
+"Alert.Select.Error.Title" = "无法选定Xcode版本";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "无法创建软链接";
+"Alert.SymLink.Message" = "Xcode.app已存在且不是软连接";
+
+// Post install
+"Alert.PostInstall.Title" = "无法执行安装前置步骤";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "此压缩文件“%@”已损坏且无法解压。";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "此压缩文件“%@”无法解压因为当前宗卷没有足够的可用空间。\n\n请空出更多空间并再次安装Xcode %@以从失败处继续。";
+"InstallationError.FailedToMoveXcodeToApplications" = "无法将Xcode移动到目录%@。";
+"InstallationError.FailedSecurityAssessment" = "Xcode %@无法完成安全评估,并输出如下:\n%@\n但它依然被安装到了%@如果您依然想使用它的话。";
+"InstallationError.CodesignVerifyFailed" = "无法验证下载的Xcode版本的代码签名,并输出如下:\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "下载的Xcode版本不包含期望的代码签名身份。\n读取到:\n%@\n%@\n期望是:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes尚未支持从%@格式安装。";
+"InstallationError.MissingSudoerPassword" = "缺少密码。请再试一次。";
+"InstallationError.UnavailableVersion" = "无法找到版本%@。";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "没有非预发布版以外的其他可用版本。";
+"InstallationError.NoPrereleaseVersionAvailable" = "没有可用的预发布版本。";
+"InstallationError.MissingUsernameOrPassword" = "缺少用户名或密码。请再试一次。";
+"InstallationError.VersionAlreadyInstalled" = "%@已经被安装到%@";
+"InstallationError.InvalidVersion" = "%@不是一个正确的版本。";
+"InstallationError.VersionNotInstalled" = "%@没有被安装。";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "安装已完成,但一些前置步骤没有被自动执行。它们将会在您第一次运行Xcode %@时执行。";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "安装已完成,但一些前置步骤没有被自动执行。Xcodes使用一个提权帮助程序来执行这些步骤,但帮助程序似乎没有被安装。您可以从 偏好设置 > 高级 中安装。\n\n这些步骤将会在您第一次运行Xcode %@时执行。";
+
+// Installation Steps
+"Downloading" = "正在下载";
+"Unarchiving" = "正在解压(这将需要一些时间)";
+"Moving" = "正在移动到%@";
+"TrashingArchive" = "正在将压缩文件移动到废纸篓";
+"CheckingSecurity" = "正在检查安全性";
+"Finishing" = "正在完成";
+
+// Notifications
+"Notification.NewVersionAvailable" = "新版本可用";
+"Notification.FinishedInstalling" = "安装完成";
+
+
+"HelperClient.error" = "无法与提权帮助程序通信。";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "新Xcode版本可用";
+"Notification.NewXcodeVersion.Body" = "新的Xcode版本已经可以下载。";
+
+// WWDC
+"WWDC.Message" = "👨🏻💻👩🏼💻 Happy WWDC %@! 👨🏽💻🧑🏻💻";
diff --git a/Xcodes/Resources/zh-Hant.lproj/Localizable.strings b/Xcodes/Resources/zh-Hant.lproj/Localizable.strings
new file mode 100644
index 0000000..3f16514
--- /dev/null
+++ b/Xcodes/Resources/zh-Hant.lproj/Localizable.strings
@@ -0,0 +1,223 @@
+// Menu
+"Menu.About" = "關於 Xcodes";
+"Menu.CheckForUpdates" = "檢查更新版本⋯";
+"Menu.Acknowledgements" = "Xcodes 版權宣告";
+"Menu.GitHubRepo" = "Xcodes GitHub Repo";
+"Menu.ReportABug" = "回報錯誤";
+"Menu.RequestNewFeature" = "請求新功能";
+
+// Common
+"Install" = "安裝";
+"InstallDescription" = "安裝這個版本";
+"RevealInFinder" = "顯示於 Finder";
+"Active" = "啟用中";
+"MakeActive" = "啟用";
+"Open" = "打開";
+"OpenDescription" = "打開這個版本";
+"CopyPath" = "拷貝路徑";
+"CreateSymLink" = "製作 Xcode.app 的 Symlink";
+"CreateSymLink" = "製作 Xcode-Beta.app 的 Symlink";
+"Uninstall" = "解除安裝";
+"Selected" = "已選取";
+"Select" = "選取";
+"Cancel" = "取消";
+"Next" = "下一步";
+"Continue" = "繼續";
+"Close" = "關閉";
+"OK" = "好";
+
+// Info Pane
+"IdenticalBuilds" = "相同的建置版本";
+"IdenticalBuilds.help" = "有時候預先發行版與正式版的建制版本號會相同,Xcodes 會自動將這些版本整理在一起。";
+
+"ReleaseDate" = "發行日期";
+"ReleaseNotes" = "版本附註";
+"ReleaseNotes.help" = "檢視版本附註";
+"CopyReleaseNoteURL" = "複製 URL";
+"Compatibility" = "相容性";
+"MacOSRequirement" = "需要 macOS %@ 或以上版本";
+"SDKs" = "SDKs";
+"Compilers" = "編譯器";
+"DownloadSize" = "下載大小";
+"NoXcodeSelected" = "沒有已選取的 Xcode";
+
+// Installation Steps
+// When localizing. Items will be replaced in order. ie "Step 1 of 6: Downloading"
+// So if changing order, make sure the positional specficier is retained. ex: "%3$@: Step %1$d of %2$d"
+"InstallationStepDescription" = "步驟 %1$d 之 %2$d:%3$@";
+"DownloadingPercentDescription" = "下載中:已完成 %d%%";
+"StopInstallation" = "停止安裝";
+"DownloadingError" = "找不到下載相關資訊";
+
+// About
+"VersionWithBuild" = "版本 %@ (%@)";
+"GithubRepo" = "GitHub Repo";
+"Acknowledgements" = "版權宣告";
+"UnxipExperiment" = "Unxip 試驗";
+"License" = "軟體許可證";
+
+// General Preference Pane
+"General" = "一般";
+"AppleID" = "Apple ID";
+"SignIn" = "登入";
+"Notifications" = "通知";
+
+// Updates Preference Pane
+"Updates" = "更新";
+"Versions" = "版本";
+"AutomaticInstallNewVersion" = "自動安裝新版本的 Xcode";
+"IncludePreRelease" = "包含預先發行及測試版";
+"AppUpdates" = "Xcodes.app 更新";
+"CheckForAppUpdates" = "自動檢查更新項目";
+"CheckNow" = "立即檢查";
+"LastChecked" = "上一次檢查: %@";
+"Never" = "從未使用";
+
+// Download Preference Pane
+"Downloads" = "下載";
+"DataSource" = "資料來源";
+"DataSourceDescription" = "Apple 資料來源是擷取 Apple 開發者網站而來,永遠會顯示最新的可用版本,但比較容易出錯。\n\nXcode Releases 是一個非官方的 Xcodes 發行版本列表。這個來源提供格式良好的資料,包含了 Apple 開發者網站上未列出的額外資訊並且即使 Apple 決定重新設計他們的開發者網站也比較不容易出錯。";
+"Downloader" = "下載器";
+"DownloaderDescription" = "aria2 相較 URLSession 可以同時使用最多 16 條連線以 3 ~ 5 倍的速度下載 Xcode。Xcodes 包含了執行檔與其原始碼以遵循他的 GPLv2 授權合約。\n\nURLSession 是系統內建用來發送 URL 連線請求的 Apple API。";
+
+// Advanced Preference Pane
+"Advanced" = "進階";
+"LocalCachePath" = "本機快取路徑";
+"LocalCachePathDescription" = "Xcodes 會快取可用的 Xcode 版本並將新版本暫存下載至這個目錄";
+"Change" = "變更";
+"Active/Select" = "啟用/選取";
+"AutomaticallyCreateSymbolicLink" = "自動建立 Symlink 至 Xcode.app";
+"AutomaticallyCreateSymbolicLinkDescription" = "當你選擇/啟用一個 Xcode 版本,自動建立一個名為 Xcode.app 的 Symlink 到該版本的安裝目錄";
+"PrivilegedHelper" = "權限輔助程式";
+"PrivilegedHelperDescription" = "Xcodes 使用一個分開的權限輔助程式以使用 root 身份執行特定工作。這些工作包含了通常需要在命令列使用 sudo 的指令,包含安裝後步驟以及使用 xcode-select 選擇 Xcode 版本。\n\n安裝時,你將會被詢問你的 macOS 帳號密碼。";
+"HelperInstalled" = "輔助程式已安裝";
+"HelperNotInstalled" = "輔助程式尚未安裝";
+"InstallHelper" = "安裝輔助程式";
+
+// Experiment Preference Pane
+"Experiments" = "實驗";
+"FasterUnxip" = "更快的 Unxip";
+"UseUnxipExperiment" = "解壓縮 (Unxip) 時,使用這個試驗版本";
+"FasterUnxipDescription" = "感謝 @_saagarjha 的努力,這個試驗版本可以在某些系統上加快 70% 的解壓縮速度。\n\n更多關於這項成就是如何達成的資訊,請參閱 unxip 的 repo - https://github.com/saagarjha/unxip";
+
+// Notifications
+"AccessGranted" = "通知已啟用。你將會開始收到來自 Xcodes 的通知。";
+"AccessDenied" = "⚠️ 通知已停用 ⚠️\n\n如果你想要啟用通知,請打開你的通知設定並選擇 Xcodes。";
+"NotificationSettings" = "通知偏好設定";
+"EnableNotifications" = "啟用通知";
+
+// SignIn
+"SignInWithApple" = "登入您的Apple ID";
+"AppleID" = "Apple ID:";
+"Password" = "密碼:";
+"Required" = "必要";
+"SignOut" = "登出";
+
+// SMS/2FA
+"DigitCodeDescription" = "請輸入顯示在你其中一個信任裝置中的 %d 位數密碼:";
+"SendSMS" = "傳送簡訊";
+"EnterDigitCodeDescription" = "請輸入 %d 位數密碼,已傳送至 %@: ";
+"SelectTrustedPhone" = "請輸入一個你想用來接收 %d 位數密碼簡訊的電話號碼:";
+"NoTrustedPhones" = "你的帳號沒有任何已信任的手機號碼,但兩階段認證需要信任的手機號碼。\n\n請參閱 https://support.apple.com/HT204915.";
+
+// MainWindow
+"UpdatedAt" = "上一次檢查:";
+
+// ToolBar
+"Login" = "登入";
+"LoginDescription" = "打開登入";
+"Refresh" = "更新";
+"RefreshDescription" = "更新 Xcode 版本列表";
+"All" = "全部";
+"Release" = "正式版";
+"ReleaseOnly" = "只看正式版";
+"Beta" = "測試版";
+"BetaOnly" = "只看測試版";
+"Filter" = "過濾";
+"FilterAvailableDescription" = "顯示可用的版本";
+"FilterInstalledDescription" = "顯示已安裝的版本";
+"Info" = "資訊";
+"InfoDescription" = "顯示或隱藏資訊面板";
+"Preferences" = "偏好設定";
+"PreferencesDescription" = "打開偏好設定";
+"Search" = "搜尋⋯";
+"SearchDescription" = "搜尋列表";
+
+// List
+"ActiveVersionDescription" = "這是啟用中版本";
+"MakeActiveVersionDescription" = "啟用這個版本";
+
+// Alerts
+// Uninstall
+"Alert.Uninstall.Title" = "解除安裝 Xcode %@?";
+"Alert.Uninstall.Message" = "它將會被移到垃圾桶,但不會被清除。";
+"Alert.Uninstall.Error.Title" = "無法解除安裝 Xcode";
+
+// Cancel Install
+"Alert.CancelInstall.Title" = "你確定你想要停止安裝 Xcode %@?";
+"Alert.CancelInstall.Message" = "所有已進行的安裝步驟將被放棄。";
+"Alert.CancelInstall.PrimaryButton" = "停止安裝";
+
+// Privileged Helper
+"Alert.PrivilegedHelper.Title" = "權限輔助程式";
+"Alert.PrivilegedHelper.Message" = "Xcodes 使用一個分開的權限輔助程式以使用 root 身份執行特定工作。這些工作包含了通常需要在命令列使用 sudo 的指令,包含安裝後步驟以及使用 xcode-select 選擇 Xcode 版本。\n\n安裝時,你將會被詢問你的 macOS 帳號密碼。";
+"Alert.PrivilegedHelper.Error.Title" = "無法安裝輔助程式";
+
+// Min MacOS Supported
+"Alert.MinSupported.Title" = "未達最低版本要求";
+"Alert.MinSupported.Message" = "Xcode %@ 需要 MacOS %@,但你正在使用 MacOS %@,你確定還要繼續安裝嗎?";
+
+// Install
+"Alert.Install.Error.Title" = "無法安裝 Xcode";
+"Alert.InstallArchive.Error.Title" = "無法安裝已封存的 Xcode";
+
+// Update
+"Alert.Update.Error.Title" = "無法更新已選擇的 Xcode";
+
+// Active/Select
+"Alert.Select.Error.Title" = "無法選擇 Xcode";
+
+// Symbolic Links
+"Alert.SymLink.Title" = "無法建立 Symlink";
+"Alert.SymLink.Message" = "Xcode.app 已經存在,但並不是一個 Symlink";
+
+// Post install
+"Alert.PostInstall.Title" = "無法進行安裝後步驟";
+
+// InstallationErrors
+"InstallationError.DamagedXIP" = "壓縮檔 \"%@\" 已經損壞並無法解壓縮。";
+"InstallationError.NotEnoughFreeSpaceToExpandArchive" = "壓縮檔 \"%@\" 由於缺乏足夠的磁碟空間,無法解壓縮。\n\n請清空更多磁碟空間以確保可以解壓縮該檔案,然後再重新安裝 Xcode %@ 一次。安裝步驟將會從上次停住的地方繼續。";
+"InstallationError.FailedToMoveXcodeToApplications" = "無法將 Xcode 移動至 %@ 目錄。";
+"InstallationError.FailedSecurityAssessment" = "由於以下原因無法安全驗證 Xcode %@:\n%@\n如果你執意使用,該版本仍然安裝於 %@。";
+"InstallationError.CodesignVerifyFailed" = "下載的 Xcode 由於以下原因無法驗證 Codesign 簽章\n%@";
+"InstallationError.UnexpectedCodeSigningIdentity" = "下載的 Xcode 含有未預期的簽章。\n簽章人:\n%@\n%@\n預期的簽章人:\n%@\n%@";
+"InstallationError.UnsupportedFileFormat" = "Xcodes 目前尚未支援由 %@ 檔案格式安裝 Xcode。";
+"InstallationError.MissingSudoerPassword" = "找不到密碼,請再試一次。";
+"InstallationError.UnavailableVersion" = "找不到版本 %@。";
+"InstallationError.NoNonPrereleaseVersionAvailable" = "目前沒有可用的非預先發行版本。";
+"InstallationError.NoPrereleaseVersionAvailable" = "目前沒有可用的預先發行版本。";
+"InstallationError.MissingUsernameOrPassword" = "找不到使用者名稱或是密碼,請再試一次。";
+"InstallationError.VersionAlreadyInstalled" = "%@ 已經安裝於 %@";
+"InstallationError.InvalidVersion" = "%@ 不是個正確的版本號碼。";
+"InstallationError.VersionNotInstalled" = "%@ 沒有被安裝。";
+"InstallationError.PostInstallStepsNotPerformed.Installed" = "安裝已經完成,但有些安裝後步驟沒有自動執行。這些步驟在你第一次啟動 Xcode %@ 的時候會自動執行。";
+"InstallationError.PostInstallStepsNotPerformed.NotInstalled" = "安裝已經完成,但有些安裝後步驟沒有自動執行。 Xcodes 需要權限輔助程式來執行這些步驟,但該程式尚未被安裝。你可以在 偏好設定 > 進階中安裝它。\n\n這些步驟在你第一次啟動 Xcode %@ 的時候會自動執行。";
+
+// Installation Steps
+"Downloading" = "下載中";
+"Unarchiving" = "解壓縮中 (可能需要一點時間)";
+"Moving" = "移動至 %@";
+"TrashingArchive" = "移動封存檔到垃圾桶";
+"CheckingSecurity" = "安全驗證中";
+"Finishing" = "收尾中";
+
+// Notifications
+"Notification.NewVersionAvailable" = "有新的版本可用";
+"Notification.FinishedInstalling" = "已經完成安裝";
+
+
+"HelperClient.error" = "無法與權限輔助程式溝通。";
+///++
+// Notifications
+"Notification.NewXcodeVersion.Title" = "新的 Xcode 版本";
+"Notification.NewXcodeVersion.Body" = "新的 Xcode 版本已經可以下載了";
diff --git a/Xcodes/XcodesApp.swift b/Xcodes/XcodesApp.swift
index 76f5186..76fb1a3 100644
--- a/Xcodes/XcodesApp.swift
+++ b/Xcodes/XcodesApp.swift
@@ -8,11 +8,13 @@ struct XcodesApp: App {
@SwiftUI.Environment(\.scenePhase) private var scenePhase: ScenePhase
@SwiftUI.Environment(\.openURL) var openURL: OpenURLAction
@StateObject private var appState = AppState()
-
+ @StateObject private var updater = ObservableUpdater()
+
var body: some Scene {
WindowGroup("Xcodes") {
MainWindow()
.environmentObject(appState)
+ .environmentObject(updater)
// This is intentionally used on a View, and not on a WindowGroup,
// so that it's triggered when an individual window's phase changes instead of all window phases.
// When used on a View it's also invoked on launch, which doesn't occur with a WindowGroup.
@@ -26,13 +28,13 @@ struct XcodesApp: App {
}
.commands {
CommandGroup(replacing: .appInfo) {
- Button("About Xcodes") {
+ Button("Menu.About") {
appDelegate.showAboutWindow()
}
}
CommandGroup(after: .appInfo) {
- Button("Check for Updates...") {
- appDelegate.checkForUpdates()
+ Button("Menu.CheckForUpdates") {
+ updater.checkForUpdates()
}
}
@@ -47,19 +49,19 @@ struct XcodesApp: App {
XcodeCommands(appState: appState)
CommandGroup(replacing: CommandGroupPlacement.help) {
- Button("Xcodes GitHub Repo") {
+ Button("Menu.GitHubRepo") {
let xcodesRepoURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/")!
openURL(xcodesRepoURL)
}
Divider()
- Button("Report a Bug") {
+ Button("Menu.ReportABug") {
let bugReportURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=bug&template=bug_report.md&title=")!
openURL(bugReportURL)
}
- Button("Request a New Feature") {
+ Button("Menu.RequestNewFeature") {
let featureRequestURL = URL(string: "https://github.com/RobotsAndPencils/XcodesApp/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=")!
openURL(featureRequestURL)
}
@@ -69,6 +71,7 @@ struct XcodesApp: App {
Settings {
PreferencesView()
.environmentObject(appState)
+ .environmentObject(updater)
}
#endif
}
@@ -81,7 +84,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
backing: .buffered,
defer: false
)) {
- $0.title = "About Xcodes"
+ $0.title = localizeString("About")
$0.contentView = NSHostingView(rootView: AboutView(showAcknowledgementsWindow: showAcknowledgementsWindow))
$0.isReleasedWhenClosed = false
}
@@ -92,7 +95,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
backing: .buffered,
defer: false
)) {
- $0.title = "Xcodes Acknowledgements"
+ $0.title = localizeString("Acknowledgements")
$0.contentView = NSHostingView(rootView: AcknowledgmentsView())
$0.isReleasedWhenClosed = false
}
@@ -112,12 +115,16 @@ class AppDelegate: NSObject, NSApplicationDelegate {
acknowledgementsWindow.makeKeyAndOrderFront(nil)
}
- func checkForUpdates() {
- SUUpdater.shared()?.checkForUpdates(self)
- }
-
func applicationDidFinishLaunching(_ notification: Notification) {
- // Initialize manually
- SUUpdater.shared()
+
}
}
+
+func localizeString(_ key: String, comment: String = "") -> String {
+ if #available(macOS 12, *) {
+ return String(localized: String.LocalizationValue(key))
+ } else {
+ return NSLocalizedString(key, comment: comment)
+ }
+
+}
diff --git a/XcodesTests/AppStateTests.swift b/XcodesTests/AppStateTests.swift
index 0c4c165..ce38607 100644
--- a/XcodesTests/AppStateTests.swift
+++ b/XcodesTests/AppStateTests.swift
@@ -81,6 +81,10 @@ class AppStateTests: XCTestCase {
return true
}
}
+ Xcodes.Current.network.validateSession = {
+ return Just(())
+ .setFailureType(to: Error.self).eraseToAnyPublisher()
+ }
Xcodes.Current.network.dataTask = { urlRequest in
// Don't have a valid session
if urlRequest.url! == URLRequest.olympusSession.url! {
@@ -89,7 +93,7 @@ class AppStateTests: XCTestCase {
}
// It's an available release version
else if urlRequest.url! == URLRequest.downloads.url! {
- let downloads = Downloads(downloads: [Download(name: "Xcode 0.0.0", files: [Download.File(remotePath: "https://apple.com/xcode.xip", fileSize: 9484444)], dateModified: Date())])
+ let downloads = Downloads(resultCode: 0, resultsString: nil, downloads: [Download(name: "Xcode 0.0.0", files: [Download.File(remotePath: "https://apple.com/xcode.xip", fileSize: 9484444)], dateModified: Date())])
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(.downloadsDateModified)
let downloadsData = try! encoder.encode(downloads)
@@ -199,7 +203,7 @@ class AppStateTests: XCTestCase {
}
// It's an available release version
else if urlRequest.url! == URLRequest.downloads.url! {
- let downloads = Downloads(downloads: [Download(name: "Xcode 0.0.0", files: [Download.File(remotePath: "https://apple.com/xcode.xip", fileSize: 9494944)], dateModified: Date())])
+ let downloads = Downloads(resultCode: 0, resultsString: nil, downloads: [Download(name: "Xcode 0.0.0", files: [Download.File(remotePath: "https://apple.com/xcode.xip", fileSize: 9494944)], dateModified: Date())])
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(.downloadsDateModified)
let downloadsData = try! encoder.encode(downloads)
diff --git a/XcodesTests/Environment+Mock.swift b/XcodesTests/Environment+Mock.swift
index ed91706..8755da5 100644
--- a/XcodesTests/Environment+Mock.swift
+++ b/XcodesTests/Environment+Mock.swift
@@ -68,6 +68,11 @@ extension Network {
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
)
+ },
+ validateSession: {
+ return Just(())
+ .setFailureType(to: Error.self)
+ .eraseToAnyPublisher()
}
)
}
diff --git a/experiment_dark.png b/experiment_dark.png
new file mode 100644
index 0000000..bfb498a
Binary files /dev/null and b/experiment_dark.png differ
diff --git a/experiment_light.png b/experiment_light.png
new file mode 100644
index 0000000..fc71b91
Binary files /dev/null and b/experiment_light.png differ
diff --git a/nextstep.md b/nextstep.md
new file mode 100644
index 0000000..79aa13d
--- /dev/null
+++ b/nextstep.md
@@ -0,0 +1,15 @@
+## The next step
+
+As Xcodes continues to grow beyond a small little utility used by Apple Developers all over the world to make their lives easier when it comes to downloading, managing and saving their sanity with Xcode, it was time to move the repo away from Robots and Pencils and into it's own managed org.
+
+Starting April 21, 2023, all Xcodes repos are now contained under the `XcodesOrg` organization. https://github.com/XcodesOrg
+
+This change will have no effect on the tools at all, but allows the utilites to grow to the next level. I'm (Matt Kiazyk) still the owner and current sole maintainer, and have no plans to stop.
+
+Xcodes would not be where it is without contributors over the years. It would also not be where it is without each and every one of you sharing the tool.
+
+So from myself - thank you!
+
+Matt Kiazyk - maintainer
+@XcodesApp
+@mattkiazyk
\ No newline at end of file
diff --git a/screenshot_dark.png b/screenshot_dark.png
new file mode 100644
index 0000000..cf4a6d7
Binary files /dev/null and b/screenshot_dark.png differ
diff --git a/screenshot_light.png b/screenshot_light.png
new file mode 100644
index 0000000..b4d2dec
Binary files /dev/null and b/screenshot_light.png differ