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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ CloudMachine is an independent project, not affiliated with Apple or Google
- About 200 GB of free disk: the machine's disk file grows with the upload
buffer.

The menu-bar app, CLI output and notifications follow the system language:
Polish when Polish is the first preferred language, English otherwise.
`CM_LANGUAGE=en` or `CM_LANGUAGE=pl` overrides it. Logs are always English,
The menu-bar app, CLI output and notifications follow the system language, in
22 languages: Arabic, Chinese (Simplified and Traditional), Danish, Dutch,
English, Finnish, French, German, Hebrew, Italian, Japanese, Korean, Malay,
Norwegian Bokmål, Polish, Portuguese (Brazil), Russian, Spanish, Swedish, Thai
and Turkish; English for any other. `CM_LANGUAGE` with a language code (`en`,
`de`, `zh-Hant`...) overrides it. Logs are always English,
so they read the same whoever sends them to you.

### Current limitations
Expand Down
32 changes: 32 additions & 0 deletions mac-app/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,38 @@
<string>__CM_COMMIT__</string>
<key>CMGitDirty</key>
<string>__CM_DIRTY__</string>
<!--
The languages L10n translates into (L10n.Language). Declaring them makes
macOS use the same language for its own parts of the app, and mirror the
window for Arabic and Hebrew.
-->
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleLocalizations</key>
<array>
<string>ar</string>
<string>da</string>
<string>de</string>
<string>en</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>he</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>ms</string>
<string>nb</string>
<string>nl</string>
<string>pl</string>
<string>pt-BR</string>
<string>ru</string>
<string>sv</string>
<string>th</string>
<string>tr</string>
<string>zh-Hans</string>
<string>zh-Hant</string>
</array>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
Expand Down
8 changes: 8 additions & 0 deletions mac-app/Sources/CloudMachineApp/CloudMachineApp.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CloudMachineCore
import SwiftUI

@main
Expand All @@ -9,18 +10,25 @@ struct CloudMachineApp: App {
MenuBarContentView()
.environmentObject(controller)
.preferredColorScheme(.dark)
.environment(\.layoutDirection, layoutDirection)
}
.menuBarExtraStyle(.window)

Window("CloudMachine", id: "dashboard") {
DashboardView()
.environmentObject(controller)
.preferredColorScheme(.dark)
.environment(\.layoutDirection, layoutDirection)
.frame(minWidth: 820, minHeight: 600)
}
// No title bar: the sidebar runs up under the traffic lights, as in the
// RenaCode window design.
.windowStyle(.hiddenTitleBar)
.defaultSize(width: 980, height: 760)
}

// Follows L10n rather than the bundle, so CM_LANGUAGE=ar mirrors it too.
private var layoutDirection: LayoutDirection {
L10n.language.isRightToLeft ? .rightToLeft : .leftToRight
}
}
81 changes: 68 additions & 13 deletions mac-app/Sources/CloudMachineCore/L10n.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,32 @@ import Foundation
///
/// The English text is the key, so the source reads naturally and a missing
/// translation falls back to English instead of to an identifier. Polish
/// translations live in the `L10nPolish+*.swift` tables. Logs are NOT
/// localized: a log is read when diagnosing, often by someone else, and a file
/// that switches language with the system setting cannot be searched.
/// translations live in the `L10nPolish+*.swift` tables, the other languages
/// in `L10nTranslations+<code>.swift`. Logs are NOT localized: a log is read
/// when diagnosing, often by someone else, and a file that switches language
/// with the system setting cannot be searched.
///
/// The language follows the system (first preferred language). `CM_LANGUAGE`
/// (`en` or `pl`) overrides it; tests always run in English, so their expected
/// The language follows the system's preferred languages, matched the way
/// macOS matches them for a localized app. `CM_LANGUAGE` (any code from
/// `Language`) overrides it; tests always run in English, so their expected
/// strings do not depend on the Mac they run on.
///
/// Rules, enforced by `L10nTests`:
/// - the key is a single-line string literal written inline in the call;
/// - placeholders are `%@` only (pass numbers as `"\(n)"`); `String(format:)`
/// with a wrong argument type crashes, and `%@` cannot be mismatched;
/// - every key has a Polish entry with the same number of placeholders.
/// - every key has an entry in every language with the same placeholders
/// (`%1$@`... when a language needs another order).
public enum L10n {
public enum Language: String {
case en
case pl
/// The 22 languages of the RenaCode games; also `CFBundleLocalizations`.
public enum Language: String, CaseIterable {
case ar, da, de, en, es, fi, fr, he, it, ja, ko, ms, nb, nl, pl
case ptBR = "pt-BR"
case ru, sv, th, tr
case zhHans = "zh-Hans"
case zhHant = "zh-Hant"

public var isRightToLeft: Bool { self == .ar || self == .he }
}

/// Settable for tests and previews; resolved once otherwise.
Expand All @@ -31,20 +40,63 @@ public enum L10n {
preferredLanguages: [String] = Locale.preferredLanguages,
isRunningTests: Bool = NSClassFromString("XCTestCase") != nil
) -> Language {
if let forced = environment["CM_LANGUAGE"].flatMap({ Language(rawValue: $0.lowercased()) }) {
return forced
if let forced = environment["CM_LANGUAGE"], let language = match([forced]) {
return language
}
if isRunningTests { return .en }
return preferredLanguages.first?.lowercased().hasPrefix("pl") == true ? .pl : .en
return match(preferredLanguages) ?? .en
}

/// The first of `preferences` the app has, matched by Foundation (zh-TW ->
/// zh-Hant, pt-PT -> pt-BR, no -> nb). Foundation answers "en" when nothing
/// matches, so English counts only when the preference itself is English.
private static func match(_ preferences: [String]) -> Language? {
let codes = Language.allCases.map(\.rawValue)
for preference in preferences {
let identifier = preference.replacingOccurrences(of: "_", with: "-")
guard
let found = Bundle.preferredLocalizations(from: codes, forPreferences: [identifier]).first
.flatMap(Language.init(rawValue:))
else { continue }
if found != .en || Locale(identifier: identifier).language.languageCode == .english {
return found
}
}
return nil
}

/// Translates `english`; with arguments, fills its `%@` placeholders.
public static func tr(_ english: String, _ arguments: String...) -> String {
let template = language == .pl ? (polish[english] ?? english) : english
let template = translations[language]?[english] ?? english
guard !arguments.isEmpty else { return template }
return String(format: template, arguments: arguments.map { $0 as NSString })
}

/// Every language except English (the keys themselves).
static let translations: [Language: [String: String]] = [
.pl: polish,
.ar: L10nTranslations.ar,
.da: L10nTranslations.da,
.de: L10nTranslations.de,
.es: L10nTranslations.es,
.fi: L10nTranslations.fi,
.fr: L10nTranslations.fr,
.he: L10nTranslations.he,
.it: L10nTranslations.it,
.ja: L10nTranslations.ja,
.ko: L10nTranslations.ko,
.ms: L10nTranslations.ms,
.nb: L10nTranslations.nb,
.nl: L10nTranslations.nl,
.ptBR: L10nTranslations.ptBR,
.ru: L10nTranslations.ru,
.sv: L10nTranslations.sv,
.th: L10nTranslations.th,
.tr: L10nTranslations.tr,
.zhHans: L10nTranslations.zhHans,
.zhHant: L10nTranslations.zhHant,
]

/// All Polish tables merged. Split into files so that parts of the code can
/// be translated in parallel without editing one shared dictionary.
static let polish: [String: String] = {
Expand All @@ -68,3 +120,6 @@ public enum L10n {

/// Namespace for the Polish tables; each `L10nPolish+*.swift` adds one.
enum L10nPolish {}

/// Namespace for the other languages; `L10nTranslations+<code>.swift` each.
enum L10nTranslations {}
Loading
Loading