Skip to content
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Added an in-app language setting with Auto, English, and Simplified Chinese options.
- Localized the main app UI, menu bar status text, local insight notifications, recovery guidance, usage reports, diagnostics, support bundles, and social share copy/card labels.

## v0.1.11

- Added a Simplified Chinese README and language switch links between English and Chinese docs.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Sub2API Status Bar is a macOS menu bar companion for Sub2API users. It keeps dai
## Highlights

- Native macOS menu bar app with a compact SwiftUI popover
- In-app language setting with Auto, English, and Simplified Chinese options
- Usage Insights that turn balance, quotas, monthly budget, spend trend, usage trend, model concentration, and latency into prioritized signals
- Local proactive alerts for warning/error insights, with severity and quiet-period controls
- Stale-data guardrail so the menu bar and local alerts warn when the last successful refresh is too old
Expand Down Expand Up @@ -81,6 +82,7 @@ To switch accounts or remove saved credentials, open Settings and choose **Disco

Settings also includes:

- **Language** to use Auto, English, or Simplified Chinese for app UI, menu bar status, local alerts, reports, diagnostics, support bundles, and share cards
- **Show text in menu bar** for a compact always-visible usage summary
- **Metric** to choose whether the menu bar emphasizes Auto, Spend, Balance, Quota, Tokens, or Requests
- **Notify on insights** to receive local macOS alerts when important usage signals cross the configured level
Expand Down
2 changes: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Sub2API Status Bar 是一款面向 Sub2API 用户的 macOS 菜单栏助手。它
## 亮点

- 原生 macOS 菜单栏应用,带紧凑的 SwiftUI 弹窗
- 应用内语言设置,支持 Auto、English 和简体中文
- Usage Insights 会把余额、额度、月度预算、花费趋势、用量趋势、模型成本集中度和延迟转成优先级信号
- 本地主动提醒,支持 warning/error 级别和静默间隔设置
- 数据过期保护:最近一次刷新太久时,菜单栏和本地提醒会提示数据已陈旧
Expand Down Expand Up @@ -81,6 +82,7 @@ swift run Sub2APIStatusBar

Settings 还包含:

- **Language**:为应用 UI、菜单栏状态、本地提醒、报告、诊断、支持包和分享卡片选择 Auto、English 或简体中文
- **Show text in menu bar**:在菜单栏显示紧凑用量摘要
- **Metric**:选择菜单栏优先展示 Auto、Spend、Balance、Quota、Tokens 或 Requests
- **Notify on insights**:重要用量信号达到设定级别时发送本地 macOS 通知
Expand Down
6 changes: 5 additions & 1 deletion Sources/Sub2APIStatusBar/ChartViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import Sub2APIStatusCore

struct ModelDistributionView: View {
let models: [ModelUsageSummary]
var language: AppLanguage = .en

private var strings: AppStrings { AppStrings(language: language) }

private var visibleModels: [ModelUsageDisplay] {
ModelUsageDisplay.make(models)
}

var body: some View {
SectionBlock(title: "Model Distribution") {
SectionBlock(title: strings.text(en: "Model Distribution", zhHans: "模型分布")) {
VStack(spacing: 0) {
ForEach(visibleModels) { item in
VStack(spacing: 8) {
Expand Down Expand Up @@ -55,6 +58,7 @@ struct ModelDistributionView: View {

struct UsageTrendSection: View {
let state: UsageTrendDisplayState
var language: AppLanguage = .en
@State private var selectedMode: UsageTrendMode = .tokens

var body: some View {
Expand Down
8 changes: 6 additions & 2 deletions Sources/Sub2APIStatusBar/DashboardComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct MetricItem: Identifiable {

struct UserAccountCard: View {
let user: CurrentUser
var language: AppLanguage = .en

private var displayName: String {
guard let username = user.username, !username.isEmpty else {
Expand Down Expand Up @@ -52,7 +53,7 @@ struct UserAccountCard: View {

if let status = user.status, !status.isEmpty {
StatusPill(
text: status.capitalized,
text: status.lowercased() == "active" ? AppStrings(language: language).text(en: "Active", zhHans: "活跃") : status.capitalized,
color: status.lowercased() == "active" ? .green : .secondary
)
}
Expand Down Expand Up @@ -127,11 +128,14 @@ private struct SafeSystemImage: View {

struct UsageInsightsView: View {
let insights: UsageInsights
var language: AppLanguage = .en

private var strings: AppStrings { AppStrings(language: language) }

var body: some View {
VStack(alignment: .leading, spacing: 10) {
HStack(alignment: .firstTextBaseline) {
Label("Usage Insights", systemImage: "sparkles")
Label(strings.usageInsights, systemImage: "sparkles")
.font(.system(size: 14, weight: .semibold))
Spacer()
Text(insights.headline)
Expand Down
6 changes: 4 additions & 2 deletions Sources/Sub2APIStatusBar/InsightNotifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ final class InsightNotifier {
_ snapshot: MonitorSnapshot,
refreshIntervalSeconds: Double,
settings: InsightAlertSettings,
now: Date = Date()
now: Date = Date(),
language: AppLanguage = .en
) {
let policy = InsightAlertPolicy(settings: settings)
guard let alert = policy.staleDataAlert(
from: snapshot,
refreshIntervalSeconds: refreshIntervalSeconds,
lastAlertedAtByFingerprint: lastAlertedAtByFingerprint,
now: now
now: now,
language: language
) else {
return
}
Expand Down
36 changes: 21 additions & 15 deletions Sources/Sub2APIStatusBar/LoginViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ struct LoginPanel: View {
@ObservedObject var model: MonitorViewModel
@FocusState private var focusedField: FocusedLoginField?

private var strings: AppStrings { AppStrings(language: model.settingsDraft.language) }

private var formState: LoginFormState {
LoginFormState(
baseURL: model.settingsDraft.baseURL,
Expand All @@ -23,7 +25,7 @@ struct LoginPanel: View {
VStack(alignment: .leading, spacing: 3) {
Text("Sub2API")
.font(.title2.bold())
Text("Connect your server")
Text(strings.connectYourServer)
.font(.callout)
.foregroundStyle(.secondary)
}
Expand All @@ -33,23 +35,23 @@ struct LoginPanel: View {
AccountListSection(model: model)
}

OnboardingChecklistView(checklist: checklist)
OnboardingChecklistView(checklist: checklist, language: model.settingsDraft.language)

VStack(alignment: .leading, spacing: 12) {
TextField("Server URL", text: $model.settingsDraft.baseURL)
TextField(strings.serverURL, text: $model.settingsDraft.baseURL)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .serverURL)

TextField("Account", text: $model.loginEmail)
TextField(strings.account, text: $model.loginEmail)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .email)

SecureField("Password", text: $model.loginPassword)
SecureField(strings.text(en: "Password", zhHans: "密码"), text: $model.loginPassword)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .password)

HStack {
Text("Refresh")
Text(strings.refresh)
.font(.callout)
.foregroundStyle(.secondary)
Slider(value: $model.settingsDraft.refreshIntervalSeconds, in: 5...300, step: 5)
Expand Down Expand Up @@ -77,7 +79,7 @@ struct LoginPanel: View {
} else {
Image(systemName: "key.fill")
}
Text(model.isLoggingIn ? "Connecting..." : "Login")
Text(model.isLoggingIn ? strings.connecting : strings.login)
}
.frame(maxWidth: .infinity)
}
Expand All @@ -88,9 +90,9 @@ struct LoginPanel: View {
Divider()

VStack(alignment: .leading, spacing: 8) {
Text("Manual token")
Text(strings.manualToken)
.font(.headline)
SecureField("Bearer Token", text: $model.settingsDraft.authToken)
SecureField(strings.bearerToken, text: $model.settingsDraft.authToken)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .token)
Button {
Expand All @@ -102,7 +104,7 @@ struct LoginPanel: View {
)
model.saveSettings()
} label: {
Label("Save Token", systemImage: "square.and.arrow.down")
Label(strings.saveToken, systemImage: "square.and.arrow.down")
}
.disabled(model.settingsDraft.authToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
Expand All @@ -113,7 +115,7 @@ struct LoginPanel: View {
Button {
model.openURL(model.settingsDraft.baseURL)
} label: {
Label("Open Server", systemImage: "safari")
Label(strings.openServer, systemImage: "safari")
}
.disabled(model.settingsDraft.baseURL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)

Expand All @@ -122,7 +124,7 @@ struct LoginPanel: View {
Button {
model.quit()
} label: {
Label("Quit", systemImage: "power")
Label(strings.quit, systemImage: "power")
}
}
.buttonStyle(.borderless)
Expand All @@ -139,7 +141,8 @@ struct LoginPanel: View {
private var checklist: OnboardingChecklist {
OnboardingChecklist.make(
form: formState,
manualToken: model.settingsDraft.authToken
manualToken: model.settingsDraft.authToken,
language: model.settingsDraft.language
)
}

Expand All @@ -160,11 +163,14 @@ struct LoginPanel: View {

struct OnboardingChecklistView: View {
let checklist: OnboardingChecklist
var language: AppLanguage = .en

private var strings: AppStrings { AppStrings(language: language) }

var body: some View {
VStack(alignment: .leading, spacing: 9) {
HStack {
Text("Connection Checklist")
Text(strings.connectionChecklist)
.font(.headline)
Spacer()
Text(checklist.summary)
Expand Down Expand Up @@ -210,7 +216,7 @@ struct AccountListSection: View {

var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Accounts")
Text(AppStrings(language: model.settingsDraft.language).accounts)
.font(.headline)

ForEach(model.config.accounts) { account in
Expand Down
Loading