Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ swift build && REMOVEMACAI_SNAPSHOTS=/tmp/shots .build/debug/removemacai app

A new tweak goes in `Sources/removemacai/Tweaks.swift`. Check that macOS still reads its key before adding it, and say how you checked.

Translations live in `Localization/<language>.lproj`: `Localizable.strings` keyed by the English text in the code, and `Localizable.stringsdict` for counts. `tools/build-app.sh` copies them into the app. `tools/check-localization.py <language>` lists window text that has no translation yet; run it after changing any text the app shows.

Every change RemoveMacAI makes is shown before it happens and can be undone, and a new tweak has to keep that. A tweak that can break something people rely on stays out of the presets.

Keep one change per pull request, and say which macOS version and build you tested on. For a bug, the output of `removemacai status` and `sw_vers` helps most.
413 changes: 413 additions & 0 deletions Localization/ru.lproj/Localizable.strings

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions Localization/ru.lproj/Localizable.stringsdict
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>%lld changes ready</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@n@</string>
<key>n</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>few</key>
<string>Готово %lld изменения</string>
<key>many</key>
<string>Готово %lld изменений</string>
<key>one</key>
<string>Готово %lld изменение</string>
<key>other</key>
<string>Готово %lld изменения</string>
</dict>
</dict>
<key>%lld features are kept on. Everything else is off and its models are gone.</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@n@</string>
<key>n</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>few</key>
<string>%lld функции оставлены включёнными. Всё остальное выключено, модели удалены.</string>
<key>many</key>
<string>%lld функций оставлено включёнными. Всё остальное выключено, модели удалены.</string>
<key>one</key>
<string>%lld функция оставлена включённой. Всё остальное выключено, модели удалены.</string>
<key>other</key>
<string>%lld функции оставлены включёнными. Всё остальное выключено, модели удалены.</string>
</dict>
</dict>
<key>%lld items macOS protects stayed where they were.</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@n@</string>
<key>n</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>few</key>
<string>%lld объекта, защищённых macOS, остались на месте.</string>
<key>many</key>
<string>%lld объектов, защищённых macOS, остались на месте.</string>
<key>one</key>
<string>%lld объект, защищённый macOS, остался на месте.</string>
<key>other</key>
<string>%lld объекта, защищённых macOS, остались на месте.</string>
</dict>
</dict>
<key>%lld settings</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@n@</string>
<key>n</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>few</key>
<string>%lld настройки</string>
<key>many</key>
<string>%lld настроек</string>
<key>one</key>
<string>%lld настройка</string>
<key>other</key>
<string>%lld настройки</string>
</dict>
</dict>
<key>%lld settings and Apple Intelligence off</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@n@</string>
<key>n</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>few</key>
<string>%lld настройки и выключение Apple Intelligence</string>
<key>many</key>
<string>%lld настроек и выключение Apple Intelligence</string>
<key>one</key>
<string>%lld настройка и выключение Apple Intelligence</string>
<key>other</key>
<string>%lld настройки и выключение Apple Intelligence</string>
</dict>
</dict>
<key>%lld snapshots</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@n@</string>
<key>n</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>few</key>
<string>%lld снимка</string>
<key>many</key>
<string>%lld снимков</string>
<key>one</key>
<string>%lld снимок</string>
<key>other</key>
<string>%lld снимка</string>
</dict>
</dict>
<key>Turn off everything except %lld kept features</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@n@</string>
<key>n</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>lld</string>
<key>few</key>
<string>Выключить всё, кроме %lld оставленных функций</string>
<key>many</key>
<string>Выключить всё, кроме %lld оставленных функций</string>
<key>one</key>
<string>Выключить всё, кроме %lld оставленной функции</string>
<key>other</key>
<string>Выключить всё, кроме %lld оставленных функций</string>
</dict>
</dict>
</dict>
</plist>
24 changes: 13 additions & 11 deletions Sources/removemacai/App/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,16 +213,16 @@ final class AppModel {
if let target = plan.profile {
await MainActor.run { self.run = .approveProfile(remove: target.isEmpty) }
if !target.isEmpty {
do { try Profile.present(target) } catch { problems.append("Could not write the profile: \(error)") }
do { try Profile.present(target) } catch { problems.append(String(localized: "Could not write the profile: \(String(describing: error))")) }
try? await Task.sleep(nanoseconds: 1_000_000_000)
}
await MainActor.run { Commands.openProfileSettings() }
let approved = await self.waitFor { target.isEmpty ? !Profile.installed().on : Profile.matches(target) }
if !approved {
problems.append(
target.isEmpty
? "The profile is still installed. Remove RemoveMacAI in System Settings > General > Device Management."
: "The profile wasn't approved, so the locked settings and Apple Intelligence didn't change.")
? String(localized: "The profile is still installed. Remove RemoveMacAI in System Settings > General > Device Management.")
: String(localized: "The profile wasn't approved, so the locked settings and Apple Intelligence didn't change."))
}
}
// Models only go once the profile blocks their download.
Expand All @@ -241,19 +241,19 @@ final class AppModel {
}

nonisolated static func deleteModels(_ sets: [String]) -> (problems: [String], freed: Int64?) {
guard Models.available() else { return (["Apple's asset service didn't answer, so the models stay for now."], nil) }
guard Models.available() else { return ([String(localized: "Apple's asset service didn't answer, so the models stay for now.")], nil) }
var problems: [String] = []
var removing = sets
do {
let split = try Models.matching(sets)
removing = split.matched
problems += split.skipped.map { "\(Catalog.modelSet($0.0)?.title ?? $0.0) stayed: \($0.1)" }
problems += split.skipped.map { String(localized: "\(L(Catalog.modelSet($0.0)?.title ?? $0.0)) stayed: \(L($0.1))") }
} catch { return (["\(error)"], nil) }
let before = Models.total(removing)
do {
let result = try Commands.removeModels(removing, before: before)
problems += result.failures.map { "\(Catalog.modelSet($0.0)?.title ?? $0.0): \($0.1)" }
if !result.verified { problems.append("Removal couldn't be confirmed yet. Check again in a minute.") }
problems += result.failures.map { "\(L(Catalog.modelSet($0.0)?.title ?? $0.0)): \(L($0.1))" }
if !result.verified { problems.append(String(localized: "Removal couldn't be confirmed yet. Check again in a minute.")) }
return (problems, result.deletedBytes)
} catch {
return (problems + ["\(error)"], nil)
Expand Down Expand Up @@ -303,7 +303,7 @@ final class AppModel {
Commands.openProfileSettings()
}
if !(await self.waitFor { !Profile.installed().on }) {
problems.append("The profile is still installed. Remove RemoveMacAI in System Settings > General > Device Management.")
problems.append(String(localized: "The profile is still installed. Remove RemoveMacAI in System Settings > General > Device Management."))
}
}
let outcome = RunState.finished(problems: problems, freed: nil)
Expand Down Expand Up @@ -357,14 +357,16 @@ final class AppModel {
let trashedTotal = chosen.filter { !$0.permanent }.reduce(Int64(0)) { $0 + $1.bytes }
let left = items.filter { trashedIDs.contains($0.id) }.reduce(Int64(0)) { $0 + $1.bytes }
var parts: [String] = []
if !trashedIDs.isEmpty { parts.append("Moved \(Term.size(max(0, trashedTotal - left))) to the Trash. Empty the Trash to free the space.") }
if !trashedIDs.isEmpty { parts.append(String(localized: "Moved \(UISize.of(max(0, trashedTotal - left))) to the Trash. Empty the Trash to free the space.")) }
let permanent = chosen.filter(\.permanent)
if !permanent.isEmpty && result.problems.isEmpty {
parts.append("Deleted \(permanent.map(\.title).joined(separator: " and ")).")
parts.append(String(localized: "Deleted \(permanent.map { L($0.title) }.joined(separator: L(" and ")))."))
}
var message = parts.joined(separator: " ")
if result.protected > 0 {
message += " \(result.protected) item\(result.protected == 1 ? "" : "s") macOS protects stayed where \(result.protected == 1 ? "it was" : "they were")."
message += " " + (result.protected == 1
? String(localized: "1 item macOS protects stayed where it was.")
: String(localized: "\(result.protected) items macOS protects stayed where they were."))
}
if !result.problems.isEmpty { message += "\n" + result.problems.joined(separator: "\n") }
let finalMessage = message
Expand Down
18 changes: 9 additions & 9 deletions Sources/removemacai/App/Help.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ struct InfoButton: View {
Image(systemName: "info.circle").foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help(title)
.accessibilityLabel("About \(title)")
.help(L(title))
.accessibilityLabel("About \(L(title))")
.popover(isPresented: $shown, arrowEdge: .bottom) {
VStack(alignment: .leading, spacing: 6) {
Text(title).font(.headline)
Text(text).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
Text(L(title)).font(.headline)
Text(L(text)).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
.padding(14)
.frame(width: 300, alignment: .leading)
Expand Down Expand Up @@ -88,8 +88,8 @@ struct PromiseCard: View {
.foregroundStyle(promise.tint)
.frame(width: 28)
VStack(alignment: .leading, spacing: 3) {
Text(promise.title).font(.headline)
Text(promise.detail).font(.callout).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
Text(L(promise.title)).font(.headline)
Text(L(promise.detail)).font(.callout).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 0)
}
Expand Down Expand Up @@ -128,7 +128,7 @@ struct SafetyCard: View {
}
}

func check(_ text: String) -> some View {
func check(_ text: LocalizedStringKey) -> some View {
Label {
Text(text).foregroundStyle(.primary)
} icon: {
Expand Down Expand Up @@ -160,12 +160,12 @@ struct SettingsGuide: View {
.background(.quaternary.opacity(0.6))
HStack(alignment: .top, spacing: 0) {
VStack(alignment: .leading, spacing: 7) {
ForEach(["Wi-Fi", "Bluetooth", "Network"], id: \.self) { Text($0) }
ForEach(["Wi-Fi", "Bluetooth", "Network"], id: \.self) { Text(L($0)) }
Text("General")
.foregroundStyle(.white)
.padding(.horizontal, 6).padding(.vertical, 2)
.background(RoundedRectangle(cornerRadius: 5).fill(Color.accentColor))
ForEach(["Appearance", "Privacy & Security"], id: \.self) { Text($0) }
ForEach(["Appearance", "Privacy & Security"], id: \.self) { Text(L($0)) }
}
.font(.caption)
.padding(10)
Expand Down
32 changes: 32 additions & 0 deletions Sources/removemacai/App/L10n.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

/// Looks up the translation of a fixed English string, such as a tweak's title,
/// in the app's Localization/<language>.lproj. Gives back the English when
/// there is none, and always from the bare command-line binary.
func L(_ s: String) -> String {
guard !s.isEmpty else { return s }
return Bundle.main.localizedString(forKey: s, value: s, table: nil)
}

/// A size for the window: the number in the region's format, the unit in the app's language.
enum UISize {
static func of(_ bytes: Int64) -> String {
let s = Term.size(bytes)
let unit: String
if s.hasSuffix(" GB") { unit = L("GB") } else if s.hasSuffix(" MB") { unit = L("MB") } else { return s }
let number = s.dropLast(3).replacingOccurrences(of: ".", with: Locale.current.decimalSeparator ?? ".")
return number + " " + unit
}
}

/// Translates a message the engine builds from parts, such as a storage
/// caveat that names apps, and anything else with a fixed English text.
func localizedMessage(_ s: String) -> String {
let prefix = "Its sound library stays, because ", suffix = " uses it too."
if s.hasPrefix(prefix) && s.hasSuffix(suffix) {
let names = String(s.dropFirst(prefix.count).dropLast(suffix.count))
.replacingOccurrences(of: " and ", with: L(" and "))
return String(localized: "Its sound library stays, because \(names) uses it too.")
}
return L(s)
}
Loading