Skip to content
Closed
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
4 changes: 3 additions & 1 deletion .github/workflows/app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ jobs:
make -s seedcheck

- name: Run the Swift tests
run: make -s app-tests
run: |
make -s localizationcheck
NOTPROTON_LANGUAGE=en make -s app-tests

- name: Package the app bundle
run: |
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ jobs:

- name: Lint the embedded run script
run: make runcheck

- name: Check localization catalogs
run: python3 scripts/check-localization.py
25 changes: 21 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ panel-behavior:
node $(PANEL_TESTS)/launch-options.js $(OUT_DIR)/panel-emit && \
node $(PANEL_TESTS)/shell.js $(OUT_DIR)/panel-emit && \
node $(PANEL_TESTS)/migration.js $(OUT_DIR)/panel-emit && \
node $(PANEL_TESTS)/localization.js $(OUT_DIR)/panel-emit && \
echo "==> panel behavior: renders as expected, no stale arguments"

CX_ROOT ?= /Applications/CrossOver Preview.app
Expand Down Expand Up @@ -524,7 +525,7 @@ app-payload: $(TARGET) $(OVERLAY_SHIM) $(ICONMAKER) $(APPINFO)
@echo "==> Staged app payload in $(APP_PAYLOAD)"

APP_BUNDLE := $(OUT_DIR)/NotProton.app
APP_BUNDLE_PAYLOAD := $(APP_BUNDLE)/Contents/Resources/NotProtonApp_NotProtonApp.bundle/Contents/Resources/payload
APP_RESOURCE_BUNDLE := $(APP_BUNDLE)/Contents/Resources/NotProtonApp_NotProtonApp.bundle
APP_ZIP := $(OUT_DIR)/NotProton.zip
APP_VERSION := $(shell sed -n 's/^\#define NOTPROTON_VERSION "\(.*\)"/\1/p' dylib/version.h)

Expand All @@ -551,7 +552,19 @@ $(ICON_CAR): $(ICONGEN)
--output-partial-info-plist "$(ICON_DIR)/partial.plist" >/dev/null
@echo "==> Built $@"

app: app-payload $(ICON_CAR)
.PHONY: localizationcheck
localizationcheck:
python3 scripts/check-localization.py
@mkdir -p "$(OUT_DIR)"
swiftc -swift-version 6 -parse-as-library -o "$(OUT_DIR)/localization-smoke" \
app/Sources/NotProtonApp/Support/L10n.swift \
app/Sources/NotProtonApp/Support/AppResources.swift scripts/localization-smoke.swift
NOTPROTON_LANGUAGE=zh-Hans "$(OUT_DIR)/localization-smoke" \
"$(CURDIR)/app/Sources/NotProtonApp/Resources/Localization"
NOTPROTON_LANGUAGE=en "$(OUT_DIR)/localization-smoke" \
"$(CURDIR)/app/Sources/NotProtonApp/Resources/Localization"

app: app-payload $(ICON_CAR) localizationcheck
swift build --package-path app -c release
rm -rf "$(APP_BUNDLE)"
@mkdir -p "$(APP_BUNDLE)/Contents/MacOS" "$(APP_BUNDLE)/Contents/Resources"
Expand All @@ -571,9 +584,11 @@ app: app-payload $(ICON_CAR)
"$(APP_BUNDLE)/Contents/Resources/"
cp "$(ICON_DIR)/Assets.car" "$(ICON_DIR)/NotProton.icns" \
"$(APP_BUNDLE)/Contents/Resources/"
@missing=$$(cd "$(APP_PAYLOAD)" && find . -type f ! -name '.DS_Store' | sed 's|^\./||' \
@resources="$(CURDIR)/$(APP_RESOURCE_BUNDLE)"; \
if [ -d "$$resources/Contents/Resources" ]; then resources="$$resources/Contents/Resources"; fi; \
missing=$$(cd "$(APP_PAYLOAD)" && find . -type f ! -name '.DS_Store' | sed 's|^\./||' \
| while read -r rel; do \
[ -s "$(CURDIR)/$(APP_BUNDLE_PAYLOAD)/$$rel" ] || echo "$$rel"; \
[ -s "$$resources/payload/$$rel" ] || echo "$$rel"; \
done); \
if [ -n "$$missing" ]; then \
echo "==> the bundle is missing staged payload, so the app would ship without it:" >&2; \
Expand All @@ -584,6 +599,8 @@ app: app-payload $(ICON_CAR)
@echo "==> Payload complete in the bundle"
codesign -f -s "$(APP_SIGN_ID)" "$(APP_BUNDLE)"
codesign --verify --strict "$(APP_BUNDLE)"
NOTPROTON_LANGUAGE=zh-Hans "$(OUT_DIR)/localization-smoke" \
"$(CURDIR)/$(APP_RESOURCE_BUNDLE)" "$(CURDIR)/$(APP_BUNDLE)"
@echo "==> Built $(APP_BUNDLE) ($(APP_VERSION))"

app-zip: app
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ Please read NOTICE for license information.

Please open issue reports with any issues. PRs are welcome and encouraged. Contributions policy to come shortly.

The app supports English and Simplified Chinese, following macOS's language
preferences. See [Localization](docs/LOCALIZATION.md) for per-app language
selection and translation checks.

There are many people who worked on similar ideas, similar projects. I did not base NotProton on their work, but I still want to
give thanks to the people who came before me:

Expand Down
5 changes: 5 additions & 0 deletions app/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>zh-Hans</string>
</array>
<key>CFBundleExecutable</key>
<string>NotProtonApp</string>
<key>CFBundleIconFile</key>
Expand Down
2 changes: 2 additions & 0 deletions app/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let hasPayload = FileManager.default.fileExists(
atPath: packageRoot.appendingPathComponent(payloadPath).path)
let package = Package(
name: "NotProtonApp",
defaultLocalization: "en",
platforms: [.macOS("26.0")],
dependencies: [
.package(url: "https://github.com/sparkle-project/Sparkle", exact: "2.10.0"),
Expand All @@ -24,6 +25,7 @@ let package = Package(
dependencies: [.product(name: "Sparkle", package: "Sparkle")],
path: "Sources/NotProtonApp",
resources: (hasPayload ? [.copy("Resources/payload")] : []) + [
.process("Resources/Localization"),
.copy("Resources/payload.manifest"),
.copy("Resources/valve-packages.manifest"),
.copy("Resources/detour2.bin"),
Expand Down
32 changes: 16 additions & 16 deletions app/Sources/NotProtonApp/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,20 @@ struct NotProtonApp: App {
CommandGroup(replacing: .newItem) {}

CommandGroup(after: .appInfo) {
Button("Check for Updates…") { updater.check() }
Button(L10n.tr("Check for Updates…")) { updater.check() }
}

CommandGroup(after: .sidebar) {
Button("Status") { pane = .status }
Button(L10n.tr("Status")) { pane = .status }
.keyboardShortcut("1", modifiers: .command)
Button("Prefixes") { pane = .prefixes }
Button(L10n.tr("Prefixes")) { pane = .prefixes }
.keyboardShortcut("2", modifiers: .command)
Button("Prefix Backups") { pane = .backups }
Button(L10n.tr("Prefix Backups")) { pane = .backups }
.keyboardShortcut("3", modifiers: .command)

Divider()

Button("Refresh") {
Button(L10n.tr("Refresh")) {
Task {
switch pane {
case .status: await status.refresh()
Expand All @@ -61,27 +61,27 @@ struct NotProtonApp: App {
Divider()
}

CommandMenu("Setup") {
Button("Install") { Task { await status.requestInstall() } }
CommandMenu(L10n.tr("Setup")) {
Button(L10n.tr("Install")) { Task { await status.requestInstall() } }
.keyboardShortcut("i", modifiers: .command)
.disabled(!status.canInstall)

Button("Set Up Compatibility Tool") { Task { await status.requestCompatibilityTool() } }
Button(L10n.tr("Set Up Compatibility Tool")) { Task { await status.requestCompatibilityTool() } }
.disabled(!status.canInstall || status.setupSource == nil)

Button("Fetch Valve Binaries") { Task { await status.fetchValveBinaries() } }
Button(L10n.tr("Fetch Valve Binaries")) { Task { await status.fetchValveBinaries() } }
.disabled(!status.canInstall)

Divider()

updateBlockCommand

Button("Repair Steam") { status.pendingConfirmation = .replaceSteam }
Button(L10n.tr("Repair Steam")) { status.pendingConfirmation = .replaceSteam }
.disabled(!status.isIdle)

Divider()

Button("Reveal Log in Finder") {
Button(L10n.tr("Reveal Log in Finder")) {
NSWorkspace.shared.activateFileViewerSelecting([AppLog.file])
}
}
Expand All @@ -91,8 +91,8 @@ struct NotProtonApp: App {

@CommandsBuilder
private var prefixMenu: some Commands {
CommandMenu("Prefix") {
Button("Run Program…") {
CommandMenu(L10n.tr("Prefix")) {
Button(L10n.tr("Run Program…")) {
if let prefix = prefixTarget { prefixes.chooseExecutable(for: prefix) }
}
.disabled(prefixTarget == nil)
Expand All @@ -108,7 +108,7 @@ struct NotProtonApp: App {

Divider()

Button("Reveal in Finder") {
Button(L10n.tr("Reveal in Finder")) {
if let prefix = prefixes.selectedPrefix { prefixes.reveal(prefix) }
}
.keyboardShortcut("r", modifiers: [.command, .shift])
Expand Down Expand Up @@ -161,10 +161,10 @@ struct NotProtonApp: App {
@ViewBuilder
private var updateBlockCommand: some View {
if status.snapshot?.updateBlocked == true {
Button("Allow Client Updates") { Task { await status.setUpdateBlock(false) } }
Button(L10n.tr("Allow Client Updates")) { Task { await status.setUpdateBlock(false) } }
.disabled(!status.isIdle)
} else {
Button("Block Client Updates") { status.pendingConfirmation = .blockUpdates }
Button(L10n.tr("Block Client Updates")) { status.pendingConfirmation = .blockUpdates }
.disabled(!status.isIdle || status.snapshot == nil)
}
}
Expand Down
7 changes: 3 additions & 4 deletions app/Sources/NotProtonApp/Model/BridgePayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import Foundation

enum BridgePayload {
static let step = "Copy the built bridge files"
static let step = L10n.tr("Copy the built bridge files")

struct Entry: Sendable {
// Two different lsteamclient binaries, one 32 bit and one 64 bit.
Expand Down Expand Up @@ -45,7 +45,7 @@ enum BridgePayload {
guard let payloadRoot = try? InstallPayload.root() else {
throw StepFailure(
step: step,
detail: "NotProton carries no components."
detail: L10n.tr("NotProton carries no components.")
)
}
base = payloadRoot.appending(path: "bridge")
Expand All @@ -67,8 +67,7 @@ enum BridgePayload {
guard missing.isEmpty else {
throw StepFailure(
step: step,
detail: "Missing components: \(missing.joined(separator: ", ")). "
+ "Run make app-payload and build the app again."
detail: L10n.tr("Missing components: \(missing.joined(separator: ", ")). Run make app-payload and build the app again.")
)
}

Expand Down
8 changes: 4 additions & 4 deletions app/Sources/NotProtonApp/Model/CrossOverLicense.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ enum CrossOverLicense {

private static let licenseBase = "com.codeweavers.CrossOver"

static let notActivatedTitle = "CrossOver does not appear to be activated."
static let notActivatedAdvice = "Please run CrossOver and try again."
static let notActivatedTitle = L10n.tr("CrossOver does not appear to be activated.")
static let notActivatedAdvice = L10n.tr("Please run CrossOver and try again.")
static var notActivated: String { "\(notActivatedTitle) \(notActivatedAdvice)" }

static let defaultOpenssl = "/usr/bin/openssl"
Expand Down Expand Up @@ -94,7 +94,7 @@ enum CrossOverLicense {
if result.succeeded {
return Status(
licensed: true,
detail: "CrossOver is activated.",
detail: L10n.tr("CrossOver is activated."),
diagnostic: "valid license in \(label)"
)
}
Expand Down Expand Up @@ -127,7 +127,7 @@ enum CrossOverLicense {
let status = check(crossOverRoot: install.crossOverRoot)
guard status.licensed else {
throw StepFailure(
step: "Verify CrossOver license",
step: L10n.tr("Verify CrossOver license"),
detail: status.detail
)
}
Expand Down
10 changes: 4 additions & 6 deletions app/Sources/NotProtonApp/Model/CrossOverSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,14 @@ enum CrossOverSource {
let ntdll = NtdllPatcher.cleanSource(inRoot: root, arch: arch)
guard let actual = Digest.sha256IfPresent(ntdll) else {
throw StepFailure(
step: "Verify CrossOver",
detail: "\(arch.rawValue)/ntdll.dll is missing from \(root.path(percentEncoded: false))."
step: L10n.tr("Verify CrossOver"),
detail: L10n.tr("\(arch.rawValue)/ntdll.dll is missing from \(root.path(percentEncoded: false)).")
)
}
guard actual == expected else {
throw StepFailure(
step: "Verify CrossOver",
detail: "\(arch.rawValue)/\(ntdll.lastPathComponent) is not the build "
+ "\(build.bundleVersion) copy. Expected \(expected.prefix(16)), "
+ "found \(actual.prefix(16))."
step: L10n.tr("Verify CrossOver"),
detail: L10n.tr("\(arch.rawValue)/\(ntdll.lastPathComponent) is not the build \(build.bundleVersion) copy. Expected \(expected.prefix(16)), found \(actual.prefix(16)).")
)
}
}
Expand Down
10 changes: 5 additions & 5 deletions app/Sources/NotProtonApp/Model/DeploymentContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ enum DeploymentContent {
let parent = app.deletingLastPathComponent().path(percentEncoded: false)
let fd = open(parent, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC)
guard fd >= 0 else {
throw StepFailure(step: SteamInstaller.step, detail: "Could not lock the installation at \(parent): \(String(cString: strerror(errno))).")
throw StepFailure(step: SteamInstaller.step, detail: L10n.tr("Could not lock the installation at \(parent): \(String(cString: strerror(errno)))."))
}
guard flock(fd, LOCK_EX | LOCK_NB) == 0 else {
close(fd)
throw StepFailure(step: SteamInstaller.step, detail: "Another installation operation is changing files here. Wait for it to finish.")
throw StepFailure(step: SteamInstaller.step, detail: L10n.tr("Another installation operation is changing files here. Wait for it to finish."))
}
return fd
}
Expand All @@ -72,7 +72,7 @@ enum DeploymentContent {
guard FileManager.default.fileExists(atPath: file.path(percentEncoded: false)) else { return nil }
let build = try JSONDecoder().decode(Build.self, from: Data(contentsOf: file))
guard !build.version.isEmpty, build.builtAt > 0 else {
throw StepFailure(step: "Check installed files", detail: "The installed build record is invalid.")
throw StepFailure(step: L10n.tr("Check installed files"), detail: L10n.tr("The installed build record is invalid."))
}
return build
}
Expand All @@ -98,8 +98,8 @@ enum DeploymentContent {
let deployed = try MachOBuild.hashesIgnoringSignature(of: dylib.destination),
hashes != deployed
{
throw StepFailure(step: "Check installed files",
detail: "Steam's installed dylib does not match this account's build record. Finish or repair its installation with the NotProton app that last installed it.")
throw StepFailure(step: L10n.tr("Check installed files"),
detail: L10n.tr("Steam's installed dylib does not match this account's build record. Finish or repair its installation with the NotProton app that last installed it."))
}
return bundled.isNewer(than: installed) ? .update(differences) : .repair(differences)
}
Expand Down
11 changes: 5 additions & 6 deletions app/Sources/NotProtonApp/Model/InstallPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import Foundation

enum InstallPayload {
static let step = "Find NotProton's components"
static let step = L10n.tr("Find NotProton's components")

struct Located: Sendable {
let dylib: URL
Expand All @@ -16,17 +16,17 @@ enum InstallPayload {
let signatures: [URL]
}

static func root(in bundle: Bundle = .module) throws -> URL {
static func root(in bundle: Bundle = AppResources.bundle) throws -> URL {
guard let url = bundle.url(forResource: "payload", withExtension: nil) else {
throw StepFailure(
step: step,
detail: "This build of NotProton carries no components at all."
detail: L10n.tr("This build of NotProton carries no components at all.")
)
}
return url
}

static func locate(in bundle: Bundle = .module) throws -> Located {
static func locate(in bundle: Bundle = AppResources.bundle) throws -> Located {
try locate(root: try root(in: bundle))
}

Expand Down Expand Up @@ -67,8 +67,7 @@ enum InstallPayload {
guard missing.isEmpty, let builtAt, builtAt > 0 else {
throw StepFailure(
step: step,
detail: "These components are missing: \(missing.joined(separator: ", ")). "
+ "Run make app-payload and build the app again."
detail: L10n.tr("These components are missing: \(missing.joined(separator: ", ")). Run make app-payload and build the app again.")
)
}

Expand Down
Loading