Skip to content
Merged
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
18 changes: 18 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,24 @@ The exported names are included in `rivet-schema.json`: removing one is a
breaking change, while adding one is compatible. See
[typed device communication](device-communication.md).

## macOS bundle localizations

Apps whose in-app strings already follow the user language declare

```
(macos-localizations "zh-Hans" "en")
```

in `rivet.rktd` (BCP 47 identifiers). Without a declaration the bundle
builds byte-identical to before, and AppKit renders every system-provided
string — About / Hide / Quit, standard alerts, SwiftUI navigation
tooltips — in the development region's language. With the declaration,
`CFBundleLocalizations` lands in Info.plist and packaging stages marker
`Contents/Resources/<lang>.lproj/InfoPlist.strings` files before signing
(no re-sign needed), so `CFBundleCopyBundleLocalizations` resolves the
user's language and the system chrome localizes to match the in-app
strings. Only declare languages the application itself already ships.

## Application resources and icons

- `resources` — a list of project-relative files or directories copied into the application resource root while preserving their relative paths. Generated/control roots (`.git`, `.rivet`, `build`, and `dist`) and symbolic links are rejected so a release cannot accidentally capture repository metadata, stale output, or files outside the project.
Expand Down
33 changes: 29 additions & 4 deletions rivet-cli/package.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@
minimum-version
icon-name
url-schemes
file-associations)
file-associations
[localizations '()])
(define (xml-escape value)
(regexp-replace*
#px"[&<>\"]" value
Expand Down Expand Up @@ -95,11 +96,22 @@
(format " <key>CFBundleIconFile</key><string>~a</string>\n"
(xml-escape icon-name))
""))
;; Declared localizations make CFBundleCopyBundleLocalizations (and thus
;; the system-provided chrome: About/Hide/Quit menus, standard alerts)
;; resolve to the user's language instead of the development region.
(define localization-fragment
(if (null? localizations)
""
(format
" <key>CFBundleLocalizations</key><array>~a</array>\n"
(apply string-append
(for/list ([language (in-list localizations)])
(format "<string>~a</string>" (xml-escape language)))))))
(call-with-output-file path
#:exists 'truncate/replace
(lambda (out)
(fprintf out
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>CFBundleDevelopmentRegion</key><string>en</string>\n <key>CFBundleExecutable</key><string>~a</string>\n <key>CFBundleIdentifier</key><string>~a</string>\n <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>\n <key>CFBundleName</key><string>~a</string>\n <key>CFBundleDisplayName</key><string>~a</string>\n <key>CFBundlePackageType</key><string>APPL</string>\n <key>CFBundleShortVersionString</key><string>~a</string>\n <key>CFBundleVersion</key><string>~a</string>\n <key>LSMinimumSystemVersion</key><string>~a</string>\n~a~a~a <key>NSHighResolutionCapable</key><true/>\n</dict>\n</plist>\n"
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n <key>CFBundleDevelopmentRegion</key><string>en</string>\n <key>CFBundleExecutable</key><string>~a</string>\n <key>CFBundleIdentifier</key><string>~a</string>\n <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>\n <key>CFBundleName</key><string>~a</string>\n <key>CFBundleDisplayName</key><string>~a</string>\n <key>CFBundlePackageType</key><string>APPL</string>\n <key>CFBundleShortVersionString</key><string>~a</string>\n <key>CFBundleVersion</key><string>~a</string>\n <key>LSMinimumSystemVersion</key><string>~a</string>\n~a~a~a~a <key>NSHighResolutionCapable</key><true/>\n</dict>\n</plist>\n"
(xml-escape executable)
(xml-escape identifier)
(xml-escape display-name)
Expand All @@ -109,7 +121,8 @@
(xml-escape minimum-version)
icon-fragment
url-fragment
association-fragment))))
association-fragment
localization-fragment))))

(define (write-entitlements! path)
(call-with-output-file path
Expand Down Expand Up @@ -354,6 +367,7 @@
"docs/configuration.md, \"Application resources and icons\".")
(string-join swiftpm-bundles ", ")))

(define localizations (project-macos-localizations project))
(write-macos-info! (build-path contents "Info.plist")
display-name
executable-name
Expand All @@ -363,7 +377,18 @@
(project-macos-min-version project)
packaged-icon-name
(project-url-schemes project)
(project-file-associations project))
(project-file-associations project)
localizations)
;; Marker .lproj directories plus an empty InfoPlist.strings make the
;; bundle resolve every declared language. Injection happens before
;; sign-macos!, so no re-sign is needed. Projects without the
;; declaration build a byte-identical bundle to before.
(for ([language (in-list localizations)])
(define lproj (build-path contents "Resources" (format "~a.lproj" language)))
(make-parent-directory* (build-path lproj "InfoPlist.strings"))
(call-with-output-file (build-path lproj "InfoPlist.strings")
#:exists 'truncate/replace
void))

(define entitlements
(project-path project ".rivet" "macos-entitlements.plist"))
Expand Down
19 changes: 19 additions & 0 deletions rivet-cli/project.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
project-device-rpcs
project-windows-icon
project-macos-icon
project-macos-localizations
project-linux-icon
project-dmg-background
project-windows-license
Expand Down Expand Up @@ -202,6 +203,18 @@
(optional 'macos-icon
(lambda (v) (path-string-has-extension? v ".icns"))
"project-relative .icns path")
(optional 'macos-localizations
(lambda (v)
(define valid-language?
(lambda (item)
(and (string? item)
(regexp-match? #px"^[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*$"
item))))
(and (list? v)
(not (null? v))
(andmap valid-language? v)
(= (length v) (length (remove-duplicates v)))))
"BCP 47 language identifiers that already ship in-app, e.g. (macos-localizations \"zh-Hans\" \"en\")")
(optional 'dmg-background
(lambda (v) (path-string-has-extension? v ".png"))
"project-relative .png path")
Expand Down Expand Up @@ -327,6 +340,12 @@
(define (project-macos-icon project)
(project-ref project 'macos-icon (lambda () #f)))

;; Languages the application itself already localizes. Declaring them makes
;; AppKit localize the system-provided chrome (About/Quit menus, standard
;; alerts) into the user's language instead of the development region's.
(define (project-macos-localizations project)
(project-ref project 'macos-localizations (lambda () '())))

;; The branded DMG layout switches on this key; projects without it keep the
;; plain single-folder image.
(define (project-dmg-background project)
Expand Down
36 changes: 36 additions & 0 deletions rivet/tests/resources.rkt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,42 @@
(regexp-match? #rx"<key>CFBundleIconFile</key><string>AppIcon.icns</string>"
plist-text))
(check-true (regexp-match? #rx"Demo &amp; Test" plist-text))
;; Without a declaration there is no CFBundleLocalizations key — the
;; byte-identical behavior projects without the key have always had.
(check-false (regexp-match? #rx"CFBundleLocalizations" plist-text))

;; Declared localizations land in Info.plist and marker lproj
;; directories; the declaration also rejects malformed language tags.
(define localized-plist (build-path temp-root "Localized-Info.plist"))
(write-macos-info! localized-plist
"Demo"
"Demo"
"dev.rivet.demo"
"1.2.3"
7
"14.0"
#f
'()
'()
'("zh-Hans" "en"))
(define localized-text (file->string localized-plist))
(check-true
(regexp-match?
#rx"<key>CFBundleLocalizations</key><array><string>zh-Hans</string><string>en</string></array>"
localized-text))
(check-false
(regexp-match? #rx"CFBundleLocalizations</key><array><string>zh"
(file->string plist)))
;; Malformed language tags fail at the declaration, not the writer.
(define bad-loc-root (build-path temp-root "bad-loc"))
(make-directory* bad-loc-root)
(call-with-output-file (build-path bad-loc-root "rivet.rktd")
(lambda (out)
(displayln
"#hasheq((name . \"Demo_App\") (backend . \"app/backend.rkt\") (module . \"backend\") (entry . \"start\") (protocol . 1) (macos-localizations . (\"zh_Hans\")))"
out)))
(check-exn #rx"BCP 47"
(lambda () (load-project bad-loc-root)))

(define missing-project
(rivet-project temp-root #hasheq((resources . ("missing.txt")))))
Expand Down
Loading