diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..b51f769 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,134 @@ +# GitHub Actions Workflows + +本项目包含一个 GitHub Actions workflow 用于自动构建和发布 DMG 包。 + +## Workflow + +### `build-universal-dmg.yml` - DMG 构建和发布 + +支持构建多种架构的 DMG 包: +- **ARM64**(默认)- Apple Silicon Mac +- **Universal** - 同时支持 ARM64 和 x86_64 +- **x86_64** - Intel Mac + +**触发方式:** +- 推送以 `v` 开头的 tag(默认构建 ARM64) +- 手动触发(可选择构建类型) + +## 使用方法 + +### 自动发布(推荐) + +1. **创建并推送 tag:** + ```bash + git tag v2.7.2 + git push origin v2.7.2 + ``` + +2. **Workflow 会自动:** + - 检测到 tag 推送 + - 默认构建 ARM64 版本 + - 创建 DMG 包 + - 发布到 GitHub Releases + +### 手动触发 + +1. 进入 GitHub 仓库的 **Actions** 标签页 +2. 选择 **Build and Release DMG** workflow +3. 点击 **Run workflow** +4. 输入版本号(例如:`v2.7.2`) +5. 选择构建类型(arm64/universal/x86_64,默认 arm64) +6. 点击 **Run workflow** + +## 构建产物 + +构建完成后,会在 GitHub Releases 中创建: +- **Release 标题**:`Release v2.7.2 (arm64)` 或 `Release v2.7.2 (universal)` 等 +- **DMG 文件**:根据构建类型命名,例如: + - `Vagrant Manager-2.7.2-arm64.dmg`(ARM64) + - `Vagrant Manager-2.7.2-universal.dmg`(Universal) + - `Vagrant Manager-2.7.2-x86_64.dmg`(Intel) + +## 系统要求 + +构建环境: +- macOS 14 (Sonoma) +- Xcode(通过 GitHub Actions 自动安装) +- Node.js 20(用于 appdmg) +- CocoaPods(自动安装) + +## 注意事项 + +1. **代码签名**:当前 workflow 使用 `CODE_SIGNING_REQUIRED=NO`,如果需要代码签名,需要: + - 配置代码签名证书 + - 更新 workflow 中的签名设置 + +2. **发布权限**:确保 GitHub token 有创建 Release 的权限(默认的 `GITHUB_TOKEN` 通常已足够) + +3. **DMG 配置**:DMG 的布局和样式在 `dmg/appdmg.json` 中配置 + +4. **版本号格式**:建议使用语义化版本号,例如 `v2.7.2`、`v2.8.0` 等 + +## 故障排除 + +### 构建失败 + +1. **检查日志**:在 GitHub Actions 中查看详细的构建日志 +2. **验证依赖**:确保 `Podfile` 和 `Podfile.lock` 正确 +3. **检查 Xcode 版本**:确保使用的 macOS runner 版本支持所需的 Xcode 版本 + +### DMG 创建失败 + +1. **检查 appdmg 配置**:验证 `dmg/appdmg.json` 格式正确 +2. **检查资源文件**:确保 `dmg/` 目录中的图片文件存在 +3. **验证 .app 文件**:确保 Xcode 构建成功生成了 .app 文件 + +### 发布失败 + +1. **检查权限**:确保 GitHub token 有创建 Release 的权限 +2. **检查 tag**:确保 tag 格式正确(以 `v` 开头) +3. **检查 Release 是否存在**:如果 Release 已存在,workflow 会失败 + +## 自定义配置 + +### 修改构建架构 + +编辑 workflow 文件中的 `-arch` 参数: +```yaml +-arch arm64 # 仅 ARM64 +-arch x86_64 # 仅 Intel +-arch arm64 -arch x86_64 # Universal +``` + +### 修改 macOS 版本 + +修改 `runs-on` 字段: +```yaml +runs-on: macos-14 # macOS 14 (Sonoma) +runs-on: macos-13 # macOS 13 (Ventura) +runs-on: macos-12 # macOS 12 (Monterey) +``` + +### 添加代码签名 + +如果需要代码签名,需要: +1. 在 GitHub Secrets 中添加证书和密钥 +2. 更新 workflow 中的签名步骤 + +示例: +```yaml +- name: Import Certificate + uses: apple-actions/import-codesign-certs@v1 + with: + p12-file-base64: ${{ secrets.CERTIFICATES_P12 }} + p12-password: ${{ secrets.CERTIFICATES_PASSWORD }} + +# 然后在 xcodebuild 命令中移除 CODE_SIGNING_REQUIRED=NO +``` + +## 相关文件 + +- `dmg/appdmg.json` - DMG 配置文件 +- `Podfile` - CocoaPods 依赖配置 +- `.github/workflows/` - Workflow 文件目录 + diff --git a/.github/workflows/build-universal-dmg.yml b/.github/workflows/build-universal-dmg.yml new file mode 100644 index 0000000..99819f4 --- /dev/null +++ b/.github/workflows/build-universal-dmg.yml @@ -0,0 +1,196 @@ +name: Build and Release DMG + +on: + push: + tags: + - 'v*' # 当推送以 v 开头的 tag 时触发,默认构建 ARM64 + workflow_dispatch: # 允许手动触发 + inputs: + version: + description: 'Version tag (e.g., v2.7.2)' + required: true + type: string + build_type: + description: 'Build type' + required: false + type: choice + options: + - arm64 + - universal + - x86_64 + default: arm64 + +jobs: + build-dmg: + name: Build DMG + runs-on: macos-14 # macOS 14 with Apple Silicon support + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install appdmg + run: npm install -g appdmg + + - name: Install CocoaPods dependencies + run: | + if ! command -v pod &> /dev/null; then + sudo gem install cocoapods + fi + pod install + + - name: Get version from tag + id: get_version + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.version }}" + BUILD_TYPE="${{ github.event.inputs.build_type || 'arm64' }}" + else + VERSION="${GITHUB_REF#refs/tags/}" + BUILD_TYPE="arm64" + fi + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + echo "VERSION_NAME=${VERSION#v}" >> $GITHUB_OUTPUT + echo "BUILD_TYPE=${BUILD_TYPE}" >> $GITHUB_OUTPUT + echo "Building version: ${VERSION} (${BUILD_TYPE})" + + - name: Determine architectures + id: archs + run: | + BUILD_TYPE="${{ steps.get_version.outputs.BUILD_TYPE }}" + if [ "${BUILD_TYPE}" == "universal" ]; then + ARCHS="arm64 x86_64" + ARCH_SUFFIX="universal" + elif [ "${BUILD_TYPE}" == "arm64" ]; then + ARCHS="arm64" + ARCH_SUFFIX="arm64" + else + ARCHS="x86_64" + ARCH_SUFFIX="x86_64" + fi + echo "archs=${ARCHS}" >> $GITHUB_OUTPUT + echo "arch_suffix=${ARCH_SUFFIX}" >> $GITHUB_OUTPUT + echo "Building for architectures: ${ARCHS}" + + - name: Build Xcode project + run: | + xcodebuild clean -workspace "Vagrant Manager.xcworkspace" \ + -scheme "Vagrant Manager" \ + -configuration Release + + xcodebuild archive -workspace "Vagrant Manager.xcworkspace" \ + -scheme "Vagrant Manager" \ + -configuration Release \ + -arch ${{ steps.archs.outputs.archs }} \ + -sdk macosx \ + -archivePath "$PWD/build/Vagrant Manager.xcarchive" \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGNING_ALLOWED=NO + + - name: Create export options plist + run: | + cat > exportOptions.plist < + + + + method + mac-application + signingStyle + manual + + + EOF + + - name: Export .app from archive + run: | + xcodebuild -exportArchive \ + -archivePath "$PWD/build/Vagrant Manager.xcarchive" \ + -exportPath "$PWD/build/export" \ + -exportOptionsPlist "$PWD/exportOptions.plist" + + - name: Verify architectures + run: | + file "build/export/Vagrant Manager.app/Contents/MacOS/Vagrant Manager" + lipo -info "build/export/Vagrant Manager.app/Contents/MacOS/Vagrant Manager" + + - name: Remove quarantine attribute from .app + run: | + xattr -cr "build/export/Vagrant Manager.app" + + - name: Copy .app to dmg directory + run: | + cp -R "build/export/Vagrant Manager.app" "dmg/" + + - name: Create DMG + working-directory: dmg + run: | + appdmg appdmg.json "../vagrant-manager-${{ steps.get_version.outputs.VERSION_NAME }}-${{ steps.archs.outputs.arch_suffix }}.dmg" + + - name: Verify DMG + run: | + hdiutil verify "vagrant-manager-${{ steps.get_version.outputs.VERSION_NAME }}-${{ steps.archs.outputs.arch_suffix }}.dmg" + echo "DMG verification completed successfully" + + - name: Get DMG size + id: dmg_size + run: | + DMG_SIZE=$(du -h "vagrant-manager-${{ steps.get_version.outputs.VERSION_NAME }}-${{ steps.archs.outputs.arch_suffix }}.dmg" | cut -f1) + echo "size=${DMG_SIZE}" >> $GITHUB_OUTPUT + echo "DMG size: ${DMG_SIZE}" + + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.get_version.outputs.VERSION }} + name: Release ${{ steps.get_version.outputs.VERSION }} (${{ steps.archs.outputs.arch_suffix }}) + body: | + ## Vagrant Manager ${{ steps.get_version.outputs.VERSION_NAME }} (${{ steps.archs.outputs.arch_suffix }}) + + ### 🍎 Architecture Support + This release is built for **${{ steps.archs.outputs.arch_suffix }}** architecture. + ${{ steps.archs.outputs.arch_suffix == 'universal' && 'Supports both Apple Silicon (ARM64) and Intel (x86_64) Macs.' || '' }} + ${{ steps.archs.outputs.arch_suffix == 'arm64' && 'Supports Apple Silicon (M1/M2/M3/M4/M5) Macs only.' || '' }} + ${{ steps.archs.outputs.arch_suffix == 'x86_64' && 'Supports Intel Macs only.' || '' }} + + ### 📦 Installation + 1. Download the DMG file + 2. Open the DMG + 3. Drag "Vagrant Manager" to Applications folder + + **Note:** If macOS shows "Vagrant Manager is damaged" error: + - Right-click the app and select "Open" (don't double-click) + - Or run: `xattr -cr /Applications/Vagrant\ Manager.app` in Terminal + - Then try opening again + + ### 📋 Changes + - Built for ${{ steps.archs.outputs.arch_suffix }} architecture + - Requires macOS 11.0 (Big Sur) or later + - Updated Sparkle to 2.x for better ARM64 support + + ### 🔧 System Requirements + - macOS 11.0 (Big Sur) or later + ${{ steps.archs.outputs.arch_suffix == 'universal' && '- Apple Silicon (M1/M2/M3/M4/M5) or Intel Mac' || '' }} + ${{ steps.archs.outputs.arch_suffix == 'arm64' && '- Apple Silicon Mac (M1/M2/M3/M4/M5)' || '' }} + ${{ steps.archs.outputs.arch_suffix == 'x86_64' && '- Intel Mac' || '' }} + - Vagrant installed and in PATH + - VirtualBox or Parallels installed (if using those providers) + + ### 📊 Build Info + - **Architecture**: ${{ steps.archs.outputs.arch_suffix }} + - **DMG Size**: ${{ steps.dmg_size.outputs.size }} + - **Build Date**: ${{ github.event.head_commit.timestamp || github.run_started_at }} + draft: false + prerelease: false + files: | + vagrant-manager-${{ steps.get_version.outputs.VERSION_NAME }}-${{ steps.archs.outputs.arch_suffix }}.dmg + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.gitignore b/.gitignore index 66ca847..35306f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,5 @@ -# OS X -.DS_Store - # Xcode +# build/ *.pbxuser !default.pbxuser @@ -13,16 +11,51 @@ build/ !default.perspectivev3 xcuserdata *.xccheckout -profile *.moved-aside DerivedData *.hmap *.ipa +*.xcuserstate +project.xcworkspace # CocoaPods -Pods -assets/clean.psd -assets/default.psd -assets/flat.psd -assets/indicators.psd -assets/problem.psd +# +Pods/ + +# App packaging +# +*.dmg +*.app.dSYM.zip +*.app.dSYM + +# Build artifacts +# +exportOptions.plist +*.xcarchive + +# OS X +# +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +.cursor diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..12d8f93 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,281 @@ +AllCops: + NewCops: enable + +Gemspec/AddRuntimeDependency: + Enabled: true +Gemspec/AttributeAssignment: + Enabled: true +Gemspec/DeprecatedAttributeAssignment: + Enabled: true +Gemspec/DevelopmentDependencies: + Enabled: true +Gemspec/RequireMFA: + Enabled: true +Layout/LineContinuationLeadingSpace: + Enabled: true +Layout/LineContinuationSpacing: + Enabled: true +Layout/LineEndStringConcatenationIndentation: + Enabled: true +Layout/SpaceBeforeBrackets: + Enabled: true +Lint/AmbiguousAssignment: + Enabled: true +Lint/AmbiguousOperatorPrecedence: + Enabled: true +Lint/AmbiguousRange: + Enabled: true +Lint/ArrayLiteralInRegexp: + Enabled: true +Lint/ConstantOverwrittenInRescue: + Enabled: true +Lint/ConstantReassignment: + Enabled: true +Lint/CopDirectiveSyntax: + Enabled: true +Lint/DeprecatedConstants: + Enabled: true +Lint/DuplicateBranch: + Enabled: true +Lint/DuplicateMagicComment: + Enabled: true +Lint/DuplicateMatchPattern: + Enabled: true +Lint/DuplicateRegexpCharacterClassElement: + Enabled: true +Lint/DuplicateSetElement: + Enabled: true +Lint/EmptyBlock: + Enabled: true +Lint/EmptyClass: + Enabled: true +Lint/EmptyInPattern: + Enabled: true +Lint/HashNewWithKeywordArgumentsAsDefault: + Enabled: true +Lint/IncompatibleIoSelectWithFiberScheduler: + Enabled: true +Lint/ItWithoutArgumentsInBlock: + Enabled: true +Lint/LambdaWithoutLiteralBlock: + Enabled: true +Lint/LiteralAssignmentInCondition: + Enabled: true +Lint/MixedCaseRange: + Enabled: true +Lint/NoReturnInBeginEndBlocks: + Enabled: true +Lint/NonAtomicFileOperation: + Enabled: true +Lint/NumberedParameterAssignment: + Enabled: true +Lint/NumericOperationWithConstantResult: + Enabled: true +Lint/OrAssignmentToConstant: + Enabled: true +Lint/RedundantDirGlobSort: + Enabled: true +Lint/RedundantRegexpQuantifiers: + Enabled: true +Lint/RedundantTypeConversion: + Enabled: true +Lint/RefinementImportMethods: + Enabled: true +Lint/RequireRangeParentheses: + Enabled: true +Lint/RequireRelativeSelfPath: + Enabled: true +Lint/SharedMutableDefault: + Enabled: true +Lint/SuppressedExceptionInNumberConversion: + Enabled: true +Lint/SymbolConversion: + Enabled: true +Lint/ToEnumArguments: + Enabled: true +Lint/TripleQuotes: + Enabled: true +Lint/UnescapedBracketInRegexp: + Enabled: true +Lint/UnexpectedBlockArity: + Enabled: true +Lint/UnmodifiedReduceAccumulator: + Enabled: true +Lint/UselessConstantScoping: + Enabled: true +Lint/UselessDefaultValueArgument: + Enabled: true +Lint/UselessDefined: + Enabled: true +Lint/UselessNumericOperation: + Enabled: true +Lint/UselessOr: + Enabled: true +Lint/UselessRescue: + Enabled: true +Lint/UselessRuby2Keywords: + Enabled: true +Metrics/CollectionLiteralLength: + Enabled: true +Naming/BlockForwarding: + Enabled: true +Naming/PredicateMethod: + Enabled: true +Security/CompoundHash: + Enabled: true +Security/IoMethods: + Enabled: true +Style/AmbiguousEndlessMethodDefinition: + Enabled: true +Style/ArgumentsForwarding: + Enabled: true +Style/ArrayIntersect: + Enabled: true +Style/BitwisePredicate: + Enabled: true +Style/CollectionCompact: + Enabled: true +Style/CollectionQuerying: + Enabled: true +Style/CombinableDefined: + Enabled: true +Style/ComparableBetween: + Enabled: true +Style/ComparableClamp: + Enabled: true +Style/ConcatArrayLiterals: + Enabled: true +Style/DataInheritance: + Enabled: true +Style/DigChain: + Enabled: true +Style/DirEmpty: + Enabled: true +Style/DocumentDynamicEvalDefinition: + Enabled: true +Style/EmptyHeredoc: + Enabled: true +Style/EmptyStringInsideInterpolation: + Enabled: true +Style/EndlessMethod: + Enabled: true +Style/EnvHome: + Enabled: true +Style/ExactRegexpMatch: + Enabled: true +Style/FetchEnvVar: + Enabled: true +Style/FileEmpty: + Enabled: true +Style/FileNull: + Enabled: true +Style/FileRead: + Enabled: true +Style/FileTouch: + Enabled: true +Style/FileWrite: + Enabled: true +Style/HashConversion: + Enabled: true +Style/HashExcept: + Enabled: true +Style/HashFetchChain: + Enabled: true +Style/HashSlice: + Enabled: true +Style/IfWithBooleanLiteralBranches: + Enabled: true +Style/InPatternThen: + Enabled: true +Style/ItAssignment: + Enabled: true +Style/ItBlockParameter: + Enabled: true +Style/KeywordArgumentsMerging: + Enabled: true +Style/MagicCommentFormat: + Enabled: true +Style/MapCompactWithConditionalBlock: + Enabled: true +Style/MapIntoArray: + Enabled: true +Style/MapToHash: + Enabled: true +Style/MapToSet: + Enabled: true +Style/MinMaxComparison: + Enabled: true +Style/MultilineInPatternThen: + Enabled: true +Style/NegatedIfElseCondition: + Enabled: true +Style/NestedFileDirname: + Enabled: true +Style/NilLambda: + Enabled: true +Style/NumberedParameters: + Enabled: true +Style/NumberedParametersLimit: + Enabled: true +Style/ObjectThen: + Enabled: true +Style/OpenStructUse: + Enabled: true +Style/OperatorMethodCall: + Enabled: true +Style/QuotedSymbols: + Enabled: true +Style/RedundantArgument: + Enabled: true +Style/RedundantArrayConstructor: + Enabled: true +Style/RedundantArrayFlatten: + Enabled: true +Style/RedundantConstantBase: + Enabled: true +Style/RedundantCurrentDirectoryInPath: + Enabled: true +Style/RedundantDoubleSplatHashBraces: + Enabled: true +Style/RedundantEach: + Enabled: true +Style/RedundantFilterChain: + Enabled: true +Style/RedundantFormat: + Enabled: true +Style/RedundantHeredocDelimiterQuotes: + Enabled: true +Style/RedundantInitialize: + Enabled: true +Style/RedundantInterpolationUnfreeze: + Enabled: true +Style/RedundantLineContinuation: + Enabled: true +Style/RedundantRegexpArgument: + Enabled: true +Style/RedundantRegexpConstructor: + Enabled: true +Style/RedundantSelfAssignmentBranch: + Enabled: true +Style/RedundantStringEscape: + Enabled: true +Style/ReturnNilInPredicateMethodDefinition: + Enabled: true +Style/SafeNavigationChainLength: + Enabled: true +Style/SelectByRegexp: + Enabled: true +Style/SendWithLiteralMethodName: + Enabled: true +Style/SingleLineDoEndBlock: + Enabled: true +Style/StringChars: + Enabled: true +Style/SuperArguments: + Enabled: true +Style/SuperWithArgsParentheses: + Enabled: true +Style/SwapValues: + Enabled: true +Style/YAMLFileRead: + Enabled: true diff --git a/Podfile b/Podfile index 592691c..e6d195a 100644 --- a/Podfile +++ b/Podfile @@ -1,6 +1,6 @@ -platform :osx, '10.8' +platform :osx, '11.0' target "Vagrant Manager" do - pod 'Sparkle' + pod 'Sparkle', '~> 2.0' end diff --git a/Podfile.lock b/Podfile.lock index 91c373a..3c89a2a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - Sparkle (1.22.0) + - Sparkle (2.8.1) DEPENDENCIES: - - Sparkle + - Sparkle (~> 2.0) SPEC REPOS: trunk: - Sparkle SPEC CHECKSUMS: - Sparkle: 593ac2e677c07bcb6c3b22d621240e7cbedaab57 + Sparkle: a346a4341537c625955751ed3ae4b340b68551fa -PODFILE CHECKSUM: e45240f106f07044e1efd268e5e93b5b059de8a3 +PODFILE CHECKSUM: cb8e6b853ca1f2f2d25cf10974384f5335c9b5d4 -COCOAPODS: 1.8.4 +COCOAPODS: 1.16.2 diff --git a/README.md b/README.md index 299b0a5..b78a991 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,56 @@ -Looking for the Windows version? Check out [Vagrant Manager for Windows](https://github.com/lanayotech/vagrant-manager-windows) +寻找 Windows 版本?请查看 [Vagrant Manager for Windows](https://github.com/lanayotech/vagrant-manager-windows) -# Vagrant Manager for OS X +# Vagrant Manager for macOS -Vagrant Manager is an OS X status bar menu app that lets you manage all of your vagrant machines from one central location. -More information is available at http://vagrantmanager.com/ +Vagrant Manager 是一个 macOS 状态栏菜单应用程序,可让您从一个中心位置管理所有 Vagrant 虚拟机。 +更多信息请访问 http://vagrantmanager.com/ ![demo.gif](http://vagrantmanager.com/demo.gif) -## Downloads -Download Vagrant Manager from the [GitHub Releases Page](https://github.com/lanayotech/vagrant-manager/releases) +## 下载 +从 [GitHub Releases 页面](https://github.com/kevin197011/vagrant-manager/releases) 下载 Vagrant Manager -## Installation Notes -* Vagrant Manager can automatically detect most machines, undetected machines will require manual configuration via bookmarks. -* Make sure that you have VirtualBox and Vagrant installed, and the `VBoxManage` and `vagrant` commands are in your path so that Vagrant Manager can execute them. If you use Parallels, ensure the `prlctl` command is also in your path. -* Currently, vagrant machines must already be initialized in order for Vagrant Manager to detect them. Make sure you have run vagrant init on any machine you want to appear in Vagrant Manager. Once Vagrant Manager has detected a machine, you can bookmark it so that it will not disappear when you destroy the machine. You can also manually add a bookmark and specify the path to your Vagrantfile. -* Vagrant Manager requires OS X 10.8 Mountain Lion or higher +## 安装说明 +* Vagrant Manager 可以自动检测大多数虚拟机,未检测到的虚拟机需要通过书签进行手动配置。 +* 请确保您已安装 VirtualBox 和 Vagrant,并且 `VBoxManage` 和 `vagrant` 命令在您的 PATH 中,以便 Vagrant Manager 可以执行它们。如果您使用 Parallels,请确保 `prlctl` 命令也在您的 PATH 中。 +* 目前,vagrant 虚拟机必须已经初始化才能被 Vagrant Manager 检测到。请确保您已在任何想要出现在 Vagrant Manager 中的虚拟机上运行了 vagrant init。一旦 Vagrant Manager 检测到虚拟机,您可以将其添加为书签,这样在销毁虚拟机后它也不会消失。您也可以手动添加书签并指定 Vagrantfile 的路径。 +* Vagrant Manager 需要 macOS 11.0 (Big Sur) 或更高版本 -## Building DMG -* Use [appdmg](https://github.com/LinusU/node-appdmg) to build the distribution DMG. -* Put the `Vagrant Manager.app` file in the `dmg` foler -* Run `appdmg dmg/appdmg.json ` +## 系统要求 +* macOS 11.0 (Big Sur) 或更高版本 +* Apple Silicon (M1/M2/M3/M4/M5) 或 Intel Mac +* Vagrant 已安装并在 PATH 中 +* VirtualBox 或 Parallels 已安装(如果使用这些提供商) -## Contributing to Vagrant Manager +## Apple Silicon 支持 +Vagrant Manager 现在支持 Apple Silicon (ARM64) Mac!最新版本包含针对 M1/M2/M3/M4/M5 Mac 的原生 ARM64 构建,以获得最佳性能。 -We love code contributions! If you would like to contribute, here are some notes and guidlines: +**注意:** 如果 macOS 在下载后显示 "Vagrant Manager 已损坏" 错误: +* 右键点击应用程序并选择"打开"(不要双击) +* 或在终端中运行:`xattr -cr /Applications/Vagrant\ Manager.app` +* 然后再次尝试打开 -* All development happens on the **develop** branch, so it is always the most up-to-date -* The **master** branch only contains tagged releases -* If you are going to be submitting a pull request, please branch from **develop**, and submit your pull request back to the **develop** branch -* [Helpful article about forking](https://help.github.com/articles/fork-a-repo) -* [Helpful article about pull requests](https://help.github.com/articles/using-pull-requests) +## 构建 DMG +* 使用 [appdmg](https://github.com/LinusU/node-appdmg) 构建分发 DMG。 +* 将 `Vagrant Manager.app` 文件放入 `dmg` 文件夹 +* 运行 `appdmg dmg/appdmg.json ` + +或使用自动化的 GitHub Actions workflow: +* 推送一个 tag(例如 `v2.8.0`)以自动构建和发布 DMG 包 +* 查看 `.github/workflows/README.md` 了解更多详情 + +## 语言支持 +Vagrant Manager 支持多语言: +* 英语 (English) +* 简体中文 (Simplified Chinese) + +您可以在偏好设置中切换语言。首次启动时会自动检测系统语言。 + +## 贡献代码 + +我们欢迎代码贡献!如果您想贡献代码,以下是一些说明和指南: + +* 所有开发都在 **main** 分支上进行 +* 如果您要提交 pull request,请从 **main** 分支创建分支,并将您的 pull request 提交回 **main** 分支 +* [关于 fork 的有用文章](https://help.github.com/articles/fork-a-repo) +* [关于 pull requests 的有用文章](https://help.github.com/articles/using-pull-requests) diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..833903f --- /dev/null +++ b/Rakefile @@ -0,0 +1,394 @@ +# frozen_string_literal: true + +# Copyright (c) 2025 kk +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +require 'time' + +task default: %w[push] + +# 生成智能 commit message +def generate_commit_message + # 获取暂存区的变更 + diff_output = `git diff --cached --name-status 2>&1` + return nil if diff_output.empty? || !$?.success? + + changed_files = diff_output.split("\n") + return nil if changed_files.empty? + + # 分析变更类型 + types = [] + scopes = [] + file_descriptions = [] + + changed_files.each do |line| + status, file = line.split("\t", 2) + next unless file + + type, scope, description = analyze_file_change(status, file) + types << type if type + scopes << scope if scope + file_descriptions << description if description + end + + # 确定主要的 commit type(优先级:feat > fix > docs > refactor > style > perf > test > chore) + type_priority = { + 'feat' => 1, + 'fix' => 2, + 'docs' => 3, + 'refactor' => 4, + 'style' => 5, + 'perf' => 6, + 'test' => 7, + 'chore' => 8 + } + + main_type = types.min_by { |t| type_priority[t] || 9 } || 'chore' + main_scope = scopes.compact.uniq.first || 'general' + + # 生成 subject + subject = generate_subject(main_type, main_scope, file_descriptions) + + # 生成 body(如果有多个文件变更) + body = generate_body(changed_files) if changed_files.length > 1 + + # 组合 commit message + message = "#{main_type}(#{main_scope}): #{subject}" + message += "\n\n#{body}" if body + + message +end + +# 分析单个文件的变更 +def analyze_file_change(status, file) + type = nil + scope = nil + description = nil + + # 根据文件路径和状态判断类型 + case file + when %r{^rules/} + type = 'docs' + scope = 'rules' + description = "更新规则文档: #{File.basename(file)}" + when %r{^backend/} + type = status == 'A' ? 'feat' : 'refactor' + scope = 'backend' + description = "#{status == 'A' ? '新增' : '更新'}后端代码: #{File.basename(file)}" + when %r{^frontend/} + type = status == 'A' ? 'feat' : 'refactor' + scope = 'frontend' + description = "#{status == 'A' ? '新增' : '更新'}前端代码: #{File.basename(file)}" + when /\.(rb|rake)$/ + type = 'chore' + scope = 'scripts' + description = "更新脚本: #{File.basename(file)}" + when /\.(sh|bash)$/ + type = 'chore' + scope = 'scripts' + description = "更新脚本: #{File.basename(file)}" + when /\.(md|mdx|txt)$/ + type = 'docs' + scope = 'docs' + description = "更新文档: #{File.basename(file)}" + when /\.(yml|yaml)$/ + type = 'ci' + scope = 'ci' + description = "更新 CI 配置: #{File.basename(file)}" + when /\.(json)$/ + type = 'chore' + scope = 'config' + description = "更新配置: #{File.basename(file)}" + when /\.(go)$/ + type = status == 'A' ? 'feat' : (status == 'D' ? 'refactor' : 'fix') + scope = 'backend' + description = "#{status == 'A' ? '新增' : status == 'D' ? '删除' : '更新'} Go 文件: #{File.basename(file)}" + when /\.(ts|tsx|js|jsx)$/ + type = status == 'A' ? 'feat' : (status == 'D' ? 'refactor' : 'fix') + scope = 'frontend' + description = "#{status == 'A' ? '新增' : status == 'D' ? '删除' : '更新'} 前端文件: #{File.basename(file)}" + else + type = 'chore' + scope = 'general' + description = "#{status == 'A' ? '新增' : status == 'D' ? '删除' : '更新'} 文件: #{File.basename(file)}" + end + + # 根据状态调整类型 + case status + when 'D' + type = 'refactor' if type == 'feat' + when 'M' + # 检查是否是修复(通过关键词) + if file.match?(/fix|bug|error|issue/i) + type = 'fix' + end + end + + [type, scope, description] +end + +# 生成 subject +def generate_subject(type, scope, descriptions) + return '更新项目文件' if descriptions.empty? + + # 如果只有一个文件,使用更具体的描述 + if descriptions.length == 1 + desc = descriptions.first + # 提取关键信息 + case desc + when /更新规则文档/ + '更新开发规范' + when /新增.*后端/ + '新增后端功能' + when /更新.*后端/ + '更新后端代码' + when /新增.*前端/ + '新增前端功能' + when /更新.*前端/ + '更新前端代码' + when /更新脚本/ + '更新构建脚本' + when /更新文档/ + '更新项目文档' + else + desc.split(':').last&.strip || '更新项目文件' + end + else + # 多个文件,生成通用描述 + case type + when 'feat' + '添加新功能' + when 'fix' + '修复问题' + when 'docs' + '更新文档' + when 'refactor' + '重构代码' + when 'style' + '代码格式调整' + when 'perf' + '性能优化' + when 'test' + '更新测试' + when 'chore' + '项目维护' + else + '更新项目文件' + end + end +end + +# 生成 body +def generate_body(changed_files) + lines = ['变更文件:'] + changed_files.each do |line| + status, file = line.split("\t", 2) + next unless file + + status_icon = case status + when 'A' then '✨' + when 'D' then '🗑️' + when 'M' then '📝' + when 'R' then '🔄' + else '📄' + end + + lines << " #{status_icon} #{file}" + end + lines.join("\n") +end + +# 生成新的 tag 版本号 +def generate_new_tag + # 获取所有 tag + tags_output = `git tag -l 'v*' 2>&1` + return 'v2.8.4' unless $?.success? && !tags_output.empty? + + # 解析版本号并排序 + tags = tags_output.split("\n").select { |t| t.match?(/^v\d+\.\d+\.\d+$/) } + return 'v2.8.4' if tags.empty? + + # 获取最新的 tag + latest_tag = tags.sort_by do |tag| + version = tag.gsub(/^v/, '').split('.').map(&:to_i) + [version[0], version[1], version[2]] + end.last + + # 解析版本号 + version_parts = latest_tag.gsub(/^v/, '').split('.').map(&:to_i) + major, minor, patch = version_parts + + # 递增 patch 版本号 + new_version = "v#{major}.#{minor}.#{patch + 1}" + new_version +end + +# 更新 Xcode 项目版本号 +def update_xcode_version(version) + project_file = 'Vagrant Manager.xcodeproj/project.pbxproj' + return false unless File.exist?(project_file) + + content = File.read(project_file) + + # 更新 MARKETING_VERSION 和 CURRENT_PROJECT_VERSION + updated_content = content.gsub(/MARKETING_VERSION = [\d.]+;/, "MARKETING_VERSION = #{version};") + updated_content = updated_content.gsub(/CURRENT_PROJECT_VERSION = [\d.]+;/, "CURRENT_PROJECT_VERSION = #{version};") + + # 如果内容有变化,写入文件 + if updated_content != content + File.write(project_file, updated_content) + puts "✅ 更新项目版本号: #{version}" + return true + end + + false +end + +# 创建并推送 tag +def create_and_push_tag(tag_name, commit_message) + # 检查 tag 是否已存在 + tag_check = `git tag -l #{tag_name} 2>&1` + if tag_check.include?(tag_name) + puts "⚠️ Tag #{tag_name} 已存在,跳过创建" + return false + end + + # 从 tag 名称中提取版本号(去掉 'v' 前缀) + version = tag_name.gsub(/^v/, '') + + # 更新 Xcode 项目版本号 + version_updated = update_xcode_version(version) + + # 如果有版本号更新,需要先提交 + if version_updated + system('git add "Vagrant Manager.xcodeproj/project.pbxproj"') + version_commit_message = "chore: 更新版本号到 #{version}" + unless system("git commit -m \"#{version_commit_message}\"") + puts "❌ 提交版本号更新失败" + return false + end + puts "✅ 已提交版本号更新" + + # 推送到远程 + push_output = `git push origin main 2>&1` + unless $?.success? + puts "❌ 推送版本号更新失败" + puts push_output + return false + end + puts "✅ 已推送版本号更新" + end + + # 创建 tag(使用 commit message 的第一行作为 tag message) + tag_message = commit_message.lines.first.chomp + success = system("git tag -a #{tag_name} -m \"#{tag_message}\" 2>&1") + + unless success + puts "❌ 创建 tag 失败" + return false + end + + puts "✅ 创建 tag: #{tag_name}" + + # 推送 tag 到远程 + push_tag_output = `git push origin #{tag_name} 2>&1` + unless $?.success? + puts "❌ 推送 tag 失败" + puts push_tag_output + return false + end + + puts "✅ 推送 tag 成功: #{tag_name}" + true +end + +task :push do + # 检查是否有变更 + status_output = `git status --porcelain 2>&1` + if status_output.empty? || !$?.success? + puts '没有变更需要提交' + exit 0 + end + + # 添加所有变更 + system 'git add .' + + # 生成智能 commit message + commit_message = generate_commit_message || "chore: 更新项目文件\n\n#{Time.now}" + + # 创建临时文件存储 commit message + require 'tempfile' + temp_file = Tempfile.new('commit_message') + temp_file.write(commit_message) + temp_file.close + + # 使用临时文件提交 + success = system("git commit -F #{temp_file.path}") + + temp_file.unlink + + unless success + puts '提交失败' + exit 1 + end + + puts "✅ 提交成功: #{commit_message.lines.first.chomp}" + + # 拉取最新代码 + # 先检查是否有跟踪分支,如果没有则设置 + current_branch = `git rev-parse --abbrev-ref HEAD`.chomp + tracking_branch = `git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>&1`.chomp + + if tracking_branch.empty? || tracking_branch.include?('fatal') + # 设置跟踪分支 + system("git branch --set-upstream-to=origin/#{current_branch} #{current_branch} 2>&1") + end + + pull_output = `git pull 2>&1` + unless $?.success? + if pull_output.include?('conflict') || pull_output.include?('CONFLICT') + puts '❌ 检测到合并冲突,请手动解决后重试' + puts pull_output + exit 1 + elsif pull_output.include?('no tracking information') + # 如果还是没有跟踪信息,尝试直接拉取 + pull_output = `git pull origin #{current_branch} 2>&1` + unless $?.success? + puts '⚠️ 拉取失败,但继续推送' + puts pull_output if pull_output.length > 0 + end + else + puts '⚠️ 拉取失败,但继续推送' + puts pull_output if pull_output.length > 0 + end + end + + # 推送到远程 + push_output = `git push origin main 2>&1` + unless $?.success? + puts '❌ 推送失败' + puts push_output + exit 1 + end + + puts '✅ 推送成功' + + # 创建并推送新的 tag + new_tag = generate_new_tag + create_and_push_tag(new_tag, commit_message) +end + +task :run do + system 'docker compose down -v' + system 'docker compose up -d --build --remove-orphans' + system 'docker compose logs -f' +end + +# task :push do +# system 'git add .' +# system "git commit -m 'Update #{Time.now}'" +# system 'git pull' +# system 'git push origin main' +# end \ No newline at end of file diff --git a/SWIFT_MIGRATION_GUIDE.md b/SWIFT_MIGRATION_GUIDE.md new file mode 100644 index 0000000..8a89f8d --- /dev/null +++ b/SWIFT_MIGRATION_GUIDE.md @@ -0,0 +1,346 @@ +# Swift 迁移指南 + +## 📊 项目现状分析 + +### 当前状态 +- **语言**: Objective-C +- **文件数量**: 约 35 对 .h/.m 文件(70 个文件) +- **框架**: Cocoa/AppKit (macOS) +- **依赖管理**: CocoaPods (Sparkle) +- **最低系统版本**: macOS 11.0+ + +### 代码规模估算 +- 核心类: ~35 个 +- 平均每个类: 100-300 行代码 +- 总代码量: 约 5,000-10,000 行 + +## ✅ 迁移可行性 + +### 技术可行性:**完全可行** + +1. **Swift 与 Objective-C 互操作** + - ✅ Swift 可以直接调用 Objective-C 代码 + - ✅ Objective-C 可以调用 Swift 代码(通过桥接头文件) + - ✅ 支持混合项目,可以逐步迁移 + +2. **框架支持** + - ✅ AppKit 完全支持 Swift + - ✅ CocoaPods 支持 Swift 项目 + - ✅ Sparkle 框架支持 Swift + +3. **Xcode 支持** + - ✅ Xcode 原生支持混合项目 + - ✅ 自动生成桥接头文件 + - ✅ 无缝编译和调试 + +## 🎯 迁移策略 + +### 方案一:渐进式迁移(推荐)⭐ + +**优点**: +- 风险低,可以逐步验证 +- 不影响现有功能 +- 可以边迁移边测试 +- 团队可以逐步学习 Swift + +**步骤**: + +#### 阶段 1:准备工作(1-2 天) +1. 在 Xcode 中启用 Swift 支持 +2. 创建桥接头文件(Bridging Header) +3. 配置 Swift 编译设置 +4. 添加 Swift 文件到项目 + +#### 阶段 2:迁移工具类(1 周) +优先迁移简单的工具类,建立信心: +- `LanguageManager` ✅ (已部分完成) +- `Util` +- `Environment` +- `VersionComparison` +- `PasswordHelper` + +#### 阶段 3:迁移数据模型(1 周) +- `Bookmark` +- `CustomCommand` +- `CustomProvider` +- `VagrantInstance` +- `VagrantMachine` + +#### 阶段 4:迁移业务逻辑(1-2 周) +- `VagrantManager` +- `BookmarkManager` +- `CustomCommandManager` +- `VagrantGlobalStatusScanner` + +#### 阶段 5:迁移 UI 层(1-2 周) +- `AppDelegate` +- `NativeMenu` +- `NativeMenuItem` +- `PreferencesWindow` +- 其他 Window Controllers + +#### 阶段 6:清理和优化(1 周) +- 移除所有 Objective-C 文件 +- 优化 Swift 代码 +- 更新文档 + +### 方案二:完全重写(不推荐) + +**缺点**: +- 工作量大(2-3 个月) +- 风险高 +- 需要完全停止新功能开发 +- 测试工作量大 + +## 📝 迁移示例 + +### 示例 1:LanguageManager (Objective-C → Swift) + +**Objective-C 版本**: +```objective-c +@interface LanguageManager : NSObject ++ (LanguageManager*)sharedManager; +- (NSString*)localizedString:(NSString*)key; +@end +``` + +**Swift 版本**: +```swift +class LanguageManager { + static let shared = LanguageManager() + + private var languageBundle: Bundle? + + private init() { + loadLanguageBundle() + } + + func localizedString(_ key: String) -> String { + if let bundle = languageBundle { + return bundle.localizedString(forKey: key, value: key, table: nil) + } + return NSLocalizedString(key, comment: "") + } + + private func loadLanguageBundle() { + let languageCode = getCurrentLanguage() + if let path = Bundle.main.path(forResource: languageCode, ofType: "lproj"), + let bundle = Bundle(path: path) { + languageBundle = bundle + } else { + languageBundle = Bundle.main + } + } + + func getCurrentLanguage() -> String { + if let savedLanguage = UserDefaults.standard.string(forKey: "appLanguage"), + !savedLanguage.isEmpty { + return savedLanguage + } + + if let systemLanguage = Locale.preferredLanguages.first, + systemLanguage.hasPrefix("zh") { + return "zh-Hans" + } + + return "en" + } +} + +// 全局函数替代宏 +func VMLocalizedString(_ key: String) -> String { + return LanguageManager.shared.localizedString(key) +} +``` + +### 示例 2:单例模式迁移 + +**Objective-C**: +```objective-c ++ (VagrantManager*)sharedManager { + static VagrantManager *manager; + @synchronized(self) { + if(manager == nil) { + manager = [[VagrantManager alloc] init]; + } + } + return manager; +} +``` + +**Swift**: +```swift +class VagrantManager { + static let shared = VagrantManager() + + private init() { + // 初始化代码 + } +} +``` + +## 🔧 迁移步骤详解 + +### 步骤 1:启用 Swift 支持 + +1. 在 Xcode 中打开项目 +2. 选择 Target → Build Settings +3. 搜索 "Swift Language Version",设置为 Swift 5.x +4. 添加 Swift 文件到项目(Xcode 会自动创建桥接头文件) + +### 步骤 2:创建桥接头文件 + +如果 Xcode 没有自动创建,手动创建 `Vagrant Manager-Bridging-Header.h`: + +```objective-c +// +// Vagrant Manager-Bridging-Header.h +// Vagrant Manager +// + +#import +#import + +// 保留需要从 Swift 访问的 Objective-C 头文件 +#import "MenuDelegate.h" +#import "VirtualMachineServiceProvider.h" +``` + +### 步骤 3:迁移单个类 + +1. 创建新的 Swift 文件(如 `LanguageManager.swift`) +2. 将 Objective-C 代码转换为 Swift +3. 更新所有引用该类的代码 +4. 测试功能是否正常 +5. 删除旧的 .h/.m 文件 + +### 步骤 4:处理宏定义 + +Objective-C 的宏需要转换为 Swift 函数: + +```swift +// Objective-C 宏 +#define VMLocalizedString(key) [[LanguageManager sharedManager] localizedString:key] + +// Swift 函数 +func VMLocalizedString(_ key: String) -> String { + return LanguageManager.shared.localizedString(key) +} +``` + +## ⚠️ 注意事项 + +### 1. 类型转换 +- Objective-C 的 `id` → Swift 的 `Any` +- `NSString*` → `String` +- `NSArray*` → `[Type]` +- `NSDictionary*` → `[Key: Value]` + +### 2. 可选值处理 +- Objective-C 的 `nil` → Swift 的 `nil`(可选类型) +- 需要仔细处理可选值解包 + +### 3. 内存管理 +- Objective-C: MRC/ARC +- Swift: ARC(自动管理,更安全) + +### 4. 协议和委托 +- Objective-C 协议 → Swift 协议 +- 委托模式在 Swift 中更简洁 + +### 5. 通知中心 +- 语法略有不同,但功能相同 + +## 📈 工作量估算 + +| 阶段 | 工作量 | 说明 | +|------|--------|------| +| 准备阶段 | 1-2 天 | 配置和设置 | +| 工具类迁移 | 1 周 | 5-10 个简单类 | +| 数据模型迁移 | 1 周 | 5-8 个模型类 | +| 业务逻辑迁移 | 1-2 周 | 10-15 个核心类 | +| UI 层迁移 | 1-2 周 | 10-12 个 UI 类 | +| 清理优化 | 1 周 | 测试和优化 | +| **总计** | **5-7 周** | 取决于代码复杂度 | + +## 🎁 迁移后的优势 + +1. **代码质量** + - 类型安全 + - 更少的运行时错误 + - 更好的代码可读性 + +2. **开发效率** + - 更简洁的语法 + - 更好的 IDE 支持 + - 更快的编译速度(某些场景) + +3. **维护性** + - 更容易理解和维护 + - 更好的错误处理 + - 更现代的编程范式 + +4. **性能** + - Swift 在某些场景下性能更好 + - 更好的内存管理 + +## 🚀 快速开始 + +### 最小化迁移示例 + +1. **创建 Swift 文件**: + ```bash + # 在 Xcode 中:File → New → File → Swift File + # 命名为 LanguageManager.swift + ``` + +2. **迁移 LanguageManager**(最简单的类) + +3. **测试功能** + +4. **删除旧的 .h/.m 文件** + +5. **重复上述步骤** + +## 📚 学习资源 + +- [Swift 官方文档](https://swift.org/documentation/) +- [Apple 的 Swift 迁移指南](https://developer.apple.com/documentation/swift/migrating-your-objective-c-code-to-swift) +- [Swift 与 Objective-C 互操作](https://developer.apple.com/documentation/swift/imported-c-and-objective-c-apis) + +## ❓ 常见问题 + +### Q: 可以只迁移部分代码吗? +A: 可以!Swift 和 Objective-C 可以共存,可以逐步迁移。 + +### Q: 迁移会影响现有功能吗? +A: 如果按照渐进式迁移,每个阶段都充分测试,不会影响现有功能。 + +### Q: 需要重写所有代码吗? +A: 不需要。可以逐步迁移,保持功能不变。 + +### Q: Swift 版本选择? +A: 建议使用 Swift 5.x(稳定且与 macOS 11.0+ 兼容)。 + +## 🎯 建议 + +1. **先从小类开始**:建立信心和经验 +2. **充分测试**:每个迁移的类都要测试 +3. **保持功能不变**:迁移的目标是语言转换,不是重构 +4. **逐步推进**:不要急于一次性迁移所有代码 +5. **团队学习**:如果团队不熟悉 Swift,可以边迁移边学习 + +## 📝 总结 + +**结论**:这个项目完全可以改造成 Swift,建议采用渐进式迁移策略。 + +**推荐路径**: +1. 先迁移 `LanguageManager`(最简单) +2. 然后迁移工具类(`Util`, `Environment` 等) +3. 再迁移数据模型 +4. 最后迁移 UI 层 + +**时间线**:5-7 周(取决于团队规模和代码复杂度) + +**风险**:低(采用渐进式迁移) + diff --git a/Vagrant Manager.xcodeproj/project.pbxproj b/Vagrant Manager.xcodeproj/project.pbxproj index f3c30cd..8c2895b 100644 --- a/Vagrant Manager.xcodeproj/project.pbxproj +++ b/Vagrant Manager.xcodeproj/project.pbxproj @@ -117,6 +117,7 @@ B17AA1961887DA5800B4C274 /* status_icon_problem.png in Resources */ = {isa = PBXBuildFile; fileRef = B17AA1931887DA5800B4C274 /* status_icon_problem.png */; }; B17AA1991887DB8C00B4C274 /* status_icon_off.png in Resources */ = {isa = PBXBuildFile; fileRef = B17AA1971887DB8C00B4C274 /* status_icon_off.png */; }; B17AA19A1887DB8C00B4C274 /* status_icon_on.png in Resources */ = {isa = PBXBuildFile; fileRef = B17AA1981887DB8C00B4C274 /* status_icon_on.png */; }; + B18AA18C1887CA4600B4C275 /* LanguageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B18AA18B1887CA4600B4C275 /* LanguageManager.m */; }; B1FAFC791920B62D009F0F86 /* status_icon_suspended.png in Resources */ = {isa = PBXBuildFile; fileRef = B1FAFC781920B62D009F0F86 /* status_icon_suspended.png */; }; EF9CC0C06EBF48E30B22392F /* libPods-Vagrant Manager.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A3AEF745A02767367BAF2F13 /* libPods-Vagrant Manager.a */; }; /* End PBXBuildFile section */ @@ -301,6 +302,9 @@ B17AA1931887DA5800B4C274 /* status_icon_problem.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = status_icon_problem.png; sourceTree = ""; }; B17AA1971887DB8C00B4C274 /* status_icon_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = status_icon_off.png; sourceTree = ""; }; B17AA1981887DB8C00B4C274 /* status_icon_on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = status_icon_on.png; sourceTree = ""; }; + B18AA18A1887CA4600B4C275 /* LanguageManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LanguageManager.h; sourceTree = ""; }; + B18AA18B1887CA4600B4C275 /* LanguageManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LanguageManager.m; sourceTree = ""; }; + B18AA18D1887CA4600B4C276 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; B1A5DE7D192354E900AF0CA5 /* VirtualMachineServiceProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VirtualMachineServiceProvider.h; sourceTree = ""; }; B1FAFC781920B62D009F0F86 /* status_icon_suspended.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = status_icon_suspended.png; sourceTree = ""; }; B6AC85BFC2445FC5F426BB3C /* Pods-Vagrant Manager.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Vagrant Manager.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Vagrant Manager/Pods-Vagrant Manager.debug.xcconfig"; sourceTree = ""; }; @@ -636,6 +640,8 @@ B1357A7E187CEB3B00811CBC /* Vagrant Manager-Prefix.pch */, B17AA1891887A75300B4C274 /* Util.h */, B17AA18A1887A75300B4C274 /* Util.m */, + B18AA18A1887CA4600B4C275 /* LanguageManager.h */, + B18AA18B1887CA4600B4C275 /* LanguageManager.m */, 8360A05618C2B37F002EA89B /* Environments.plist */, 83F1696D1923789900C54F13 /* VersionComparison.h */, 83F1696E1923789900C54F13 /* VersionComparison.m */, @@ -827,12 +833,10 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Vagrant Manager/Pods-Vagrant Manager-frameworks.sh", "${PODS_ROOT}/Sparkle/Sparkle.framework", - "${PODS_ROOT}/Sparkle/Sparkle.framework.dSYM", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sparkle.framework", - "${DWARF_DSYM_FOLDER_PATH}/Sparkle.framework.dSYM", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -899,6 +903,7 @@ 8339DCA1187E756B0036E162 /* TaskOutputWindow.m in Sources */, B1357A7D187CEB3B00811CBC /* main.m in Sources */, B17AA18B1887A75300B4C274 /* Util.m in Sources */, + B18AA18C1887CA4600B4C275 /* LanguageManager.m in Sources */, B17AA18F1887CA4600B4C274 /* PreferencesWindow.m in Sources */, 837A30891A5D0D5500E12A40 /* ManageCustomCommandsWindow.m in Sources */, 83FD353D19826945002EEA2D /* ManageBookmarksWindow.m in Sources */, @@ -930,6 +935,7 @@ isa = PBXVariantGroup; children = ( B1357A7A187CEB3B00811CBC /* en */, + B18AA18D1887CA4600B4C276 /* zh-Hans */, ); name = InfoPlist.strings; sourceTree = ""; @@ -1008,7 +1014,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.8; + MACOSX_DEPLOYMENT_TARGET = 11.0; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; }; @@ -1051,7 +1057,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.8; + MACOSX_DEPLOYMENT_TARGET = 11.0; SDKROOT = macosx; }; name = Release; @@ -1062,7 +1068,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2.7.1; + CURRENT_PROJECT_VERSION = 2.8.8; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", @@ -1070,8 +1076,8 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Vagrant Manager/Vagrant Manager-Prefix.pch"; INFOPLIST_FILE = "Vagrant Manager/Vagrant Manager-Info.plist"; - MACOSX_DEPLOYMENT_TARGET = 10.8; - MARKETING_VERSION = 2.7.1; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 2.8.8; PRODUCT_BUNDLE_IDENTIFIER = "lanayo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; @@ -1084,7 +1090,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2.7.1; + CURRENT_PROJECT_VERSION = 2.8.8; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", @@ -1092,8 +1098,8 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "Vagrant Manager/Vagrant Manager-Prefix.pch"; INFOPLIST_FILE = "Vagrant Manager/Vagrant Manager-Info.plist"; - MACOSX_DEPLOYMENT_TARGET = 10.8; - MARKETING_VERSION = 2.7.1; + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 2.8.8; PRODUCT_BUNDLE_IDENTIFIER = "lanayo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; diff --git a/Vagrant Manager/AboutWindow.m b/Vagrant Manager/AboutWindow.m index a36c459..f7c1c1d 100644 --- a/Vagrant Manager/AboutWindow.m +++ b/Vagrant Manager/AboutWindow.m @@ -7,6 +7,7 @@ #import "AboutWindow.h" #import "Environment.h" +#import "LanguageManager.h" @interface AboutWindow () @@ -16,12 +17,25 @@ @implementation AboutWindow - (id)initWithWindow:(NSWindow *)window { self = [super initWithWindow:window]; + + // Listen for language changes + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(languageChanged:) name:@"vagrant-manager.language-changed" object:nil]; return self; } +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + - (void)windowDidLoad { [super windowDidLoad]; + [self updateContent]; +} + +- (void)updateContent { + // Update window title + self.window.title = VMLocalizedString(@"About Vagrant Manager"); BOOL isDarkMode = [[[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"] isEqualToString:@"Dark"]; @@ -31,12 +45,7 @@ - (void)windowDidLoad { styles = @""; } - NSString *str = [NSString stringWithFormat:@"%@
Copyright ©{YEAR} Lanayo Tech

Vagrant Manager {VERSION}

For more information visit:
{URL}

or check us out on GitHub:
{GITHUB_URL}
", styles]; - - NSString *dateString = [NSString stringWithCString:__DATE__ encoding:NSASCIIStringEncoding]; - NSString *yearString = [dateString substringWithRange:NSMakeRange([dateString length] - 4, 4)]; - - str = [str stringByReplacingOccurrencesOfString:@"{YEAR}" withString:yearString]; + NSString *str = [NSString stringWithFormat:@"%@
Copyright ©2025 kk

Vagrant Manager {VERSION}

%@
{URL}

%@
{GITHUB_URL}
", styles, VMLocalizedString(@"For more information visit:"), VMLocalizedString(@"or check us out on GitHub:")]; str = [str stringByReplacingOccurrencesOfString:@"{VERSION}" withString:[[[NSBundle mainBundle] infoDictionary] valueForKey:@"CFBundleShortVersionString"]]; str = [str stringByReplacingOccurrencesOfString:@"{URL}" withString:[[Environment sharedInstance] aboutURL]]; str = [str stringByReplacingOccurrencesOfString:@"{GITHUB_URL}" withString:[[Environment sharedInstance] githubURL]]; @@ -47,6 +56,13 @@ - (void)windowDidLoad { [self.webView.mainFrame loadHTMLString:str baseURL:nil]; } +- (void)languageChanged:(NSNotification*)notification { + // Update content when language changes + if([self.window isVisible]) { + [self updateContent]; + } +} + - (void)webView:(WebView*)webView decidePolicyForNavigationAction:(NSDictionary*)actionInformation request:(NSURLRequest*)request frame:(WebFrame*)frame decisionListener:(id)listener { NSString *host = [[request URL] host]; if(host) { diff --git a/Vagrant Manager/AppDelegate.m b/Vagrant Manager/AppDelegate.m index aa06036..5f1e132 100644 --- a/Vagrant Manager/AppDelegate.m +++ b/Vagrant Manager/AppDelegate.m @@ -12,6 +12,7 @@ #import "BookmarkManager.h" #import "CustomCommandManager.h" #import "CustomProviderManager.h" +#import "LanguageManager.h" @implementation AppDelegate { BOOL isRefreshingVagrantMachines; @@ -30,6 +31,9 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { //initialize data openWindows = [[NSMutableArray alloc] init]; + //initialize language manager + [LanguageManager sharedManager]; + //make sure process is running in the right state [self updateProcessType]; @@ -43,6 +47,7 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showUpdateNotificationPreferenceChanged:) name:@"vagrant-manager.show-update-notification-preference-changed" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(bookmarksUpdated:) name:@"vagrant-manager.bookmarks-updated" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(customCommandsUpdated:) name:@"vagrant-manager.custom-commands-updated" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(languageChanged:) name:@"vagrant-manager.language-changed" object:nil]; //register for wake from sleep notification [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:@selector(receivedWakeNotification:) name:NSWorkspaceDidWakeNotification object:NULL]; @@ -81,8 +86,8 @@ - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sende // ask if the user would like to halt all running machines [NSApp activateIgnoringOtherApps:YES]; - NSAlert *confirmAlert = [NSAlert alertWithMessageText:@"Would you like to stop all running machines?" defaultButton:@"Halt & Quit" alternateButton:@"Quit" otherButton:@"Suspend & Quit" informativeTextWithFormat:@""]; - [confirmAlert addButtonWithTitle:@"Cancel"]; + NSAlert *confirmAlert = [NSAlert alertWithMessageText:VMLocalizedString(@"Would you like to stop all running machines?") defaultButton:VMLocalizedString(@"Halt & Quit") alternateButton:VMLocalizedString(@"Quit") otherButton:VMLocalizedString(@"Suspend & Quit") informativeTextWithFormat:@""]; + [confirmAlert addButtonWithTitle:VMLocalizedString(@"Cancel")]; NSInteger button = [confirmAlert runModal]; @@ -143,6 +148,11 @@ - (void)customCommandsUpdated:(NSNotification*)notification { [_nativeMenu rebuildMenu]; } +- (void)languageChanged:(NSNotification*)notification { + // Rebuild menu when language changes + [_nativeMenu rebuildMenu]; +} + - (void)themeChanged:(NSNotification*)notification { [self updateRunningVmCount]; } @@ -308,7 +318,7 @@ - (void)openInstanceInFinder:(VagrantInstance *)instance { [[NSWorkspace sharedWorkspace] openURL:fileURL]; } else { [NSApp activateIgnoringOtherApps:YES]; - NSAlert *confirmAlert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"Path not found: %@", path] defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; + NSAlert *confirmAlert = [NSAlert alertWithMessageText:[NSString stringWithFormat:VMLocalizedString(@"Path not found: %@"), path] defaultButton:VMLocalizedString(@"OK") alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; [confirmAlert.window makeKeyWindow]; [confirmAlert runModal]; } @@ -322,7 +332,7 @@ - (void)openInstanceInTerminal:(VagrantInstance *)instance { [self runTerminalCommand:[NSString stringWithFormat:@"cd %@", [Util escapeShellArg:path]]]; } else { [NSApp activateIgnoringOtherApps:YES]; - NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"Path not found: %@", path] defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; + NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:VMLocalizedString(@"Path not found: %@"), path] defaultButton:VMLocalizedString(@"OK") alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; [alert.window makeKeyWindow]; [alert runModal]; } @@ -398,14 +408,14 @@ - (void)checkForVagrantUpdates:(BOOL)showAlert { if(showAlert) { if(invalidOutput) { [NSApp activateIgnoringOtherApps:YES]; - NSAlert *alert = [NSAlert alertWithMessageText:@"There was a problem checking your Vagrant version" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; + NSAlert *alert = [NSAlert alertWithMessageText:VMLocalizedString(@"There was a problem checking your Vagrant version") defaultButton:VMLocalizedString(@"OK") alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; [alert.window makeKeyWindow]; [alert runModal]; } else if(newVersionAvailable) { [NSApp activateIgnoringOtherApps:YES]; - NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"There is a newer version of Vagrant available.\n\nCurrent version: %@\nLatest version: %@", currentVersion, latestVersion] defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; + NSAlert *alert = [NSAlert alertWithMessageText:[NSString stringWithFormat:VMLocalizedString(@"There is a newer version of Vagrant available.\n\nCurrent version: %@\nLatest version: %@"), currentVersion, latestVersion] defaultButton:VMLocalizedString(@"OK") alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; [alert.window makeKeyWindow]; - [alert addButtonWithTitle:@"Visit Vagrant Website"]; + [alert addButtonWithTitle:VMLocalizedString(@"Visit Vagrant Website")]; long response = [alert runModal]; @@ -414,7 +424,7 @@ - (void)checkForVagrantUpdates:(BOOL)showAlert { } } else { [NSApp activateIgnoringOtherApps:YES]; - NSAlert *alert = [NSAlert alertWithMessageText:@"You are running the latest version of Vagrant" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; + NSAlert *alert = [NSAlert alertWithMessageText:VMLocalizedString(@"You are running the latest version of Vagrant") defaultButton:VMLocalizedString(@"OK") alternateButton:nil otherButton:nil informativeTextWithFormat:@""]; [alert.window makeKeyWindow]; [alert runModal]; } @@ -650,7 +660,7 @@ - (void)runTerminalCommand:(NSString*)command { [as executeAndReturnError:&errors]; if (errors) { - [[NSAlert alertWithMessageText:@"There was an error performing the command" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@"%@", errors[NSAppleScriptErrorMessage]] runModal]; + [[NSAlert alertWithMessageText:VMLocalizedString(@"There was an error performing the command") defaultButton:VMLocalizedString(@"OK") alternateButton:nil otherButton:nil informativeTextWithFormat:@"%@", errors[NSAppleScriptErrorMessage]] runModal]; } } diff --git a/Vagrant Manager/LanguageManager.h b/Vagrant Manager/LanguageManager.h new file mode 100644 index 0000000..06f2965 --- /dev/null +++ b/Vagrant Manager/LanguageManager.h @@ -0,0 +1,23 @@ +// +// LanguageManager.h +// Vagrant Manager +// +// Copyright (c) 2024. All rights reserved. +// + +#import + +// Macro to use LanguageManager for localization +#define VMLocalizedString(key) [[LanguageManager sharedManager] localizedString:key] + +@interface LanguageManager : NSObject + ++ (LanguageManager*)sharedManager; + +- (NSArray*)getAvailableLanguages; +- (NSString*)getCurrentLanguage; +- (void)setLanguage:(NSString*)languageCode; +- (NSString*)localizedString:(NSString*)key; + +@end + diff --git a/Vagrant Manager/LanguageManager.m b/Vagrant Manager/LanguageManager.m new file mode 100644 index 0000000..d6b26ae --- /dev/null +++ b/Vagrant Manager/LanguageManager.m @@ -0,0 +1,86 @@ +// +// LanguageManager.m +// Vagrant Manager +// +// Copyright (c) 2024. All rights reserved. +// + +#import "LanguageManager.h" + +@implementation LanguageManager { + NSBundle *_languageBundle; +} + ++ (LanguageManager*)sharedManager { + static LanguageManager *manager; + @synchronized(self) { + if(manager == nil) { + manager = [[LanguageManager alloc] init]; + } + } + return manager; +} + +- (id)init { + self = [super init]; + if(self) { + [self loadLanguageBundle]; + } + return self; +} + +- (void)loadLanguageBundle { + NSString *languageCode = [self getCurrentLanguage]; + NSString *path = [[NSBundle mainBundle] pathForResource:languageCode ofType:@"lproj"]; + if(path) { + _languageBundle = [NSBundle bundleWithPath:path]; + } else { + _languageBundle = [NSBundle mainBundle]; + } +} + +- (NSArray*)getAvailableLanguages { + return @[ + @{@"code": @"en", @"name": @"English"}, + @{@"code": @"zh-Hans", @"name": @"简体中文"} + ]; +} + +- (NSString*)getCurrentLanguage { + NSString *savedLanguage = [[NSUserDefaults standardUserDefaults] stringForKey:@"appLanguage"]; + if(savedLanguage && [savedLanguage length] > 0) { + return savedLanguage; + } + + // 如果没有保存的语言,使用系统语言 + NSArray *preferredLanguages = [NSLocale preferredLanguages]; + if(preferredLanguages && [preferredLanguages count] > 0) { + NSString *systemLanguage = [preferredLanguages objectAtIndex:0]; + if([systemLanguage hasPrefix:@"zh"]) { + return @"zh-Hans"; + } + } + + return @"en"; +} + +- (void)setLanguage:(NSString*)languageCode { + [[NSUserDefaults standardUserDefaults] setObject:languageCode forKey:@"appLanguage"]; + [[NSUserDefaults standardUserDefaults] synchronize]; + + [self loadLanguageBundle]; + + // 发送通知,通知其他组件语言已更改 + [[NSNotificationCenter defaultCenter] postNotificationName:@"vagrant-manager.language-changed" object:nil]; +} + +- (NSString*)localizedString:(NSString*)key { + if(_languageBundle) { + NSString *localized = [_languageBundle localizedStringForKey:key value:key table:nil]; + return localized; + } + return NSLocalizedString(key, nil); +} + +@end + diff --git a/Vagrant Manager/ManageBookmarksWindow.m b/Vagrant Manager/ManageBookmarksWindow.m index 9a59fbc..bd95979 100644 --- a/Vagrant Manager/ManageBookmarksWindow.m +++ b/Vagrant Manager/ManageBookmarksWindow.m @@ -7,6 +7,7 @@ #import "ManageBookmarksWindow.h" #import "BookmarkManager.h" +#import "LanguageManager.h" @interface ManageBookmarksWindow () @@ -25,6 +26,9 @@ - (id)initWithWindow:(NSWindow *)window { - (void)windowDidLoad { [super windowDidLoad]; + // Set localized window title + self.window.title = VMLocalizedString(@"Manage Bookmarks"); + bookmarks = [[NSMutableArray alloc] initWithArray:[[BookmarkManager sharedManager] getBookmarks] copyItems:YES]; _scanCancelled = NO; diff --git a/Vagrant Manager/ManageCustomCommandsWindow.m b/Vagrant Manager/ManageCustomCommandsWindow.m index 798e532..3778c1c 100644 --- a/Vagrant Manager/ManageCustomCommandsWindow.m +++ b/Vagrant Manager/ManageCustomCommandsWindow.m @@ -7,12 +7,16 @@ #import "ManageCustomCommandsWindow.h" #import "CustomCommandManager.h" +#import "LanguageManager.h" @implementation ManageCustomCommandsWindow - (void)windowDidLoad { [super windowDidLoad]; + // Set localized window title + self.window.title = VMLocalizedString(@"Manage Custom Commands"); + _commands = [[CustomCommandManager sharedManager] getCustomCommands]; [self.commandsTableView registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeString, nil]]; @@ -21,7 +25,7 @@ - (void)windowDidLoad { - (void)addCommandButtonClicked:(id)sender { CustomCommand *customCommand = [[CustomCommand alloc] init]; - customCommand.displayName = @"New Command"; + customCommand.displayName = VMLocalizedString(@"New Command"); [_commands addObject:customCommand]; [self saveCustomCommands]; diff --git a/Vagrant Manager/ManageCustomProvidersWindow.m b/Vagrant Manager/ManageCustomProvidersWindow.m index a4f8dbe..7db7602 100644 --- a/Vagrant Manager/ManageCustomProvidersWindow.m +++ b/Vagrant Manager/ManageCustomProvidersWindow.m @@ -7,12 +7,16 @@ #import "ManageCustomProvidersWindow.h" #import "CustomProviderManager.h" +#import "LanguageManager.h" @implementation ManageCustomProvidersWindow - (void)windowDidLoad { [super windowDidLoad]; + // Set localized window title + self.window.title = VMLocalizedString(@"Manage Custom Providers"); + _providers = [[CustomProviderManager sharedManager] getCustomProviders]; [self.providersTableView registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeString, nil]]; @@ -21,7 +25,7 @@ - (void)windowDidLoad { - (void)addProviderButtonClicked:(id)sender { CustomProvider *customProvider = [[CustomProvider alloc] init]; - customProvider.name = @"New Provider"; + customProvider.name = VMLocalizedString(@"New Provider"); [_providers addObject:customProvider]; [self saveCustomProviders]; diff --git a/Vagrant Manager/NativeMenu.m b/Vagrant Manager/NativeMenu.m index b8416f3..9e5b678 100644 --- a/Vagrant Manager/NativeMenu.m +++ b/Vagrant Manager/NativeMenu.m @@ -8,6 +8,7 @@ #import "NativeMenu.h" #import "BookmarkManager.h" #import "VagrantInstanceCache.h" +#import "LanguageManager.h" @implementation NativeMenu { NSStatusItem *_statusItem; @@ -24,6 +25,23 @@ @implementation NativeMenu { NSMenuItem *_checkForUpdatesMenuItem; NSMenuItem *_checkForVagrantUpdatesMenuItem; + NSMenuItem *_allMachinesMenuItem; + NSMenu *_allMachinesMenu; + NSMenuItem *_allUpMenuItem; + NSMenuItem *_allReloadMenuItem; + NSMenuItem *_allSuspendMenuItem; + NSMenuItem *_allHaltMenuItem; + NSMenuItem *_allProvisionMenuItem; + NSMenuItem *_allDestroyMenuItem; + NSMenuItem *_manageBookmarksMenuItem; + NSMenuItem *_manageCustomCommandsMenuItem; + NSMenuItem *_manageCustomProvidersMenuItem; + NSMenuItem *_extrasMenuItem; + NSMenuItem *_editHostsMenuItem; + NSMenuItem *_preferencesMenuItem; + NSMenuItem *_aboutMenuItem; + NSMenuItem *_quitMenuItem; + NSMutableArray *_runningTasks; int _runningVmCount; } @@ -47,6 +65,7 @@ - (id)init { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskCompleted:) name:@"vagrant-manager.task-completed" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startedShutdown:) name:@"vagrant-manager.started-shutdown" object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(customProvidersUpdated:) name:@"vagrant-manager.custom-providers-updated" object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(languageChanged:) name:@"vagrant-manager.language-changed" object:nil]; _statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; @@ -59,7 +78,7 @@ - (id)init { _statusItem.highlightMode = YES; _statusItem.menu = _menu; - _refreshMenuItem = [[NSMenuItem alloc] initWithTitle:@"Refresh" action:@selector(refreshMenuItemClicked:) keyEquivalent:@""]; + _refreshMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Refresh") action:@selector(refreshMenuItemClicked:) keyEquivalent:@""]; _refreshMenuItem.target = self; [_menu addItem:_refreshMenuItem]; @@ -70,94 +89,94 @@ - (id)init { _bottomMachineSeparator = [NSMenuItem separatorItem]; [_menu addItem:_bottomMachineSeparator]; - NSMenu *allMachinesMenu = [[NSMenu alloc] init]; - [allMachinesMenu setAutoenablesItems:NO]; - - NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:@"Up" action:@selector(allUpMenuItemClicked:) keyEquivalent:@""]; - menuItem.target = self; - menuItem.image = [NSImage imageNamed:@"up"]; - [menuItem.image setTemplate:YES]; - [allMachinesMenu addItem:menuItem]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"Reload" action:@selector(allReloadMenuItemClicked:) keyEquivalent:@""]; - menuItem.target = self; - menuItem.image = [NSImage imageNamed:@"reload"]; - [menuItem.image setTemplate:YES]; - [allMachinesMenu addItem:menuItem]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"Suspend" action:@selector(allSuspendMenuItemClicked:) keyEquivalent:@""]; - menuItem.target = self; - menuItem.image = [NSImage imageNamed:@"suspend"]; - [menuItem.image setTemplate:YES]; - [allMachinesMenu addItem:menuItem]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"Halt" action:@selector(allHaltMenuItemClicked:) keyEquivalent:@""]; - menuItem.target = self; - menuItem.image = [NSImage imageNamed:@"halt"]; - [menuItem.image setTemplate:YES]; - [allMachinesMenu addItem:menuItem]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"Provision" action:@selector(allProvisionMenuItemClicked:) keyEquivalent:@""]; - menuItem.target = self; - menuItem.image = [NSImage imageNamed:@"provision"]; - [menuItem.image setTemplate:YES]; - [allMachinesMenu addItem:menuItem]; - - menuItem = [[NSMenuItem alloc] initWithTitle:@"Destroy" action:@selector(allDestroyMenuItemClicked:) keyEquivalent:@""]; - menuItem.target = self; - menuItem.image = [NSImage imageNamed:@"destroy"]; - [menuItem.image setTemplate:YES]; - [allMachinesMenu addItem:menuItem]; - - NSMenuItem *allMachinesMenuItem = [[NSMenuItem alloc] initWithTitle:@"All Machines" action:nil keyEquivalent:@""]; - [allMachinesMenuItem setSubmenu:allMachinesMenu]; - - [_menu addItem:allMachinesMenuItem]; - - NSMenuItem *manageBookmarksMenuItem = [[NSMenuItem alloc] initWithTitle:@"Manage Bookmarks" action:@selector(manageBookmarksMenuItemClicked:) keyEquivalent:@""]; - manageBookmarksMenuItem.target = self; - [_menu addItem:manageBookmarksMenuItem]; - - NSMenuItem *manageCustomCommandsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Manage Custom Commands" action:@selector(manageCustomCommandsMenuItemClicked:) keyEquivalent:@""]; - manageCustomCommandsMenuItem.target = self; - [_menu addItem:manageCustomCommandsMenuItem]; - - NSMenuItem *manageCustomProvidersMenuItem = [[NSMenuItem alloc] initWithTitle:@"Manage Custom Providers" action:@selector(manageCustomProvidersMenuItemClicked:) keyEquivalent:@""]; - manageCustomProvidersMenuItem.target = self; - [_menu addItem:manageCustomProvidersMenuItem]; + _allMachinesMenu = [[NSMenu alloc] init]; + [_allMachinesMenu setAutoenablesItems:NO]; + + _allUpMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Up") action:@selector(allUpMenuItemClicked:) keyEquivalent:@""]; + _allUpMenuItem.target = self; + _allUpMenuItem.image = [NSImage imageNamed:@"up"]; + [_allUpMenuItem.image setTemplate:YES]; + [_allMachinesMenu addItem:_allUpMenuItem]; + + _allReloadMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Reload") action:@selector(allReloadMenuItemClicked:) keyEquivalent:@""]; + _allReloadMenuItem.target = self; + _allReloadMenuItem.image = [NSImage imageNamed:@"reload"]; + [_allReloadMenuItem.image setTemplate:YES]; + [_allMachinesMenu addItem:_allReloadMenuItem]; + + _allSuspendMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Suspend") action:@selector(allSuspendMenuItemClicked:) keyEquivalent:@""]; + _allSuspendMenuItem.target = self; + _allSuspendMenuItem.image = [NSImage imageNamed:@"suspend"]; + [_allSuspendMenuItem.image setTemplate:YES]; + [_allMachinesMenu addItem:_allSuspendMenuItem]; + + _allHaltMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Halt") action:@selector(allHaltMenuItemClicked:) keyEquivalent:@""]; + _allHaltMenuItem.target = self; + _allHaltMenuItem.image = [NSImage imageNamed:@"halt"]; + [_allHaltMenuItem.image setTemplate:YES]; + [_allMachinesMenu addItem:_allHaltMenuItem]; + + _allProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Provision") action:@selector(allProvisionMenuItemClicked:) keyEquivalent:@""]; + _allProvisionMenuItem.target = self; + _allProvisionMenuItem.image = [NSImage imageNamed:@"provision"]; + [_allProvisionMenuItem.image setTemplate:YES]; + [_allMachinesMenu addItem:_allProvisionMenuItem]; + + _allDestroyMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Destroy") action:@selector(allDestroyMenuItemClicked:) keyEquivalent:@""]; + _allDestroyMenuItem.target = self; + _allDestroyMenuItem.image = [NSImage imageNamed:@"destroy"]; + [_allDestroyMenuItem.image setTemplate:YES]; + [_allMachinesMenu addItem:_allDestroyMenuItem]; + + _allMachinesMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"All Machines") action:nil keyEquivalent:@""]; + [_allMachinesMenuItem setSubmenu:_allMachinesMenu]; + + [_menu addItem:_allMachinesMenuItem]; + + _manageBookmarksMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Manage Bookmarks") action:@selector(manageBookmarksMenuItemClicked:) keyEquivalent:@""]; + _manageBookmarksMenuItem.target = self; + [_menu addItem:_manageBookmarksMenuItem]; + + _manageCustomCommandsMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Manage Custom Commands") action:@selector(manageCustomCommandsMenuItemClicked:) keyEquivalent:@""]; + _manageCustomCommandsMenuItem.target = self; + [_menu addItem:_manageCustomCommandsMenuItem]; + + _manageCustomProvidersMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Manage Custom Providers") action:@selector(manageCustomProvidersMenuItemClicked:) keyEquivalent:@""]; + _manageCustomProvidersMenuItem.target = self; + [_menu addItem:_manageCustomProvidersMenuItem]; [_menu addItem:[NSMenuItem separatorItem]]; - NSMenuItem *extrasMenuItem = [[NSMenuItem alloc] initWithTitle:@"Extras" action:nil keyEquivalent:@""]; - [_menu addItem:extrasMenuItem]; + _extrasMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Extras") action:nil keyEquivalent:@""]; + [_menu addItem:_extrasMenuItem]; NSMenu *extrasMenu = [[NSMenu alloc] init]; - NSMenuItem *editHostsMenuItem = [[NSMenuItem alloc] initWithTitle:@"Edit hosts file" action:@selector(editHostsMenuItemClicked:) keyEquivalent:@""]; - editHostsMenuItem.target = self; - [extrasMenu addItem:editHostsMenuItem]; + _editHostsMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Edit hosts file") action:@selector(editHostsMenuItemClicked:) keyEquivalent:@""]; + _editHostsMenuItem.target = self; + [extrasMenu addItem:_editHostsMenuItem]; - [extrasMenuItem setSubmenu:extrasMenu]; + [_extrasMenuItem setSubmenu:extrasMenu]; - NSMenuItem *preferencesMenuItem = [[NSMenuItem alloc] initWithTitle:@"Preferences" action:@selector(preferencesMenuItemClicked:) keyEquivalent:@""]; - preferencesMenuItem.target = self; - [_menu addItem:preferencesMenuItem]; + _preferencesMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Preferences...") action:@selector(preferencesMenuItemClicked:) keyEquivalent:@""]; + _preferencesMenuItem.target = self; + [_menu addItem:_preferencesMenuItem]; - NSMenuItem *aboutMenuItem = [[NSMenuItem alloc] initWithTitle:@"About" action:@selector(aboutMenuItemClicked:) keyEquivalent:@""]; - aboutMenuItem.target = self; - [_menu addItem:aboutMenuItem]; + _aboutMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"About Vagrant Manager") action:@selector(aboutMenuItemClicked:) keyEquivalent:@""]; + _aboutMenuItem.target = self; + [_menu addItem:_aboutMenuItem]; - _checkForUpdatesMenuItem = [[NSMenuItem alloc] initWithTitle:@"Check For Updates" action:@selector(checkForUpdatesMenuItemClicked:) keyEquivalent:@""]; + _checkForUpdatesMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Check For Updates") action:@selector(checkForUpdatesMenuItemClicked:) keyEquivalent:@""]; _checkForUpdatesMenuItem.target = self; [_menu addItem:_checkForUpdatesMenuItem]; - _checkForVagrantUpdatesMenuItem = [[NSMenuItem alloc] initWithTitle:@"Check For Vagrant Updates" action:@selector(checkForVagrantUpdatesMenuItemClicked:) keyEquivalent:@""]; + _checkForVagrantUpdatesMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Check For Vagrant Updates") action:@selector(checkForVagrantUpdatesMenuItemClicked:) keyEquivalent:@""]; _checkForVagrantUpdatesMenuItem.target = self; [_menu addItem:_checkForVagrantUpdatesMenuItem]; - NSMenuItem *quitMenuItem = [[NSMenuItem alloc] initWithTitle:@"Quit" action:@selector(quitMenuItemClicked:) keyEquivalent:@""]; - quitMenuItem.target = self; - [_menu addItem:quitMenuItem]; + _quitMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Quit Vagrant Manager") action:@selector(quitMenuItemClicked:) keyEquivalent:@""]; + _quitMenuItem.target = self; + [_menu addItem:_quitMenuItem]; return self; } @@ -176,6 +195,35 @@ - (void)customProvidersUpdated:(NSNotification*)notification { [self rebuildMenu]; } +- (void)languageChanged:(NSNotification*)notification { + // Update all menu item titles when language changes + _refreshMenuItem.title = VMLocalizedString(@"Refresh"); + _checkForUpdatesMenuItem.title = VMLocalizedString(@"Check For Updates"); + _checkForVagrantUpdatesMenuItem.title = VMLocalizedString(@"Check For Vagrant Updates"); + + // Update "All Machines" submenu items + _allMachinesMenuItem.title = VMLocalizedString(@"All Machines"); + _allUpMenuItem.title = VMLocalizedString(@"Up"); + _allReloadMenuItem.title = VMLocalizedString(@"Reload"); + _allSuspendMenuItem.title = VMLocalizedString(@"Suspend"); + _allHaltMenuItem.title = VMLocalizedString(@"Halt"); + _allProvisionMenuItem.title = VMLocalizedString(@"Provision"); + _allDestroyMenuItem.title = VMLocalizedString(@"Destroy"); + + // Update other menu items + _manageBookmarksMenuItem.title = VMLocalizedString(@"Manage Bookmarks"); + _manageCustomCommandsMenuItem.title = VMLocalizedString(@"Manage Custom Commands"); + _manageCustomProvidersMenuItem.title = VMLocalizedString(@"Manage Custom Providers"); + _extrasMenuItem.title = VMLocalizedString(@"Extras"); + _editHostsMenuItem.title = VMLocalizedString(@"Edit hosts file"); + _preferencesMenuItem.title = VMLocalizedString(@"Preferences..."); + _aboutMenuItem.title = VMLocalizedString(@"About Vagrant Manager"); + _quitMenuItem.title = VMLocalizedString(@"Quit Vagrant Manager"); + + // Rebuild menu to update all instance menu items + [self rebuildMenu]; +} + - (void)notificationPreferenceChanged: (NSNotification*)notification { } @@ -313,7 +361,7 @@ - (void)setVagrantUpdatesAvailable:(BOOL)updatesAvailable { - (void)setIsRefreshing:(BOOL)isRefreshing { [_refreshMenuItem setEnabled:!isRefreshing]; - _refreshMenuItem.title = isRefreshing ? @"Refreshing..." : @"Refresh"; + _refreshMenuItem.title = isRefreshing ? VMLocalizedString(@"Refreshing...") : VMLocalizedString(@"Refresh"); if(isRefreshing && ![[NSUserDefaults standardUserDefaults] boolForKey:@"dontAnimateStatusIcon"]) { _refreshIconFrame = 1; @@ -364,7 +412,7 @@ - (void)nativeMenuItemSuspendAllMachines:(NativeMenuItem*)menuItem { - (void)nativeMenuItemDestroyAllMachines:(NativeMenuItem *)menuItem { [NSApp activateIgnoringOtherApps:YES]; - NSAlert *confirmAlert = [NSAlert alertWithMessageText:[NSString stringWithFormat:@"Are you sure you want to destroy %@?", menuItem.instance.machines.count > 1 ? @" all machines in the group" : @"this machine"] defaultButton:@"Confirm" alternateButton:@"Cancel" otherButton:nil informativeTextWithFormat:@""]; + NSAlert *confirmAlert = [NSAlert alertWithMessageText:[NSString stringWithFormat:VMLocalizedString(@"Are you sure you want to destroy %@?"), menuItem.instance.machines.count > 1 ? VMLocalizedString(@" all machines in the group") : VMLocalizedString(@"this machine")] defaultButton:VMLocalizedString(@"Confirm") alternateButton:VMLocalizedString(@"Cancel") otherButton:nil informativeTextWithFormat:@""]; [confirmAlert.window makeKeyWindow]; NSInteger button = [confirmAlert runModal]; @@ -446,7 +494,7 @@ - (void)nativeMenuItemSuspendMachine:(VagrantMachine *)machine { - (void)nativeMenuItemDestroyMachine:(VagrantMachine *)machine { [NSApp activateIgnoringOtherApps:YES]; - NSAlert *confirmAlert = [NSAlert alertWithMessageText:@"Are you sure you want to destroy this machine?" defaultButton:@"Confirm" alternateButton:@"Cancel" otherButton:nil informativeTextWithFormat:@""]; + NSAlert *confirmAlert = [NSAlert alertWithMessageText:VMLocalizedString(@"Are you sure you want to destroy this machine?") defaultButton:VMLocalizedString(@"Confirm") alternateButton:VMLocalizedString(@"Cancel") otherButton:nil informativeTextWithFormat:@""]; [confirmAlert.window makeKeyWindow]; NSInteger button = [confirmAlert runModal]; @@ -607,7 +655,7 @@ - (IBAction)allProvisionMenuItemClicked:(NSMenuItem*)sender { - (IBAction)allDestroyMenuItemClicked:(NSMenuItem*)sender { [NSApp activateIgnoringOtherApps:YES]; - NSAlert *confirmAlert = [NSAlert alertWithMessageText:@"Are you sure you want to destroy all machines?" defaultButton:@"Confirm" alternateButton:@"Cancel" otherButton:nil informativeTextWithFormat:@""]; + NSAlert *confirmAlert = [NSAlert alertWithMessageText:VMLocalizedString(@"Are you sure you want to destroy all machines?") defaultButton:VMLocalizedString(@"Confirm") alternateButton:VMLocalizedString(@"Cancel") otherButton:nil informativeTextWithFormat:@""]; [confirmAlert.window makeKeyWindow]; NSInteger button = [confirmAlert runModal]; @@ -655,7 +703,7 @@ - (void)updateStatusItem { } if ([[NSUserDefaults standardUserDefaults] boolForKey:@"hideTaskWindows"] && [_runningTasks count]) { - [parts addObject:[NSString stringWithFormat:@"(%lu running task%@)", _runningTasks.count, _runningTasks.count == 1 ? @"" : @"s"]]; + [parts addObject:[NSString stringWithFormat:VMLocalizedString(@"(%lu running task%@)"), _runningTasks.count, _runningTasks.count == 1 ? @"" : @"s"]]; } if (_runningVmCount) { diff --git a/Vagrant Manager/NativeMenuItem.m b/Vagrant Manager/NativeMenuItem.m index ea9d47a..2e3268c 100644 --- a/Vagrant Manager/NativeMenuItem.m +++ b/Vagrant Manager/NativeMenuItem.m @@ -8,6 +8,7 @@ #import "NativeMenuItem.h" #import "BookmarkManager.h" #import "CustomCommandManager.h" +#import "LanguageManager.h" @implementation NativeMenuItem { NSMenu *_submenu; @@ -79,27 +80,27 @@ - (void)refresh { } if(!_instanceUpMenuItem) { - _instanceUpMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Up All" : @"Up" action:@selector(upAllMachines:) keyEquivalent:@""]; + _instanceUpMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Up All") : VMLocalizedString(@"Up") action:@selector(upAllMachines:) keyEquivalent:@""]; _instanceUpMenuItem.target = self; _instanceUpMenuItem.image = [NSImage imageNamed:@"up"]; [_instanceUpMenuItem.image setTemplate:YES]; [_submenu addItem:_instanceUpMenuItem]; } else { - _instanceUpMenuItem.title = self.instance.machines.count > 1 ? @"Up All" : @"Up"; + _instanceUpMenuItem.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Up All") : VMLocalizedString(@"Up"); } if(!_instanceUpProvisionMenuItem) { - _instanceUpProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Up All (with provision)" : @"Up (with provision)" action:@selector(upProvisionAllMachines:) keyEquivalent:@""]; + _instanceUpProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Up All (with provision)") : VMLocalizedString(@"Up (with provision)") action:@selector(upProvisionAllMachines:) keyEquivalent:@""]; _instanceUpProvisionMenuItem.target = self; _instanceUpProvisionMenuItem.image = [NSImage imageNamed:@"up"]; [_instanceUpProvisionMenuItem.image setTemplate:YES]; [_submenu addItem:_instanceUpProvisionMenuItem]; } else { - _instanceUpProvisionMenuItem.title = self.instance.machines.count > 1 ? @"Up All (with provision)" : @"Up (with provision)"; + _instanceUpProvisionMenuItem.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Up All (with provision)") : VMLocalizedString(@"Up (with provision)"); } if(!_sshMenuItem) { - _sshMenuItem = [[NSMenuItem alloc] initWithTitle:@"SSH" action:@selector(sshInstance:) keyEquivalent:@""]; + _sshMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"SSH") action:@selector(sshInstance:) keyEquivalent:@""]; _sshMenuItem.target = self; _sshMenuItem.image = [NSImage imageNamed:@"ssh"]; [_sshMenuItem.image setTemplate:YES]; @@ -107,7 +108,7 @@ - (void)refresh { } if(!_rdpMenuItem) { - _rdpMenuItem = [[NSMenuItem alloc] initWithTitle:@"RDP" action:@selector(rdpInstance:) keyEquivalent:@""]; + _rdpMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"RDP") action:@selector(rdpInstance:) keyEquivalent:@""]; _rdpMenuItem.target = self; _rdpMenuItem.image = [NSImage imageNamed:@"rdp"]; [_rdpMenuItem.image setTemplate:YES]; @@ -115,45 +116,45 @@ - (void)refresh { } if(!_instanceReloadMenuItem) { - _instanceReloadMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Reload All" : @"Reload" action:@selector(reloadAllMachines:) keyEquivalent:@""]; + _instanceReloadMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Reload All") : VMLocalizedString(@"Reload") action:@selector(reloadAllMachines:) keyEquivalent:@""]; _instanceReloadMenuItem.target = self; _instanceReloadMenuItem.image = [NSImage imageNamed:@"reload"]; [_instanceReloadMenuItem.image setTemplate:YES]; [_submenu addItem:_instanceReloadMenuItem]; } else { - _instanceReloadMenuItem.title = self.instance.machines.count > 1 ? @"Reload All" : @"Reload"; + _instanceReloadMenuItem.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Reload All") : VMLocalizedString(@"Reload"); } if(!_instanceSuspendMenuItem) { - _instanceSuspendMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Suspend All" : @"Suspend" action:@selector(suspendAllMachines:) keyEquivalent:@""]; + _instanceSuspendMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Suspend All") : VMLocalizedString(@"Suspend") action:@selector(suspendAllMachines:) keyEquivalent:@""]; _instanceSuspendMenuItem.target = self; _instanceSuspendMenuItem.image = [NSImage imageNamed:@"suspend"]; [_instanceSuspendMenuItem.image setTemplate:YES]; [_submenu addItem:_instanceSuspendMenuItem]; } else { - _instanceSuspendMenuItem.title = self.instance.machines.count > 1 ? @"Suspend All" : @"Suspend"; + _instanceSuspendMenuItem.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Suspend All") : VMLocalizedString(@"Suspend"); } if(!_instanceHaltMenuItem) { - _instanceHaltMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Halt All" : @"Halt" action:@selector(haltAllMachines:) keyEquivalent:@""]; + _instanceHaltMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Halt All") : VMLocalizedString(@"Halt") action:@selector(haltAllMachines:) keyEquivalent:@""]; _instanceHaltMenuItem.target = self; _instanceHaltMenuItem.image = [NSImage imageNamed:@"halt"]; [_instanceHaltMenuItem.image setTemplate:YES]; [_submenu addItem:_instanceHaltMenuItem]; } else { - _instanceHaltMenuItem.title = self.instance.machines.count > 1 ? @"Halt All" : @"Halt"; + _instanceHaltMenuItem.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Halt All") : VMLocalizedString(@"Halt"); } BOOL optionKeyDestroy = [[NSUserDefaults standardUserDefaults] boolForKey:@"optionKeyDestroy"]; if(!_instanceDestroyMenuItemPlaceholder) { - _instanceDestroyMenuItemPlaceholder = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Destroy All" : @"Destroy" action:nil keyEquivalent:@""]; + _instanceDestroyMenuItemPlaceholder = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Destroy All") : VMLocalizedString(@"Destroy") action:nil keyEquivalent:@""]; _instanceDestroyMenuItemPlaceholder.image = [NSImage imageNamed:@"destroy"]; [_instanceDestroyMenuItemPlaceholder.image setTemplate:YES]; _instanceDestroyMenuItemPlaceholder.enabled = NO; [_submenu addItem:_instanceDestroyMenuItemPlaceholder]; } else { - _instanceDestroyMenuItemPlaceholder.title = self.instance.machines.count > 1 ? @"Destroy All" : @"Destroy"; + _instanceDestroyMenuItemPlaceholder.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Destroy All") : VMLocalizedString(@"Destroy"); } if(!optionKeyDestroy) { @@ -161,13 +162,13 @@ - (void)refresh { } if(!_instanceDestroyMenuItem) { - _instanceDestroyMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Destroy All" : @"Destroy" action:@selector(destroyAllMachines:) keyEquivalent:@""]; + _instanceDestroyMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Destroy All") : VMLocalizedString(@"Destroy") action:@selector(destroyAllMachines:) keyEquivalent:@""]; _instanceDestroyMenuItem.target = self; _instanceDestroyMenuItem.image = [NSImage imageNamed:@"destroy"]; [_instanceDestroyMenuItem.image setTemplate:YES]; [_submenu addItem:_instanceDestroyMenuItem]; } else { - _instanceDestroyMenuItem.title = self.instance.machines.count > 1 ? @"Destroy All" : @"Destroy"; + _instanceDestroyMenuItem.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Destroy All") : VMLocalizedString(@"Destroy"); } if(optionKeyDestroy) { @@ -178,24 +179,24 @@ - (void)refresh { } if(!_instanceProvisionMenuItem) { - _instanceProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Provision All" : @"Provision" action:@selector(provisionAllMachines:) keyEquivalent:@""]; + _instanceProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Provision All") : VMLocalizedString(@"Provision") action:@selector(provisionAllMachines:) keyEquivalent:@""]; _instanceProvisionMenuItem.target = self; _instanceProvisionMenuItem.image = [NSImage imageNamed:@"provision"]; [_instanceProvisionMenuItem.image setTemplate:YES]; [_submenu addItem:_instanceProvisionMenuItem]; } else { - _instanceProvisionMenuItem.title = self.instance.machines.count > 1 ? @"Provision All" : @"Provision"; + _instanceProvisionMenuItem.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Provision All") : VMLocalizedString(@"Provision"); } if(!_instanceCustomCommandMenuItem) { - _instanceCustomCommandMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? @"Custom Command All" : @"Custom Command" action:nil keyEquivalent:@""]; + _instanceCustomCommandMenuItem = [[NSMenuItem alloc] initWithTitle:self.instance.machines.count > 1 ? VMLocalizedString(@"Custom Command All") : VMLocalizedString(@"Custom Command") action:nil keyEquivalent:@""]; _instanceCustomCommandMenuItem.target = self; [_submenu addItem:_instanceCustomCommandMenuItem]; _instanceCustomCommandMenuItem.submenu = [[NSMenu alloc] init]; [_instanceCustomCommandMenuItem.submenu setAutoenablesItems:NO]; } else { - _instanceCustomCommandMenuItem.title = self.instance.machines.count > 1 ? @"Custom Command All" : @"Custom Command"; + _instanceCustomCommandMenuItem.title = self.instance.machines.count > 1 ? VMLocalizedString(@"Custom Command All") : VMLocalizedString(@"Custom Command"); } [_instanceCustomCommandMenuItem.submenu removeAllItems]; @@ -220,25 +221,26 @@ - (void)refresh { } if (!_editVagrantfileMenuItem) { - _editVagrantfileMenuItem = [[NSMenuItem alloc] initWithTitle:@"Edit Vagrantfile" action:@selector(editVagrantfileMenuItemClicked:) keyEquivalent:@""]; + _editVagrantfileMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Edit Vagrantfile") action:@selector(editVagrantfileMenuItemClicked:) keyEquivalent:@""]; _editVagrantfileMenuItem.target = self; [_submenu addItem:_editVagrantfileMenuItem]; } if (!_openInFinderMenuItem) { - _openInFinderMenuItem = [[NSMenuItem alloc] initWithTitle:@"Open in Finder" action:@selector(finderMenuItemClicked:) keyEquivalent:@""]; + _openInFinderMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Open in Finder") action:@selector(finderMenuItemClicked:) keyEquivalent:@""]; _openInFinderMenuItem.target = self; [_submenu addItem:_openInFinderMenuItem]; } if (!_openInTerminalMenuItem) { - _openInTerminalMenuItem = [[NSMenuItem alloc] initWithTitle:@"Open in Terminal" action:@selector(terminalMenuItemClicked:) keyEquivalent:@""]; + _openInTerminalMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Open in Terminal") action:@selector(terminalMenuItemClicked:) keyEquivalent:@""]; _openInTerminalMenuItem.target = self; [_submenu addItem:_openInTerminalMenuItem]; } if (!_chooseProviderMenuItem) { - _chooseProviderMenuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"Provider: %@", self.instance.providerIdentifier ?: @"Unknown"] action:nil keyEquivalent:@""]; + NSString *providerName = self.instance.providerIdentifier ?: VMLocalizedString(@"Unknown"); + _chooseProviderMenuItem = [[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:VMLocalizedString(@"Provider: %@"), providerName] action:nil keyEquivalent:@""]; [_submenu addItem:_chooseProviderMenuItem]; } @@ -253,16 +255,17 @@ - (void)refresh { } [_chooseProviderMenuItem setSubmenu:submenu]; - _chooseProviderMenuItem.title = [NSString stringWithFormat:@"Provider: %@", self.instance.providerIdentifier ?: @"Unknown"]; + NSString *providerName = self.instance.providerIdentifier ?: VMLocalizedString(@"Unknown"); + _chooseProviderMenuItem.title = [NSString stringWithFormat:VMLocalizedString(@"Provider: %@"), providerName]; if (!_removeBookmarkMenuItem) { - _removeBookmarkMenuItem = [[NSMenuItem alloc] initWithTitle:@"Remove from bookmarks" action:@selector(removeBookmarkMenuItemClicked:) keyEquivalent:@""]; + _removeBookmarkMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Remove Bookmark") action:@selector(removeBookmarkMenuItemClicked:) keyEquivalent:@""]; _removeBookmarkMenuItem.target = self; [_submenu addItem:_removeBookmarkMenuItem]; } if (!_addBookmarkMenuItem) { - _addBookmarkMenuItem = [[NSMenuItem alloc] initWithTitle:@"Add to bookmarks" action:@selector(addBookmarkMenuItemClicked:) keyEquivalent:@""]; + _addBookmarkMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Add Bookmark") action:@selector(addBookmarkMenuItemClicked:) keyEquivalent:@""]; _addBookmarkMenuItem.target = self; [_submenu addItem:_addBookmarkMenuItem]; } @@ -359,70 +362,70 @@ - (void)refresh { [machineSubmenu setAutoenablesItems:NO]; machineSubmenu.delegate = self; - NSMenuItem *machineUpMenuItem = [[NSMenuItem alloc] initWithTitle:@"Up" action:@selector(upMachine:) keyEquivalent:@""]; + NSMenuItem *machineUpMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Up") action:@selector(upMachine:) keyEquivalent:@""]; machineUpMenuItem.target = self; machineUpMenuItem.representedObject = machine; machineUpMenuItem.image = [NSImage imageNamed:@"up"]; [machineUpMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineUpMenuItem]; - NSMenuItem *machineUpProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:@"Up (with provision)" action:@selector(upProvisionMachine:) keyEquivalent:@""]; + NSMenuItem *machineUpProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Up (with provision)") action:@selector(upProvisionMachine:) keyEquivalent:@""]; machineUpProvisionMenuItem.target = self; machineUpProvisionMenuItem.representedObject = machine; machineUpProvisionMenuItem.image = [NSImage imageNamed:@"up"]; [machineUpProvisionMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineUpProvisionMenuItem]; - NSMenuItem *machineSSHMenuItem = [[NSMenuItem alloc] initWithTitle:@"SSH" action:@selector(sshMachine:) keyEquivalent:@""]; + NSMenuItem *machineSSHMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"SSH") action:@selector(sshMachine:) keyEquivalent:@""]; machineSSHMenuItem.target = self; machineSSHMenuItem.representedObject = machine; machineSSHMenuItem.image = [NSImage imageNamed:@"ssh"]; [machineSSHMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineSSHMenuItem]; - NSMenuItem *machineRDPMenuItem = [[NSMenuItem alloc] initWithTitle:@"RDP" action:@selector(rdpMachine:) keyEquivalent:@""]; + NSMenuItem *machineRDPMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"RDP") action:@selector(rdpMachine:) keyEquivalent:@""]; machineRDPMenuItem.target = self; machineRDPMenuItem.representedObject = machine; machineRDPMenuItem.image = [NSImage imageNamed:@"rdp"]; [machineRDPMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineRDPMenuItem]; - NSMenuItem *machineReloadMenuItem = [[NSMenuItem alloc] initWithTitle:@"Reload" action:@selector(reloadMachine:) keyEquivalent:@""]; + NSMenuItem *machineReloadMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Reload") action:@selector(reloadMachine:) keyEquivalent:@""]; machineReloadMenuItem.target = self; machineReloadMenuItem.representedObject = machine; machineReloadMenuItem.image = [NSImage imageNamed:@"reload"]; [machineReloadMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineReloadMenuItem]; - NSMenuItem *machineSuspendMenuItem = [[NSMenuItem alloc] initWithTitle:@"Suspend" action:@selector(suspendMachine:) keyEquivalent:@""]; + NSMenuItem *machineSuspendMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Suspend") action:@selector(suspendMachine:) keyEquivalent:@""]; machineSuspendMenuItem.target = self; machineSuspendMenuItem.representedObject = machine; machineSuspendMenuItem.image = [NSImage imageNamed:@"suspend"]; [machineSuspendMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineSuspendMenuItem]; - NSMenuItem *machineHaltMenuItem = [[NSMenuItem alloc] initWithTitle:@"Halt" action:@selector(haltMachine:) keyEquivalent:@""]; + NSMenuItem *machineHaltMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Halt") action:@selector(haltMachine:) keyEquivalent:@""]; machineHaltMenuItem.target = self; machineHaltMenuItem.representedObject = machine; machineHaltMenuItem.image = [NSImage imageNamed:@"halt"]; [machineHaltMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineHaltMenuItem]; - NSMenuItem *machineDestroyMenuItem = [[NSMenuItem alloc] initWithTitle:@"Destroy" action:@selector(destroyMachine:) keyEquivalent:@""]; + NSMenuItem *machineDestroyMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Destroy") action:@selector(destroyMachine:) keyEquivalent:@""]; machineDestroyMenuItem.target = self; machineDestroyMenuItem.representedObject = machine; machineDestroyMenuItem.image = [NSImage imageNamed:@"destroy"]; [machineDestroyMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineDestroyMenuItem]; - NSMenuItem *machineProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:@"Provision" action:@selector(provisionMachine:) keyEquivalent:@""]; + NSMenuItem *machineProvisionMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Provision") action:@selector(provisionMachine:) keyEquivalent:@""]; machineProvisionMenuItem.target = self; machineProvisionMenuItem.representedObject = machine; machineProvisionMenuItem.image = [NSImage imageNamed:@"provision"]; [machineProvisionMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineProvisionMenuItem]; - NSMenuItem *machineCustomCommandMenuItem = [[NSMenuItem alloc] initWithTitle:@"Custom Command" action:nil keyEquivalent:@""]; + NSMenuItem *machineCustomCommandMenuItem = [[NSMenuItem alloc] initWithTitle:VMLocalizedString(@"Custom Command") action:nil keyEquivalent:@""]; //machineCustomCommandMenuItem.image = [NSImage imageNamed:@"provision"]; [machineCustomCommandMenuItem.image setTemplate:YES]; [machineSubmenu addItem:machineCustomCommandMenuItem]; diff --git a/Vagrant Manager/PreferencesWindow.h b/Vagrant Manager/PreferencesWindow.h index cd1c976..89be4d1 100644 --- a/Vagrant Manager/PreferencesWindow.h +++ b/Vagrant Manager/PreferencesWindow.h @@ -31,6 +31,7 @@ @property (weak) IBOutlet NSPopUpButton *intervalMenu; @property (weak) IBOutlet NSButton *dontAnimateStatusIconCheckBox; @property (weak) IBOutlet NSButton *showTaskNotificationCheckBox; +@property (weak) IBOutlet NSPopUpButton *languagePopUpButton; - (IBAction)autoCloseCheckBoxClicked:(id)sender; @@ -51,6 +52,7 @@ - (IBAction)intervalMenuChanged:(id)sender; - (IBAction)dontAnimateStatusIconCheckBoxClicked:(id)sender; - (IBAction)showTaskNotificationCheckBoxClicked:(id)sender; +- (IBAction)languagePopUpButtonClicked:(id)sender; @end diff --git a/Vagrant Manager/PreferencesWindow.m b/Vagrant Manager/PreferencesWindow.m index 984d60b..e405872 100644 --- a/Vagrant Manager/PreferencesWindow.m +++ b/Vagrant Manager/PreferencesWindow.m @@ -6,6 +6,7 @@ // #import "PreferencesWindow.h" +#import "LanguageManager.h" @interface PreferencesWindow () @@ -22,6 +23,9 @@ - (id)initWithWindow:(NSWindow *)window { - (void)windowDidLoad { [super windowDidLoad]; + // Set localized window title + self.window.title = VMLocalizedString(@"Preferences..."); + NSString *terminalPreference = [[NSUserDefaults standardUserDefaults] stringForKey:@"terminalPreference"]; NSString *terminalEditorPreference = [[NSUserDefaults standardUserDefaults] stringForKey:@"terminalEditorPreference"]; BOOL autoCloseTaskWindows = [[NSUserDefaults standardUserDefaults] boolForKey:@"autoCloseTaskWindows"]; @@ -92,6 +96,22 @@ - (void)windowDidLoad { [self.sendProfileDataCheckBox setState:[Util shouldSendProfileData] ? NSOnState : NSOffState]; [self.launchAtLoginCheckBox setState:[self willStartAtLogin] ? NSOnState : NSOffState]; + + // Setup language selector + [self.languagePopUpButton removeAllItems]; + NSArray *languages = [[LanguageManager sharedManager] getAvailableLanguages]; + NSString *currentLanguage = [[LanguageManager sharedManager] getCurrentLanguage]; + + for(NSDictionary *lang in languages) { + NSString *code = [lang objectForKey:@"code"]; + NSString *name = [lang objectForKey:@"name"]; + [self.languagePopUpButton addItemWithTitle:name]; + NSMenuItem *item = [self.languagePopUpButton lastItem]; + item.representedObject = code; + if([code isEqualToString:currentLanguage]) { + [self.languagePopUpButton selectItem:item]; + } + } } - (IBAction)haltOnExitCheckBoxClicked:(id)sender { @@ -252,6 +272,16 @@ - (IBAction)intervalMenuChanged:(id)sender { [[Util getApp] refreshTimerState]; } +- (IBAction)languagePopUpButtonClicked:(id)sender { + NSString *selectedLanguage = self.languagePopUpButton.selectedItem.representedObject; + if(selectedLanguage) { + [[LanguageManager sharedManager] setLanguage:selectedLanguage]; + + // Language change notification will update the UI automatically + // No need to restart the app + } +} + - (void)setLaunchOnLogin:(BOOL)launchOnLogin { NSURL *bundleURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]]; diff --git a/Vagrant Manager/PreferencesWindow.xib b/Vagrant Manager/PreferencesWindow.xib index 7f8f7b5..048e058 100644 --- a/Vagrant Manager/PreferencesWindow.xib +++ b/Vagrant Manager/PreferencesWindow.xib @@ -21,6 +21,7 @@ + @@ -34,17 +35,17 @@ - + - + - + - + @@ -386,6 +387,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +