diff --git a/apps/backend/cmd/worker/main.go b/apps/backend/cmd/worker/main.go index 9f37f48..c082764 100644 --- a/apps/backend/cmd/worker/main.go +++ b/apps/backend/cmd/worker/main.go @@ -494,6 +494,10 @@ func runScoutScan(ctx context.Context, jobs *jobUC.Service, scout *scoutUC.Servi if err := json.Unmarshal([]byte(j.Payload), &payload); err != nil { return fmt.Errorf("invalid scout scan payload: %w", err) } + if payload.RunID == "" { + // 部署前排入的舊 job 只存平面 RunBrief;run id 一律等於 Job.RefID + payload.RunID = j.RefID + } if payload.RunID == "" || payload.RunID != j.RefID { return fmt.Errorf("scout run/job reference mismatch") } @@ -530,8 +534,9 @@ func runScoutScan(ctx context.Context, jobs *jobUC.Service, scout *scoutUC.Servi _ = scout.FailRun(ctx, j.OwnerUID, payload.RunID, err.Error()) return err } + // 已發佈即成功;進度回報失敗只記 log,不可把 job 標為失敗 if _, err := jobs.MarkRunningProgress(ctx, j.ID, 90, fmt.Sprintf("海巡 · 已發佈 %d 筆候選", len(posts))); err != nil { - return err + logx.Errorf("scout scan %s progress after publish: %v", j.ID, err) } _, err = jobs.SucceedJob(ctx, j.ID, fmt.Sprintf("海巡完成 · 命中 %d 筆", len(posts))) return err diff --git a/apps/backend/crawler/src/server.ts b/apps/backend/crawler/src/server.ts index e4f27b5..5f29d3a 100644 --- a/apps/backend/crawler/src/server.ts +++ b/apps/backend/crawler/src/server.ts @@ -66,7 +66,8 @@ function textMatchesQuery(text: string, query: string): boolean { if (anchors.length > 0) { return anchors.some((a) => body.includes(a)); } - return true; + // 查詢過短無法斷詞時,退回要求正文含原始查詢字串,避免放行整條推薦流 + return body.includes(q.replace(/\s+/g, "").toLowerCase()); } function parsePublishedFromCardText(text: string): { iso?: string; label?: string } { @@ -243,31 +244,35 @@ async function search(storageState: string, terms: string[], limit: number): Pro const perTrack = Math.min(Math.max(limit, 10), 24); const pageTop = await context.newPage(); const pageRecent = await context.newPage(); - let top: Post[] = []; - let recent: Post[] = []; - try { - [top, recent] = await Promise.all([ - searchTrack(pageTop, query, "top", perTrack), - searchTrack(pageRecent, query, "recent", perTrack), - ]); - } catch (e) { - // 一軌失敗仍用另一軌 - if (top.length === 0) { - try { - top = await searchTrack(pageTop, query, "top", perTrack); - } catch { - /* keep empty */ - } + // allSettled 保留成功軌結果;失敗軌各自重試一次(session 失效重試無意義) + const isSessionError = (r: unknown) => r instanceof Error && r.message.includes("session"); + const [topResult, recentResult] = await Promise.allSettled([ + searchTrack(pageTop, query, "top", perTrack), + searchTrack(pageRecent, query, "recent", perTrack), + ]); + let top: Post[] = topResult.status === "fulfilled" ? topResult.value : []; + let recent: Post[] = recentResult.status === "fulfilled" ? recentResult.value : []; + const firstFailure = + topResult.status === "rejected" + ? topResult.reason + : recentResult.status === "rejected" + ? recentResult.reason + : undefined; + if (topResult.status === "rejected" && !isSessionError(topResult.reason)) { + try { + top = await searchTrack(pageTop, query, "top", perTrack); + } catch { + /* keep empty */ } - if (recent.length === 0 && !(e instanceof Error && e.message.includes("session"))) { - try { - recent = await searchTrack(pageRecent, query, "recent", perTrack); - } catch { - /* keep empty */ - } - } - if (top.length === 0 && recent.length === 0) throw e; } + if (recentResult.status === "rejected" && !isSessionError(recentResult.reason)) { + try { + recent = await searchTrack(pageRecent, query, "recent", perTrack); + } catch { + /* keep empty */ + } + } + if (firstFailure !== undefined && top.length === 0 && recent.length === 0) throw firstFailure; await pageTop.close().catch(() => undefined); await pageRecent.close().catch(() => undefined); const merged = mergeRecentPrimary(top, recent, query, limit); diff --git a/apps/backend/generate/api/crm.api b/apps/backend/generate/api/crm.api index 2788f6d..f322653 100644 --- a/apps/backend/generate/api/crm.api +++ b/apps/backend/generate/api/crm.api @@ -223,6 +223,9 @@ type ( Terms []TermConversionStat `json:"terms"` Variants []VariantConversionStat `json:"variants"` Sources []SourceConversionStat `json:"sources"` + // UnavailableDimensions 列出「尚未實作」而非「查無資料」的維度, + // 前端才不會把功能缺口誤顯示為使用者還沒有數據。 + UnavailableDimensions []string `json:"unavailable_dimensions"` } ) diff --git a/apps/backend/generate/database/mongo/000019_radar_sweep_job_dedupe.down.json b/apps/backend/generate/database/mongo/000019_radar_sweep_job_dedupe.down.json new file mode 100644 index 0000000..b4be03b --- /dev/null +++ b/apps/backend/generate/database/mongo/000019_radar_sweep_job_dedupe.down.json @@ -0,0 +1,3 @@ +[ + { "dropIndexes": "jobs", "index": "radar_sweep_job_ref_unique" } +] diff --git a/apps/backend/generate/database/mongo/000019_radar_sweep_job_dedupe.up.json b/apps/backend/generate/database/mongo/000019_radar_sweep_job_dedupe.up.json new file mode 100644 index 0000000..d3ce822 --- /dev/null +++ b/apps/backend/generate/database/mongo/000019_radar_sweep_job_dedupe.up.json @@ -0,0 +1,13 @@ +[ + { + "createIndexes": "jobs", + "indexes": [ + { + "key": { "owner_uid": 1, "template_type": 1, "ref_id": 1 }, + "name": "radar_sweep_job_ref_unique", + "unique": true, + "partialFilterExpression": { "template_type": "radar_sweep" } + } + ] + } +] diff --git a/apps/backend/internal/logic/crm/get_crm_stats_logic.go b/apps/backend/internal/logic/crm/get_crm_stats_logic.go index 0cd79f8..0cf9a78 100644 --- a/apps/backend/internal/logic/crm/get_crm_stats_logic.go +++ b/apps/backend/internal/logic/crm/get_crm_stats_logic.go @@ -3,6 +3,7 @@ package crm import ( "context" + crmUC "apps/backend/internal/module/crm/usecase" "apps/backend/internal/svc" "apps/backend/internal/types" @@ -28,15 +29,28 @@ func (l *GetCrmStatsLogic) GetCrmStats(req *types.CrmStatsReq) (*types.CrmStatsD if err != nil { return nil, err } - won := 0 - if v, ok := stats["won"].(int); ok { - won = v + terms := make([]types.TermConversionStat, 0, len(stats.Terms)) + for _, s := range stats.Terms { + row := types.TermConversionStat{ + Term: s.Key, Accepted: s.Accepted, Replied: s.Replied, Won: s.Won, + InsufficientSample: s.Accepted < crmUC.StatsMinSample, + } + // 樣本不足只給絕對數,不給會被過度解讀的比率(spec §9.5) + if !row.InsufficientSample { + row.ConversionRate = float64(s.Won) / float64(s.Accepted) + } + terms = append(terms, row) + } + sources := make([]types.SourceConversionStat, 0, len(stats.Sources)) + for _, s := range stats.Sources { + sources = append(sources, types.SourceConversionStat{ + Source: s.Key, Won: s.Won, InsufficientSample: s.Won < crmUC.StatsMinSample, + }) } return &types.CrmStatsData{ - Terms: []types.TermConversionStat{}, - Variants: []types.VariantConversionStat{}, - Sources: []types.SourceConversionStat{ - {Source: "radar", Won: won, InsufficientSample: won < 5}, - }, + Terms: terms, + Variants: []types.VariantConversionStat{}, + Sources: sources, + UnavailableDimensions: stats.UnavailableDimensions, }, nil } diff --git a/apps/backend/internal/logic/crm/list_follow_ups_logic.go b/apps/backend/internal/logic/crm/list_follow_ups_logic.go index b837f10..29d0b07 100644 --- a/apps/backend/internal/logic/crm/list_follow_ups_logic.go +++ b/apps/backend/internal/logic/crm/list_follow_ups_logic.go @@ -33,17 +33,24 @@ func (l *ListFollowUpsLogic) ListFollowUps(req *types.ListFollowUpsReq) (*types. return nil, err } out := make([]types.FollowUpPublic, 0, len(list)) + // 同一聯絡人常有多筆追蹤;快取避免整頁重複查同一筆 + briefs := make(map[string]types.ContactBrief, len(list)) for _, f := range list { p := crmmap.FollowUp(f) if p == nil { continue } - if c, cerr := l.svcCtx.Crm.GetContactOnly(l.ctx, uid, f.ContactID); cerr == nil && c != nil { - p.Contact = types.ContactBrief{ - Id: c.ID, SourcePlatform: c.SourcePlatform, AuthorHandle: c.AuthorHandle, - DisplayName: c.DisplayName, Stage: c.Stage, + brief, cached := briefs[f.ContactID] + if !cached { + if c, cerr := l.svcCtx.Crm.GetContactOnly(l.ctx, uid, f.ContactID); cerr == nil && c != nil { + brief = types.ContactBrief{ + Id: c.ID, SourcePlatform: c.SourcePlatform, AuthorHandle: c.AuthorHandle, + DisplayName: c.DisplayName, Stage: c.Stage, + } } + briefs[f.ContactID] = brief } + p.Contact = brief out = append(out, *p) } return &types.FollowUpListData{List: out, Pagination: crmmap.Pagination(req.Page, req.PageSize, total)}, nil diff --git a/apps/backend/internal/logic/crmmap/map.go b/apps/backend/internal/logic/crmmap/map.go index 1eb0ac7..a5b118d 100644 --- a/apps/backend/internal/logic/crmmap/map.go +++ b/apps/backend/internal/logic/crmmap/map.go @@ -65,16 +65,6 @@ func FollowUp(f *domain.FollowUp) *types.FollowUpPublic { } } -func FollowUpList(list []*domain.FollowUp) []types.FollowUpPublic { - out := make([]types.FollowUpPublic, 0, len(list)) - for _, f := range list { - if p := FollowUp(f); p != nil { - out = append(out, *p) - } - } - return out -} - func StageCounts(m map[string]int) []types.StageCount { out := make([]types.StageCount, 0, len(m)) for k, v := range m { diff --git a/apps/backend/internal/logic/growthmap/map.go b/apps/backend/internal/logic/growthmap/map.go index e492fe4..a8d690d 100644 --- a/apps/backend/internal/logic/growthmap/map.go +++ b/apps/backend/internal/logic/growthmap/map.go @@ -99,16 +99,6 @@ func Health(h *domain.AccountHealth) *types.AccountHealthPublic { } } -func HealthList(list []*domain.AccountHealth) []types.AccountHealthPublic { - out := make([]types.AccountHealthPublic, 0, len(list)) - for _, h := range list { - if p := Health(h); p != nil { - out = append(out, *p) - } - } - return out -} - func Workspace(w *domain.Workspace) *types.WorkspacePublic { if w == nil { return nil diff --git a/apps/backend/internal/module/crm/domain/contact.go b/apps/backend/internal/module/crm/domain/contact.go index 2fa3cf4..898b8fc 100644 --- a/apps/backend/internal/module/crm/domain/contact.go +++ b/apps/backend/internal/module/crm/domain/contact.go @@ -34,6 +34,8 @@ const ( FollowUpEscalated = "escalated" DefaultFollowUpDays = 3 + // MaxFollowUpNotifications:通知達此次數仍無動作即 escalated(spec FU-03)。 + MaxFollowUpNotifications = 2 ) type Contact struct { diff --git a/apps/backend/internal/module/crm/repository/memory.go b/apps/backend/internal/module/crm/repository/memory.go index 9f1be44..fabbd00 100644 --- a/apps/backend/internal/module/crm/repository/memory.go +++ b/apps/backend/internal/module/crm/repository/memory.go @@ -304,7 +304,14 @@ func (m *Memory) ListDueFollowUps(_ context.Context, now int64, limit int) ([]*d defer m.mu.Unlock() out := make([]*domain.FollowUp, 0) for _, x := range m.followups { - if (x.Status == domain.FollowUpScheduled || x.Status == domain.FollowUpSnoozed) && x.DueAt <= now { + // notified 也要再掃:第二次通知(進而 escalated)靠的是它下一次到期。 + // snoozed 已不再寫入,保留以相容既有資料。 + switch x.Status { + case domain.FollowUpScheduled, domain.FollowUpSnoozed, domain.FollowUpNotified: + default: + continue + } + if x.DueAt <= now { cp := *x out = append(out, &cp) } diff --git a/apps/backend/internal/module/crm/repository/mongo.go b/apps/backend/internal/module/crm/repository/mongo.go index cea9477..396e333 100644 --- a/apps/backend/internal/module/crm/repository/mongo.go +++ b/apps/backend/internal/module/crm/repository/mongo.go @@ -281,8 +281,12 @@ func (s *MonStore) ListDueFollowUps(ctx context.Context, now int64, limit int) ( limit = 50 } var list []*domain.FollowUp + // notified 也要再掃:第二次通知(進而 escalated)靠的是它下一次到期。 + // snoozed 已不再寫入,保留以相容既有資料。 err := s.followups.Find(ctx, &list, bson.M{ - "status": bson.M{"$in": []string{domain.FollowUpScheduled, domain.FollowUpSnoozed}}, + "status": bson.M{"$in": []string{ + domain.FollowUpScheduled, domain.FollowUpSnoozed, domain.FollowUpNotified, + }}, "due_at": bson.M{"$lte": now}, }, options.Find().SetLimit(int64(limit))) return list, err diff --git a/apps/backend/internal/module/crm/usecase/followup_scan_test.go b/apps/backend/internal/module/crm/usecase/followup_scan_test.go new file mode 100644 index 0000000..53c92fe --- /dev/null +++ b/apps/backend/internal/module/crm/usecase/followup_scan_test.go @@ -0,0 +1,114 @@ +package usecase + +import ( + "context" + "testing" + "time" + + "apps/backend/internal/module/crm/domain" + "apps/backend/internal/module/crm/repository" +) + +const day = int64(24 * time.Hour) + +func seedFollowUp(t *testing.T, repo domain.Repository, days int) (*domain.Contact, *domain.FollowUp) { + t.Helper() + ctx := context.Background() + c, err := repo.UpsertContactByIdentity(ctx, &domain.Contact{ + OwnerUID: 7, AuthorHandle: "buyer", FollowUpDays: days, + }) + if err != nil { + t.Fatal(err) + } + f := &domain.FollowUp{ + ID: domain.NewID(), OwnerUID: 7, ContactID: c.ID, + DueAt: 1, Status: domain.FollowUpScheduled, + CreatedAt: 1, UpdatedAt: 1, + } + if err := repo.SaveFollowUp(ctx, f); err != nil { + t.Fatal(err) + } + return c, f +} + +// FU-01/FU-03:第一次通知後要再等一個間隔才第二次通知,第二次之後才 escalated。 +func TestScanFollowUpsNotifiesTwiceThenEscalates(t *testing.T) { + ctx := context.Background() + repo := repository.NewMemory() + svc := New(repo) + _, f := seedFollowUp(t, repo, 3) + + now := int64(10 * day) + if n, err := svc.ScanFollowUps(ctx, now); err != nil || n != 1 { + t.Fatalf("first scan n=%d err=%v", n, err) + } + first, err := repo.GetFollowUp(ctx, f.ID) + if err != nil || first.Status != domain.FollowUpNotified || first.NotifiedCount != 1 { + t.Fatalf("after first scan=%+v err=%v", first, err) + } + if first.DueAt != now+3*day { + t.Fatalf("first notify must push due_at by the contact interval, got %d", first.DueAt) + } + + // 還沒到下一次到期:不可重複通知 + if n, err := svc.ScanFollowUps(ctx, now+day); err != nil || n != 0 { + t.Fatalf("premature rescan n=%d err=%v", n, err) + } + + if n, err := svc.ScanFollowUps(ctx, now+3*day); err != nil || n != 1 { + t.Fatalf("second scan n=%d err=%v", n, err) + } + second, err := repo.GetFollowUp(ctx, f.ID) + if err != nil || second.Status != domain.FollowUpEscalated || second.NotifiedCount != 2 { + t.Fatalf("after second scan=%+v err=%v", second, err) + } + + // escalated 是終點:不再被掃到 + if n, err := svc.ScanFollowUps(ctx, now+30*day); err != nil || n != 0 { + t.Fatalf("escalated must stop scanning n=%d err=%v", n, err) + } +} + +// FU-04:延後把到期日後移並回到 scheduled。 +func TestSnoozeReturnsToScheduled(t *testing.T) { + ctx := context.Background() + repo := repository.NewMemory() + svc := New(repo) + _, f := seedFollowUp(t, repo, 3) + if _, err := svc.ScanFollowUps(ctx, 10*day); err != nil { + t.Fatal(err) + } + + before := domain.NowNano() + got, err := svc.SnoozeFollowUp(ctx, 7, f.ID, 5) + if err != nil { + t.Fatal(err) + } + if got.Status != domain.FollowUpScheduled { + t.Fatalf("snooze status = %s, want scheduled", got.Status) + } + if got.DueAt < before+5*day { + t.Fatalf("snooze must push due_at at least 5 days out, got %d", got.DueAt) + } +} + +func TestScanFollowUpsFallsBackToDefaultInterval(t *testing.T) { + ctx := context.Background() + repo := repository.NewMemory() + svc := New(repo) + f := &domain.FollowUp{ + ID: domain.NewID(), OwnerUID: 7, ContactID: "missing-contact", + DueAt: 1, Status: domain.FollowUpScheduled, CreatedAt: 1, UpdatedAt: 1, + } + if err := repo.SaveFollowUp(ctx, f); err != nil { + t.Fatal(err) + } + now := int64(10 * day) + if _, err := svc.ScanFollowUps(ctx, now); err != nil { + t.Fatal(err) + } + got, err := repo.GetFollowUp(ctx, f.ID) + if err != nil || got.DueAt != now+int64(domain.DefaultFollowUpDays)*day { + t.Fatalf("missing contact should use default interval, got %+v err=%v", got, err) + } +} diff --git a/apps/backend/internal/module/crm/usecase/service.go b/apps/backend/internal/module/crm/usecase/service.go index 50bfbf6..13c9855 100644 --- a/apps/backend/internal/module/crm/usecase/service.go +++ b/apps/backend/internal/module/crm/usecase/service.go @@ -20,9 +20,10 @@ type GrowthOutcomes interface { type Service struct { Repo domain.Repository Growth GrowthOutcomes - // RadarOpps optional for contact detail briefs + // RadarOpps optional for contact detail briefs and conversion attribution RadarOpps interface { GetOpportunity(ctx context.Context, id string) (*radarDomain.Opportunity, error) + ListOpportunities(ctx context.Context, ownerUID int64, f radarDomain.OpportunityListFilter) ([]*radarDomain.Opportunity, int64, error) } Notifier FollowUpNotifier } @@ -382,7 +383,8 @@ func (s *Service) SnoozeFollowUp(ctx context.Context, ownerUID int64, id string, if f.OwnerUID != ownerUID { return nil, domain.ErrForbidden } - f.Status = domain.FollowUpSnoozed + // spec FU-04:延後只是把到期日後移,狀態回到可再次排程的 scheduled。 + f.Status = domain.FollowUpScheduled f.DueAt = domain.NowNano() + int64(days)*int64(24*time.Hour) f.UpdatedAt = domain.NowNano() if err := s.Repo.SaveFollowUp(ctx, f); err != nil { @@ -428,11 +430,15 @@ func (s *Service) ScanFollowUps(ctx context.Context, now int64) (int, error) { } n := 0 for _, f := range due { - f.Status = domain.FollowUpNotified f.NotifiedCount++ f.UpdatedAt = now - if f.NotifiedCount >= 2 { + if f.NotifiedCount >= domain.MaxFollowUpNotifications { + // 達上限只建議轉未成交,不再排下一次通知(spec FU-03) f.Status = domain.FollowUpEscalated + } else { + // 到期日必須往後推,否則下一個 tick 會立刻重複通知同一筆 + f.Status = domain.FollowUpNotified + f.DueAt = now + int64(s.followUpDays(ctx, f.ContactID))*int64(24*time.Hour) } if err := s.Repo.SaveFollowUp(ctx, f); err != nil { continue @@ -445,24 +451,13 @@ func (s *Service) ScanFollowUps(ctx context.Context, now int64) (int, error) { return n, nil } -// Stats returns three-dimension CRM stats for the range. -func (s *Service) Stats(ctx context.Context, ownerUID int64, from, to int64) (map[string]any, error) { - counts, err := s.Repo.CountByStage(ctx, ownerUID) - if err != nil { - return nil, err +// followUpDays resolves the owner's configured interval, falling back to the +// default when the contact is gone or never had one set. +func (s *Service) followUpDays(ctx context.Context, contactID string) int { + c, err := s.Repo.GetContact(ctx, contactID) + if err != nil || c == nil || c.FollowUpDays <= 0 { + return domain.DefaultFollowUpDays } - list, total, err := s.Repo.ListContacts(ctx, ownerUID, domain.ContactListFilter{Page: 1, PageSize: 500}) - if err != nil { - return nil, err - } - won := counts[domain.StageWon] - _ = from - _ = to - return map[string]any{ - "total_contacts": total, - "by_stage": counts, - "won": won, - "follow_up": counts["needs_follow_up"], - "sample": len(list), - }, nil + return c.FollowUpDays } + diff --git a/apps/backend/internal/module/crm/usecase/stats.go b/apps/backend/internal/module/crm/usecase/stats.go new file mode 100644 index 0000000..48b69ff --- /dev/null +++ b/apps/backend/internal/module/crm/usecase/stats.go @@ -0,0 +1,234 @@ +package usecase + +import ( + "context" + "sort" + + "apps/backend/internal/module/crm/domain" + radarDomain "apps/backend/internal/module/radar/domain" +) + +// StatsMinSample 是能公布比率的最低樣本數(spec §9.5:樣本 < 5 只給絕對數)。 +const StatsMinSample = 5 + +// DimensionVariants 目前無法計算:回覆版本與成交之間還沒有歸因欄位, +// 硬回空陣列會讓前端把功能缺口顯示成「使用者還沒有資料」。 +const DimensionVariants = "variants" + +// DimensionTerms/DimensionSources 需要 radar 商機才能歸因。 +const ( + DimensionTerms = "terms" + DimensionSources = "sources" +) + +// ConversionStat 只帶絕對數;比率是否揭露由樣本門檻決定。 +type ConversionStat struct { + Key string + Accepted int + Replied int + Won int +} + +// StatsResult 是 CRM 轉換統計。UnavailableDimensions 明確標出「尚未實作/無法計算」 +// 的維度,讓呼叫端能跟「查得到但沒有資料」區分開來。 +type StatsResult struct { + TotalContacts int64 + ByStage map[string]int + NeedsFollowUp int + Terms []ConversionStat + Sources []ConversionStat + UnavailableDimensions []string +} + +/* +Stats 聚合 CRM 轉換統計;from/to 任一邊為 0 代表該側不設限。 + +兩種維度的時間語意不同,因為問的問題不同: + - 名單分佈看聯絡人「最近活動時間」落在區間內。 + - 關鍵字/來源看商機的「建立時間」落在區間內,成交與否則取該聯絡人的現況階段。 +*/ +func (s *Service) Stats(ctx context.Context, ownerUID int64, from, to int64) (*StatsResult, error) { + out := &StatsResult{ + ByStage: map[string]int{}, + Terms: []ConversionStat{}, + Sources: []ConversionStat{}, + // 回覆版本維度缺資料模型支援,永遠標為不可用。 + UnavailableDimensions: []string{DimensionVariants}, + } + + stageByContact, err := s.collectContactStages(ctx, ownerUID, from, to, out) + if err != nil { + return nil, err + } + out.ByStage["needs_follow_up"] = out.NeedsFollowUp + + if s.RadarOpps == nil { + out.UnavailableDimensions = append(out.UnavailableDimensions, DimensionTerms, DimensionSources) + return out, nil + } + terms, sources, err := s.attributeOpportunities(ctx, ownerUID, from, to, stageByContact) + if err != nil { + return nil, err + } + out.Terms, out.Sources = sortedStats(terms), sortedStats(sources) + return out, nil +} + +// collectContactStages 走訪全部聯絡人:區間內的計入名單分佈, +// 同時建立 contact → stage 對照供商機歸因使用(避免逐筆回查)。 +func (s *Service) collectContactStages( + ctx context.Context, ownerUID, from, to int64, out *StatsResult, +) (map[string]string, error) { + const pageSize = 500 + stageByContact := map[string]string{} + scanned := 0 + for page := 1; ; page++ { + list, total, err := s.Repo.ListContacts(ctx, ownerUID, domain.ContactListFilter{ + Page: page, PageSize: pageSize, + }) + if err != nil { + return nil, err + } + scanned += len(list) + for _, c := range list { + stageByContact[c.ID] = c.Stage + if !withinRange(contactActivityAt(c), from, to) { + continue + } + out.ByStage[c.Stage]++ + if c.NeedsFollowUp { + out.NeedsFollowUp++ + } + out.TotalContacts++ + } + if len(list) == 0 || int64(scanned) >= total { + break + } + } + return stageByContact, nil +} + +// attributeOpportunities 把已接受的商機分攤到關鍵字與來源兩個維度。 +// 同一個聯絡人在同一關鍵字下只算一次,否則多筆商機會灌大 accepted。 +func (s *Service) attributeOpportunities( + ctx context.Context, ownerUID, from, to int64, stageByContact map[string]string, +) (map[string]*ConversionStat, map[string]*ConversionStat, error) { + const pageSize = 200 + terms := map[string]*ConversionStat{} + sources := map[string]*ConversionStat{} + countedTerm := map[string]bool{} + countedSource := map[string]bool{} + scanned := 0 + for page := 1; ; page++ { + list, total, err := s.RadarOpps.ListOpportunities(ctx, ownerUID, radarDomain.OpportunityListFilter{ + Statuses: []string{radarDomain.OppAccepted}, + CreatedFrom: from, + CreatedTo: to, + Page: page, + PageSize: pageSize, + }) + if err != nil { + return nil, nil, err + } + scanned += len(list) + for _, o := range list { + if o == nil || o.ContactID == "" { + continue + } + stage, ok := stageByContact[o.ContactID] + if !ok { + continue + } + for _, term := range o.MatchedTerms { + if term == "" || countedTerm[o.ContactID+"\x00"+term] { + continue + } + countedTerm[o.ContactID+"\x00"+term] = true + tally(terms, term, stage) + } + source := conversionSource(o.Source) + if !countedSource[o.ContactID+"\x00"+source] { + countedSource[o.ContactID+"\x00"+source] = true + tally(sources, source, stage) + } + } + if len(list) == 0 || int64(scanned) >= total { + break + } + } + return terms, sources, nil +} + +func tally(into map[string]*ConversionStat, key, stage string) { + stat := into[key] + if stat == nil { + stat = &ConversionStat{Key: key} + into[key] = stat + } + stat.Accepted++ + if stageReachedReply(stage) { + stat.Replied++ + } + if stage == domain.StageWon { + stat.Won++ + } +} + +// stageReachedReply:現況階段已走到「對方回覆」之後才算 replied。 +func stageReachedReply(stage string) bool { + switch stage { + case domain.StageReplied, domain.StageQuoted, domain.StageWon: + return true + } + return false +} + +// conversionSource 把商機來源翻成統計維度的來源名稱。 +func conversionSource(oppSource string) string { + switch oppSource { + case radarDomain.OppSourceScoutPromote: + return "scout" + case radarDomain.OppSourceManualImport: + return "manual_import" + default: + return "radar" + } +} + +func contactActivityAt(c *domain.Contact) int64 { + if c.LastTouchAt > 0 { + return c.LastTouchAt + } + if c.UpdatedAt > 0 { + return c.UpdatedAt + } + return c.CreatedAt +} + +func withinRange(at, from, to int64) bool { + if from > 0 && at < from { + return false + } + if to > 0 && at > to { + return false + } + return true +} + +// sortedStats 以成交數→接受數→名稱排序,讓輸出穩定且高價值的排前面。 +func sortedStats(in map[string]*ConversionStat) []ConversionStat { + out := make([]ConversionStat, 0, len(in)) + for _, stat := range in { + out = append(out, *stat) + } + sort.Slice(out, func(i, j int) bool { + if out[i].Won != out[j].Won { + return out[i].Won > out[j].Won + } + if out[i].Accepted != out[j].Accepted { + return out[i].Accepted > out[j].Accepted + } + return out[i].Key < out[j].Key + }) + return out +} diff --git a/apps/backend/internal/module/crm/usecase/stats_test.go b/apps/backend/internal/module/crm/usecase/stats_test.go new file mode 100644 index 0000000..91acd7a --- /dev/null +++ b/apps/backend/internal/module/crm/usecase/stats_test.go @@ -0,0 +1,169 @@ +package usecase + +import ( + "context" + "testing" + + "apps/backend/internal/module/crm/domain" + "apps/backend/internal/module/crm/repository" + radarDomain "apps/backend/internal/module/radar/domain" +) + +type stubRadarOpps struct{ opps []*radarDomain.Opportunity } + +func (s stubRadarOpps) GetOpportunity(_ context.Context, id string) (*radarDomain.Opportunity, error) { + for _, o := range s.opps { + if o.ID == id { + return o, nil + } + } + return nil, radarDomain.ErrNotFound +} + +func (s stubRadarOpps) ListOpportunities( + _ context.Context, ownerUID int64, f radarDomain.OpportunityListFilter, +) ([]*radarDomain.Opportunity, int64, error) { + matched := make([]*radarDomain.Opportunity, 0, len(s.opps)) + for _, o := range s.opps { + if o.OwnerUID != ownerUID || o.Status != radarDomain.OppAccepted { + continue + } + if f.CreatedFrom > 0 && o.CreatedAt < f.CreatedFrom { + continue + } + if f.CreatedTo > 0 && o.CreatedAt > f.CreatedTo { + continue + } + matched = append(matched, o) + } + return matched, int64(len(matched)), nil +} + +func seedStatsContact(t *testing.T, repo domain.Repository, handle, stage string, at int64) *domain.Contact { + t.Helper() + c, err := repo.UpsertContactByIdentity(context.Background(), &domain.Contact{ + OwnerUID: 7, AuthorHandle: handle, Stage: stage, LastTouchAt: at, + }) + if err != nil { + t.Fatal(err) + } + c.Stage = stage + c.LastTouchAt = at + if err := repo.SaveContact(context.Background(), c); err != nil { + t.Fatal(err) + } + return c +} + +func acceptedOpp(id, contactID, source string, createdAt int64, terms ...string) *radarDomain.Opportunity { + return &radarDomain.Opportunity{ + ID: id, OwnerUID: 7, Status: radarDomain.OppAccepted, Source: source, + ContactID: contactID, MatchedTerms: terms, CreatedAt: createdAt, + } +} + +func TestStatsAttributesTermsAndSources(t *testing.T) { + ctx := context.Background() + repo := repository.NewMemory() + svc := New(repo) + + won := seedStatsContact(t, repo, "won-buyer", domain.StageWon, 100) + replied := seedStatsContact(t, repo, "replied-buyer", domain.StageReplied, 100) + cold := seedStatsContact(t, repo, "cold-buyer", domain.StageNewFound, 100) + + svc.RadarOpps = stubRadarOpps{opps: []*radarDomain.Opportunity{ + acceptedOpp("o1", won.ID, radarDomain.OppSourceThreads, 100, "婚攝"), + // 同一聯絡人同一關鍵字的第二筆商機不可重複計入 accepted + acceptedOpp("o2", won.ID, radarDomain.OppSourceThreads, 101, "婚攝"), + acceptedOpp("o3", replied.ID, radarDomain.OppSourceThreads, 100, "婚攝"), + acceptedOpp("o4", cold.ID, radarDomain.OppSourceScoutPromote, 100, "外包"), + }} + + got, err := svc.Stats(ctx, 7, 0, 0) + if err != nil { + t.Fatal(err) + } + + terms := map[string]ConversionStat{} + for _, s := range got.Terms { + terms[s.Key] = s + } + if s := terms["婚攝"]; s.Accepted != 2 || s.Replied != 2 || s.Won != 1 { + t.Fatalf("婚攝 = %+v, want accepted 2 / replied 2 / won 1", s) + } + if s := terms["外包"]; s.Accepted != 1 || s.Replied != 0 || s.Won != 0 { + t.Fatalf("外包 = %+v", s) + } + + sources := map[string]ConversionStat{} + for _, s := range got.Sources { + sources[s.Key] = s + } + if s := sources["radar"]; s.Accepted != 2 || s.Won != 1 { + t.Fatalf("radar source = %+v", s) + } + if s := sources["scout"]; s.Accepted != 1 { + t.Fatalf("scout source = %+v", s) + } +} + +// 回覆版本維度沒有資料模型支援,必須明說不可用而不是回空清單。 +func TestStatsFlagsVariantsUnavailable(t *testing.T) { + svc := New(repository.NewMemory()) + svc.RadarOpps = stubRadarOpps{} + got, err := svc.Stats(context.Background(), 7, 0, 0) + if err != nil { + t.Fatal(err) + } + if !hasDimension(got.UnavailableDimensions, DimensionVariants) { + t.Fatalf("variants must be reported unavailable, got %v", got.UnavailableDimensions) + } + if hasDimension(got.UnavailableDimensions, DimensionTerms) { + t.Fatalf("terms is computable when radar is wired, got %v", got.UnavailableDimensions) + } +} + +func TestStatsFlagsTermsUnavailableWithoutRadar(t *testing.T) { + svc := New(repository.NewMemory()) + got, err := svc.Stats(context.Background(), 7, 0, 0) + if err != nil { + t.Fatal(err) + } + for _, dim := range []string{DimensionVariants, DimensionTerms, DimensionSources} { + if !hasDimension(got.UnavailableDimensions, dim) { + t.Fatalf("%s must be unavailable without radar, got %v", dim, got.UnavailableDimensions) + } + } +} + +func TestStatsHonoursDateRange(t *testing.T) { + ctx := context.Background() + repo := repository.NewMemory() + svc := New(repo) + old := seedStatsContact(t, repo, "old-buyer", domain.StageWon, 50) + recent := seedStatsContact(t, repo, "recent-buyer", domain.StageWon, 500) + svc.RadarOpps = stubRadarOpps{opps: []*radarDomain.Opportunity{ + acceptedOpp("o1", old.ID, radarDomain.OppSourceThreads, 50, "舊詞"), + acceptedOpp("o2", recent.ID, radarDomain.OppSourceThreads, 500, "新詞"), + }} + + got, err := svc.Stats(ctx, 7, 400, 600) + if err != nil { + t.Fatal(err) + } + if got.TotalContacts != 1 || got.ByStage[domain.StageWon] != 1 { + t.Fatalf("range should keep only the recent contact, got %+v", got) + } + if len(got.Terms) != 1 || got.Terms[0].Key != "新詞" { + t.Fatalf("range should keep only the recent term, got %+v", got.Terms) + } +} + +func hasDimension(list []string, want string) bool { + for _, v := range list { + if v == want { + return true + } + } + return false +} diff --git a/apps/backend/internal/module/job/domain/repository.go b/apps/backend/internal/module/job/domain/repository.go index 950d208..91e866b 100644 --- a/apps/backend/internal/module/job/domain/repository.go +++ b/apps/backend/internal/module/job/domain/repository.go @@ -4,6 +4,11 @@ import "context" type Repository interface { Insert(ctx context.Context, j *Job) error + // InsertUniqueRef atomically inserts j unless a job with the same + // owner+template+ref already exists, in which case the existing job is + // returned with created=false. Check-then-insert is not enough: concurrent + // schedulers would each see "no existing job" and all insert. + InsertUniqueRef(ctx context.Context, j *Job) (stored *Job, created bool, err error) Update(ctx context.Context, j *Job) error // UpdateOwned atomically requires the current running lease to belong to leaseOwner. UpdateOwned(ctx context.Context, j *Job, leaseOwner string) error diff --git a/apps/backend/internal/module/job/repository/memory.go b/apps/backend/internal/module/job/repository/memory.go index 16c6099..fea84f5 100644 --- a/apps/backend/internal/module/job/repository/memory.go +++ b/apps/backend/internal/module/job/repository/memory.go @@ -24,6 +24,23 @@ func (s *MemoryStore) Insert(_ context.Context, j *domain.Job) error { return nil } +func (s *MemoryStore) InsertUniqueRef(_ context.Context, j *domain.Job) (*domain.Job, bool, error) { + if j == nil { + return nil, false, domain.ErrNotFound + } + s.mu.Lock() + defer s.mu.Unlock() + for _, stored := range s.byID { + if stored.OwnerUID == j.OwnerUID && stored.TemplateType == j.TemplateType && stored.RefID == j.RefID { + cp := *stored + return &cp, false, nil + } + } + cp := *j + s.byID[j.ID] = &cp + return j, true, nil +} + func (s *MemoryStore) Update(_ context.Context, j *domain.Job) error { return s.update(j, "") } diff --git a/apps/backend/internal/module/job/repository/mongo.go b/apps/backend/internal/module/job/repository/mongo.go index 43c1e97..876b8ed 100644 --- a/apps/backend/internal/module/job/repository/mongo.go +++ b/apps/backend/internal/module/job/repository/mongo.go @@ -38,6 +38,31 @@ func (s *MonStore) Insert(ctx context.Context, j *domain.Job) error { return err } +// InsertUniqueRef upserts on (owner_uid, template_type, ref_id) so concurrent +// schedulers converge on one job. The unique index from migration 000019 is +// what makes this safe; without it two upserts can both insert. +func (s *MonStore) InsertUniqueRef(ctx context.Context, j *domain.Job) (*domain.Job, bool, error) { + if j == nil { + return nil, false, domain.ErrNotFound + } + filter := bson.M{"owner_uid": j.OwnerUID, "template_type": j.TemplateType, "ref_id": j.RefID} + var stored domain.Job + err := s.claimJobs.FindOneAndUpdate(ctx, filter, + bson.M{"$setOnInsert": j}, + options.FindOneAndUpdate().SetUpsert(true).SetReturnDocument(options.After), + ).Decode(&stored) + if err != nil { + // Lost the upsert race against the unique index: the winner is durable. + if mongo.IsDuplicateKeyError(err) { + if ferr := s.claimJobs.FindOne(ctx, filter).Decode(&stored); ferr == nil { + return &stored, false, nil + } + } + return nil, false, err + } + return &stored, stored.ID == j.ID, nil +} + func (s *MonStore) Update(ctx context.Context, j *domain.Job) error { return s.update(ctx, j, nil) } diff --git a/apps/backend/internal/module/job/usecase/service.go b/apps/backend/internal/module/job/usecase/service.go index 3c74377..2447999 100644 --- a/apps/backend/internal/module/job/usecase/service.go +++ b/apps/backend/internal/module/job/usecase/service.go @@ -178,14 +178,6 @@ func (s *Service) ScheduleRadarSweep(ctx context.Context, ownerUID int64, watchI day := time.Unix(0, runAt).UTC().Format("2006-01-02") ref := RadarSweepRef(watchID, day) - existing, err := s.findRadarSweepForRef(ctx, ownerUID, ref) - if err != nil { - return nil, err - } - if existing != nil { - return existing, nil - } - body, err := json.Marshal(RadarSweepPayload{WatchID: watchID, Day: day}) if err != nil { return nil, err @@ -204,28 +196,15 @@ func (s *Service) ScheduleRadarSweep(ctx context.Context, ownerUID int64, watchI CreatedAt: now, UpdatedAt: now, } - if err := s.Repo.Insert(ctx, j); err != nil { - // Race with another scheduler: re-check and return the winner. - if again, ferr := s.findRadarSweepForRef(ctx, ownerUID, ref); ferr == nil && again != nil { - return again, nil - } - return nil, err - } - s.notify(ctx, j) - return j, nil -} - -func (s *Service) findRadarSweepForRef(ctx context.Context, ownerUID int64, ref string) (*domain.Job, error) { - list, err := s.Repo.ListByOwner(ctx, ownerUID) + stored, created, err := s.Repo.InsertUniqueRef(ctx, j) if err != nil { return nil, err } - for _, j := range list { - if j.TemplateType == domain.TemplateRadarSweep && j.RefID == ref { - return j, nil - } + // 只有真的建立才通知,否則同一天的重複排程會轟炸使用者。 + if created { + s.notify(ctx, stored) } - return nil, nil + return stored, nil } // ScheduleManualRadarSweep starts an extra patrol now. diff --git a/apps/backend/internal/module/radar/domain/product_match.go b/apps/backend/internal/module/radar/domain/product_match.go index cd73b10..2ccf067 100644 --- a/apps/backend/internal/module/radar/domain/product_match.go +++ b/apps/backend/internal/module/radar/domain/product_match.go @@ -102,7 +102,7 @@ func (m *ProductMatch) ValidateForWrite() error { if m.Excluded && strings.TrimSpace(m.ExcludeReason) == "" { return fmt.Errorf("%w: excluded product match requires exclude_reason", ErrValidation) } - m.Eligible = total >= 45 && painOrScenario && !m.Excluded + m.Eligible = ProductFitEligible(total, painOrScenario, m.Excluded) m.WatchIDs = uniqueStrings(m.WatchIDs) m.MatchedTerms = NormalizeMatchedTerms(m.MatchedTerms) if m.Risks == nil { diff --git a/apps/backend/internal/module/radar/domain/product_scoring.go b/apps/backend/internal/module/radar/domain/product_scoring.go index 28c8204..32016ad 100644 --- a/apps/backend/internal/module/radar/domain/product_scoring.go +++ b/apps/backend/internal/module/radar/domain/product_scoring.go @@ -32,6 +32,8 @@ func ProductFitDimensionWeight(dimension string) int { } } -func ProductFitEligible(score, pain, scenario int, excluded bool) bool { - return score >= ProductFitEligibleMinScore && (pain > 0 || scenario > 0) && !excluded +// ProductFitEligible is the single definition of "worth surfacing": enough +// total score, at least one pain or scenario hit, and not excluded. +func ProductFitEligible(score int, painOrScenario, excluded bool) bool { + return score >= ProductFitEligibleMinScore && painOrScenario && !excluded } diff --git a/apps/backend/internal/module/radar/domain/region.go b/apps/backend/internal/module/radar/domain/region.go index d064eaa..0d1a395 100644 --- a/apps/backend/internal/module/radar/domain/region.go +++ b/apps/backend/internal/module/radar/domain/region.go @@ -1,6 +1,9 @@ package domain -import "strings" +import ( + "sort" + "strings" +) // regionAliases maps common Taiwan place names (zh) → service area code. // Exact alias match only — never geo-infer (OP-04). @@ -29,34 +32,46 @@ var regionAliases = map[string]string{ "連江": "LIE", "連江縣": "LIE", "馬祖": "LIE", "lie": "LIE", } +type regionAliasEntry struct { + runes []rune + code string +} + +// sortedRegionAliases is built once at startup: longest alias first so 「台北市」 +// wins over 「台北」, with the alias itself breaking ties so detection order is +// reproducible instead of following map iteration. +var sortedRegionAliases = buildSortedRegionAliases() + +func buildSortedRegionAliases() []regionAliasEntry { + out := make([]regionAliasEntry, 0, len(regionAliases)) + for alias, code := range regionAliases { + if alias == "" { + continue + } + out = append(out, regionAliasEntry{runes: []rune(strings.ToLower(alias)), code: code}) + } + sort.Slice(out, func(i, j int) bool { + if len(out[i].runes) != len(out[j].runes) { + return len(out[i].runes) > len(out[j].runes) + } + return string(out[i].runes) < string(out[j].runes) + }) + return out +} + // DetectRegionCodes finds service-area codes mentioned in free text (exact alias only). func DetectRegionCodes(text string) []string { lower := strings.ToLower(text) + runes := []rune(lower) + if len(runes) == 0 { + return nil + } seen := map[string]bool{} var out []string - // Longer aliases first so 「台北市」 wins over 「台北」. - type pair struct{ alias, code string } - pairs := make([]pair, 0, len(regionAliases)) - for a, c := range regionAliases { - pairs = append(pairs, pair{a, c}) - } - // crude length sort - for i := 0; i < len(pairs); i++ { - for j := i + 1; j < len(pairs); j++ { - if len([]rune(pairs[j].alias)) > len([]rune(pairs[i].alias)) { - pairs[i], pairs[j] = pairs[j], pairs[i] - } - } - } - matched := make([]string, len(lower)) - copy(matched, []string{}) // silence unused if empty - _ = matched - covered := make([]bool, len([]rune(lower))) - runes := []rune(lower) - for _, p := range pairs { - alias := strings.ToLower(p.alias) - ar := []rune(alias) - if len(ar) == 0 { + covered := make([]bool, len(runes)) + for _, p := range sortedRegionAliases { + ar := p.runes + if len(ar) > len(runes) { continue } for i := 0; i+len(ar) <= len(runes); i++ { @@ -79,14 +94,16 @@ func DetectRegionCodes(text string) []string { } } } - // also direct code tokens + // also direct code tokens;map 迭代無序,排序後再併入以維持輸出穩定 + direct := make([]string, 0) for code := range serviceAreaCodes { if strings.Contains(lower, strings.ToLower(code)) && !seen[code] { seen[code] = true - out = append(out, code) + direct = append(direct, code) } } - return out + sort.Strings(direct) + return append(out, direct...) } // MatchRegion compares detected codes against the owner's service areas. diff --git a/apps/backend/internal/module/radar/domain/repository.go b/apps/backend/internal/module/radar/domain/repository.go index 4d99f72..7b9c276 100644 --- a/apps/backend/internal/module/radar/domain/repository.go +++ b/apps/backend/internal/module/radar/domain/repository.go @@ -54,5 +54,8 @@ type Repository interface { // ReplyVariant SaveReply(ctx context.Context, r *ReplyVariant) error ListReplies(ctx context.Context, ownerUID int64, opportunityID string) ([]*ReplyVariant, error) + // ListRepliesForOpportunities fetches replies for many opportunities at once, + // keyed by opportunity id, so list pages do not issue one query per row. + ListRepliesForOpportunities(ctx context.Context, ownerUID int64, opportunityIDs []string) (map[string][]*ReplyVariant, error) GetReply(ctx context.Context, id string) (*ReplyVariant, error) } diff --git a/apps/backend/internal/module/radar/repository/reply_memory.go b/apps/backend/internal/module/radar/repository/reply_memory.go index 6afd33d..021d105 100644 --- a/apps/backend/internal/module/radar/repository/reply_memory.go +++ b/apps/backend/internal/module/radar/repository/reply_memory.go @@ -32,6 +32,30 @@ func (m *Memory) ListReplies(_ context.Context, ownerUID int64, opportunityID st return out, nil } +func (m *Memory) ListRepliesForOpportunities(_ context.Context, ownerUID int64, opportunityIDs []string) (map[string][]*domain.ReplyVariant, error) { + out := make(map[string][]*domain.ReplyVariant, len(opportunityIDs)) + if len(opportunityIDs) == 0 { + return out, nil + } + wanted := make(map[string]bool, len(opportunityIDs)) + for _, id := range opportunityIDs { + wanted[id] = true + } + m.mu.Lock() + defer m.mu.Unlock() + for _, r := range m.replies { + if r.OwnerUID != ownerUID || !wanted[r.OpportunityID] { + continue + } + cp := *r + out[r.OpportunityID] = append(out[r.OpportunityID], &cp) + } + for _, group := range out { + sort.Slice(group, func(i, j int) bool { return group[i].CreatedAt > group[j].CreatedAt }) + } + return out, nil +} + func (m *Memory) GetReply(_ context.Context, id string) (*domain.ReplyVariant, error) { m.mu.Lock() defer m.mu.Unlock() diff --git a/apps/backend/internal/module/radar/repository/reply_mongo.go b/apps/backend/internal/module/radar/repository/reply_mongo.go index 1b854a4..d752095 100644 --- a/apps/backend/internal/module/radar/repository/reply_mongo.go +++ b/apps/backend/internal/module/radar/repository/reply_mongo.go @@ -24,6 +24,25 @@ func (s *MonStore) ListReplies(ctx context.Context, ownerUID int64, opportunityI return list, err } +func (s *MonStore) ListRepliesForOpportunities(ctx context.Context, ownerUID int64, opportunityIDs []string) (map[string][]*domain.ReplyVariant, error) { + out := make(map[string][]*domain.ReplyVariant, len(opportunityIDs)) + if len(opportunityIDs) == 0 { + return out, nil + } + var list []*domain.ReplyVariant + err := s.replies.Find(ctx, &list, bson.M{ + "owner_uid": ownerUID, + "opportunity_id": bson.M{"$in": opportunityIDs}, + }, options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})) + if err != nil { + return nil, err + } + for _, r := range list { + out[r.OpportunityID] = append(out[r.OpportunityID], r) + } + return out, nil +} + func (s *MonStore) GetReply(ctx context.Context, id string) (*domain.ReplyVariant, error) { var r domain.ReplyVariant err := s.replies.FindOne(ctx, &r, bson.M{"_id": id}) diff --git a/apps/backend/internal/module/radar/usecase/sweep_notify.go b/apps/backend/internal/module/radar/usecase/sweep_notify.go index 165176c..d2e4088 100644 --- a/apps/backend/internal/module/radar/usecase/sweep_notify.go +++ b/apps/backend/internal/module/radar/usecase/sweep_notify.go @@ -1,13 +1,6 @@ package usecase -import ( - "context" - "fmt" - - appnotifDomain "apps/backend/internal/module/appnotif/domain" - - "github.com/google/uuid" -) +import "context" // AppNotifWriter is satisfied by appnotif usecase for system notifications. type AppNotifWriter interface { @@ -15,27 +8,6 @@ type AppNotifWriter interface { InsertSystem(ctx context.Context, ownerUID int64, title, body, refType, refID string) error } -// AppNotifBridge adapts appnotif.Service-like insert. -type AppNotifBridge struct { - Insert func(ctx context.Context, n *appnotifDomain.Notification) error -} - -func (b *AppNotifBridge) InsertSystem(ctx context.Context, ownerUID int64, title, body, refType, refID string) error { - if b == nil || b.Insert == nil { - return nil - } - return b.Insert(ctx, &appnotifDomain.Notification{ - ID: uuid.NewString(), - OwnerUID: ownerUID, - Title: title, - Body: body, - Kind: appnotifDomain.KindSystem, - RefType: refType, - RefID: refID, - CreatedAt: appnotifDomain.NowNano(), - }) -} - // NotifierFromAppNotif builds SweepNotifier from appnotif bridge. func NotifierFromAppNotif(w AppNotifWriter) SweepNotifier { return sweepNotifyFunc(func(ctx context.Context, ownerUID int64, sweepID, watchID, reason string) error { @@ -56,6 +28,3 @@ type sweepNotifyFunc func(ctx context.Context, ownerUID int64, sweepID, watchID, func (f sweepNotifyFunc) NotifySweepFailed(ctx context.Context, ownerUID int64, sweepID, watchID, reason string) error { return f(ctx, ownerUID, sweepID, watchID, reason) } - -// Ensure compile-time string for watchID usage in future deep-links. -var _ = fmt.Sprintf diff --git a/apps/backend/internal/module/radar/usecase/today.go b/apps/backend/internal/module/radar/usecase/today.go index 03bae8e..d719d09 100644 --- a/apps/backend/internal/module/radar/usecase/today.go +++ b/apps/backend/internal/module/radar/usecase/today.go @@ -83,7 +83,7 @@ func (s *Service) GetTodayFiltered(ctx context.Context, ownerUID int64, productF } productFiltered := productFilter.BrandID != "" || productFilter.ProductID != "" || productFilter.FitBand != "" - high, mid, low := []TodayOpportunity{}, []TodayOpportunity{}, []TodayOpportunity{} + visible := make([]*domain.Opportunity, 0, len(list)) for _, o := range list { if len(o.ProductMatches) > 0 && !todayHasEligibleProduct(o, productFilter) { continue @@ -91,13 +91,25 @@ func (s *Service) GetTodayFiltered(ctx context.Context, ownerUID int64, productF if productFiltered && len(o.ProductMatches) == 0 { continue } + visible = append(visible, o) + } + visibleIDs := make([]string, 0, len(visible)) + for _, o := range visible { + visibleIDs = append(visibleIDs, o.ID) + } + // 一次取回整頁的回覆;預設回覆只是輔助資訊,查不到不擋今日名單。 + repliesByOpportunity, rerr := s.Repo.ListRepliesForOpportunities(ctx, ownerUID, visibleIDs) + if rerr != nil { + repliesByOpportunity = nil + } + + high, mid, low := []TodayOpportunity{}, []TodayOpportunity{}, []TodayOpportunity{} + for _, o := range visible { card := TodayOpportunity{Opportunity: o} - if replies, rerr := s.Repo.ListReplies(ctx, ownerUID, o.ID); rerr == nil { - for _, r := range replies { - if r.Variant == domain.ReplyPublicComment { - card.DefaultReply = r - break - } + for _, r := range repliesByOpportunity[o.ID] { + if r.Variant == domain.ReplyPublicComment { + card.DefaultReply = r + break } } switch o.IntentBand { diff --git a/apps/backend/internal/module/radar/usecase/watch_quota.go b/apps/backend/internal/module/radar/usecase/watch_quota.go index 93e88f1..fa6f22b 100644 --- a/apps/backend/internal/module/radar/usecase/watch_quota.go +++ b/apps/backend/internal/module/radar/usecase/watch_quota.go @@ -70,17 +70,13 @@ func (s *Service) MaxDailyOpportunities(ctx context.Context, ownerUID int64) (in } /* -assertCanActivate 是「變成 active」的兩道閘(SP-01、RW-01)。 +assertCanActivateForWatch 是「變成 active」的兩道閘(SP-01、RW-01)。 exceptWatchID 是正在恢復的那一筆:它目前不是 active,所以不會被算進 CountActive, 帶進來只為了在訊息與計算上表達清楚。 既有超額者不強制降級(spec §3.1):這裡只擋「再多一個」。 */ -func (s *Service) assertCanActivate(ctx context.Context, ownerUID int64, exceptWatchID string) error { - return s.assertCanActivateForWatch(ctx, ownerUID, exceptWatchID, false) -} - func (s *Service) assertCanActivateForWatch(ctx context.Context, ownerUID int64, exceptWatchID string, productWatch bool) error { if productWatch { return s.assertCanActivateQuota(ctx, ownerUID, exceptWatchID) diff --git a/apps/backend/internal/module/scout/repository/mongo.go b/apps/backend/internal/module/scout/repository/mongo.go index e247018..bd65d04 100644 --- a/apps/backend/internal/module/scout/repository/mongo.go +++ b/apps/backend/internal/module/scout/repository/mongo.go @@ -276,6 +276,10 @@ func (s *MonStore) ListRunPosts(ctx context.Context, ownerUID int64, runID strin return domain.RunPostPage{}, err } page := domain.NormalizePage(requestedPage, requestedSize) + // 與 memory store 一致的可見性屏障:run 成功前的候選是暫存資料,不得外流。 + if r.Status != domain.RunSucceeded { + return domain.RunPostPage{Run: r, Items: []*domain.Post{}, Pagination: page.WithTotal(0)}, nil + } q := bson.M{"owner_uid": ownerUID, "run_id": runID} total, err := s.posts.CountDocuments(ctx, q) if err != nil { diff --git a/apps/backend/internal/module/scout/usecase/search_pipeline.go b/apps/backend/internal/module/scout/usecase/search_pipeline.go index 27ca66b..8b96107 100644 --- a/apps/backend/internal/module/scout/usecase/search_pipeline.go +++ b/apps/backend/internal/module/scout/usecase/search_pipeline.go @@ -239,13 +239,3 @@ func shortfallReasons(d SearchPipelineDiagnostics) []string { } return reasons } - -func normalizePipelineTerms(terms []string) []string { - out := make([]string, 0, len(terms)) - for _, term := range terms { - if term = strings.TrimSpace(term); term != "" { - out = append(out, term) - } - } - return dedupeTerms(out) -} diff --git a/apps/backend/internal/module/scout/usecase/service.go b/apps/backend/internal/module/scout/usecase/service.go index af43bf7..bce8321 100644 --- a/apps/backend/internal/module/scout/usecase/service.go +++ b/apps/backend/internal/module/scout/usecase/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/url" + "sort" "strings" "time" "unicode/utf8" @@ -860,32 +861,25 @@ func (s *Service) persistSearchHitsWithRun(ctx context.Context, ownerUID int64, } func sortPostsByScore(posts []*domain.Post) { - for i := 0; i < len(posts); i++ { - for j := i + 1; j < len(posts); j++ { - a, b := posts[i], posts[j] - shouldSwap := false - if a.Score != b.Score { - shouldSwap = b.Score > a.Score - } else if (a.PostedAt > 0) != (b.PostedAt > 0) { - shouldSwap = b.PostedAt > 0 - } else if a.PostedAt > 0 && a.PostedAt != b.PostedAt { - shouldSwap = b.PostedAt > a.PostedAt - } else if a.CreatedAt != b.CreatedAt { - shouldSwap = b.CreatedAt > a.CreatedAt - } else { - shouldSwap = b.ID > a.ID - } - if shouldSwap { - posts[i], posts[j] = posts[j], posts[i] - } + sort.SliceStable(posts, func(i, j int) bool { + a, b := posts[i], posts[j] + if a.Score != b.Score { + return a.Score > b.Score } - } + // 有發文時間者優先,其次新到舊;ID 收尾保證結果穩定可重現 + if (a.PostedAt > 0) != (b.PostedAt > 0) { + return a.PostedAt > 0 + } + if a.PostedAt > 0 && a.PostedAt != b.PostedAt { + return a.PostedAt > b.PostedAt + } + if a.CreatedAt != b.CreatedAt { + return a.CreatedAt > b.CreatedAt + } + return a.ID > b.ID + }) } -// sortPostsByResultTime is kept as a package-local compatibility name for -// older tests/callers; result ordering now intentionally delegates to score. -func sortPostsByResultTime(posts []*domain.Post) { sortPostsByScore(posts) } - func hitsHaveTrack(hits []ThreadSearchResult) bool { for _, h := range hits { if h.Track != "" { @@ -982,88 +976,18 @@ func sortHitsByTrackAndPostedAt(hits []ThreadSearchResult) { return 3 } } - for i := 0; i < len(hits); i++ { - for j := i + 1; j < len(hits); j++ { - ri, rj := trackRank(hits[i].Track), trackRank(hits[j].Track) - if rj < ri { - hits[i], hits[j] = hits[j], hits[i] - continue - } - if rj > ri { - continue - } - ai, aj := hits[i].PublishedAt, hits[j].PublishedAt - if ai == 0 && aj == 0 { - continue - } - if ai == 0 || (aj > 0 && aj > ai) { - hits[i], hits[j] = hits[j], hits[i] - } + sort.SliceStable(hits, func(i, j int) bool { + ri, rj := trackRank(hits[i].Track), trackRank(hits[j].Track) + if ri != rj { + return ri < rj } - } -} - -// sortPostsByTrackAndPostedAt 保留相容:目前先 track 再時間在 sortHits 已做;此函式 no-op 佔位避免誤用。 -func sortPostsByTrackAndPostedAt(_ []*domain.Post, _ []ThreadSearchResult) {} - -func sortHitsByPostedAt(hits []ThreadSearchResult) { - // newest first; unknown published time last - for i := 0; i < len(hits); i++ { - for j := i + 1; j < len(hits); j++ { - ai, aj := hits[i].PublishedAt, hits[j].PublishedAt - if ai == 0 && aj == 0 { - continue - } - if ai == 0 || (aj > 0 && aj > ai) { - hits[i], hits[j] = hits[j], hits[i] - } + // 同軌內新到舊;沒有發布時間的排最後 + ai, aj := hits[i].PublishedAt, hits[j].PublishedAt + if ai == 0 || aj == 0 { + return ai != 0 } - } -} - -func sortPostsByPostedAt(posts []*domain.Post) { - for i := 0; i < len(posts); i++ { - for j := i + 1; j < len(posts); j++ { - ai := posts[i].PostedAt - if ai == 0 { - ai = posts[i].CreatedAt - } - aj := posts[j].PostedAt - if aj == 0 { - aj = posts[j].CreatedAt - } - if aj > ai { - posts[i], posts[j] = posts[j], posts[i] - } - } - } -} - -func sortActivityPostsByMomentum(posts []*domain.Post) { - for i := 0; i < len(posts); i++ { - for j := i + 1; j < len(posts); j++ { - if posts[j].Score > posts[i].Score || - (posts[j].Score == posts[i].Score && postTime(posts[j]) > postTime(posts[i])) { - posts[i], posts[j] = posts[j], posts[i] - } - } - } -} - -func postTime(post *domain.Post) int64 { - if post.PostedAt > 0 { - return post.PostedAt - } - return post.CreatedAt -} - -func matchingTerm(text string, terms []string) string { - for _, term := range terms { - if term = strings.TrimSpace(term); term != "" && strings.Contains(strings.ToLower(text), strings.ToLower(term)) { - return term - } - } - return "" + return ai > aj + }) } func authorFromThreadsURL(raw string) string { diff --git a/apps/backend/internal/module/search/exa.go b/apps/backend/internal/module/search/exa.go index a9e36a0..36195b9 100644 --- a/apps/backend/internal/module/search/exa.go +++ b/apps/backend/internal/module/search/exa.go @@ -62,13 +62,6 @@ func NewExa() *ExaClient { } } -// NewExaThreads prefers results from Threads domains. -func NewExaThreads() *ExaClient { - c := NewExa() - c.IncludeDomains = []string{"threads.net", "threads.com"} - return c -} - func (c *ExaClient) Search(ctx context.Context, apiKey, query string, limit int) ([]Hit, error) { apiKey = strings.TrimSpace(apiKey) query = strings.TrimSpace(query) diff --git a/apps/backend/internal/svc/service_context.go b/apps/backend/internal/svc/service_context.go index d6e4502..3664028 100644 --- a/apps/backend/internal/svc/service_context.go +++ b/apps/backend/internal/svc/service_context.go @@ -856,6 +856,15 @@ func (b *crmRadarOppBridge) GetOpportunity(ctx context.Context, id string) (*rad return b.Radar.Repo.GetOpportunity(ctx, id) } +func (b *crmRadarOppBridge) ListOpportunities( + ctx context.Context, ownerUID int64, f radarDomain.OpportunityListFilter, +) ([]*radarDomain.Opportunity, int64, error) { + if b == nil || b.Radar == nil { + return nil, 0, radarDomain.ErrNotFound + } + return b.Radar.Repo.ListOpportunities(ctx, ownerUID, f) +} + type radarHealthBridge struct{ Growth *growthUC.Service } func (b *radarHealthBridge) WorstLevel(ctx context.Context, ownerUID int64) (level string, advice string, err error) { diff --git a/apps/backend/internal/types/types.go b/apps/backend/internal/types/types.go index a140da6..b92c7f6 100644 --- a/apps/backend/internal/types/types.go +++ b/apps/backend/internal/types/types.go @@ -530,9 +530,10 @@ type CrmConversionData struct { } type CrmStatsData struct { - Terms []TermConversionStat `json:"terms"` - Variants []VariantConversionStat `json:"variants"` - Sources []SourceConversionStat `json:"sources"` + Terms []TermConversionStat `json:"terms"` + Variants []VariantConversionStat `json:"variants"` + Sources []SourceConversionStat `json:"sources"` + UnavailableDimensions []string `json:"unavailable_dimensions"` } type CrmStatsReq struct { diff --git a/apps/web/src/data/live/radarRepos.ts b/apps/web/src/data/live/radarRepos.ts index 3a5959d..0d5f3aa 100644 --- a/apps/web/src/data/live/radarRepos.ts +++ b/apps/web/src/data/live/radarRepos.ts @@ -800,6 +800,9 @@ export function createLiveCrmRepo(): CrmRepo { won: num(s.won), insufficient_sample: Boolean(s.insufficient_sample), })), + unavailable_dimensions: (Array.isArray(raw.unavailable_dimensions) + ? raw.unavailable_dimensions.map(str) + : []) as CrmStats["unavailable_dimensions"], }; }, }; diff --git a/apps/web/src/domain/types.ts b/apps/web/src/domain/types.ts index 35ae2b4..6f2cdb1 100644 --- a/apps/web/src/domain/types.ts +++ b/apps/web/src/domain/types.ts @@ -1303,10 +1303,14 @@ export type SourceConversionStat = { insufficient_sample: boolean; }; +export type CrmStatsDimension = "terms" | "variants" | "sources"; + export type CrmStats = { terms: TermConversionStat[]; variants: VariantConversionStat[]; sources: SourceConversionStat[]; + /** 尚未實作/無法計算的維度;用來跟「查得到但沒有資料」區分 */ + unavailable_dimensions: CrmStatsDimension[]; }; export type ContactStageCount = { diff --git a/apps/web/src/firstRun/FirstRunContext.tsx b/apps/web/src/firstRun/FirstRunContext.tsx index fd41fd7..02abca6 100644 --- a/apps/web/src/firstRun/FirstRunContext.tsx +++ b/apps/web/src/firstRun/FirstRunContext.tsx @@ -97,11 +97,11 @@ export function FirstRunProvider({ children }: { children: ReactNode }) { [pending, allDone, busy, steps, current], ); - const gateOffPath = value.active && value.current && !isFirstRunAllowedPath(pathname); + const gateStep = value.active && !isFirstRunAllowedPath(pathname) ? value.current : null; return ( - {gateOffPath ? : children} + {gateStep ? : children} ); } diff --git a/apps/web/src/i18n/I18nContext.tsx b/apps/web/src/i18n/I18nContext.tsx index 24e51e0..ae18a8c 100644 --- a/apps/web/src/i18n/I18nContext.tsx +++ b/apps/web/src/i18n/I18nContext.tsx @@ -8,9 +8,14 @@ import { type ReactNode, } from "react"; import { formatMoney, formatPlanPrice } from "../lib/i18n/format"; -import { translate } from "../lib/i18n/messages"; +import { + ensureCatalog, + formatMessage, + getCatalog, + isCatalogLoaded, +} from "../lib/i18n/messages"; import { loadUiPrefs, saveUiPrefs } from "../lib/i18n/prefs"; -import type { AppCurrency, AppLocale } from "../lib/i18n/types"; +import type { AppCurrency, AppLocale, MessageDict } from "../lib/i18n/types"; type I18nContextValue = { locale: AppLocale; @@ -31,10 +36,25 @@ export function I18nProvider({ children }: { children: ReactNode }) { () => loadUiPrefs().currency, ); + const [catalog, setCatalog] = useState(() => getCatalog(locale)); + useEffect(() => { document.documentElement.lang = locale === "en" ? "en" : "zh-Hant"; }, [locale]); + // 非預設語系是獨立 chunk:先用手上的字典頂著,載完再換上正式的。 + useEffect(() => { + setCatalog(getCatalog(locale)); + if (isCatalogLoaded(locale)) return; + let cancelled = false; + void ensureCatalog(locale).then((dict) => { + if (!cancelled) setCatalog(dict); + }); + return () => { + cancelled = true; + }; + }, [locale]); + const setLocale = useCallback((next: AppLocale) => { setLocaleState(next); saveUiPrefs({ locale: next }); @@ -58,8 +78,8 @@ export function I18nProvider({ children }: { children: ReactNode }) { const t = useCallback( (key: string, params?: Record) => - translate(locale, key, params), - [locale], + formatMessage(catalog, key, params), + [catalog], ); const value = useMemo( diff --git a/apps/web/src/lib/i18n/catalog.en.ts b/apps/web/src/lib/i18n/catalog.en.ts new file mode 100644 index 0000000..8cd18a3 --- /dev/null +++ b/apps/web/src/lib/i18n/catalog.en.ts @@ -0,0 +1,2903 @@ +import type { MessageDict } from "./types"; + +export const en: MessageDict = { + "app.name": "Lapras", + "app.nameEn": "Lapras", + "app.nameZh": "巡樓", + "app.tagline": "Patrol Threads with ease", + "app.taglineEn": "Patrol Threads with ease", + "nav.today": "Today", + "nav.crew": "Accounts", + "nav.studio": "Studio", + "nav.radar": "Demand", + "nav.crm": "CRM", + "nav.scout": "Topics", + "nav.outbox": "Outbox", + "nav.jobs": "Jobs", + "nav.brands": "Brands", + "nav.policy": "Opportunity policy", + "nav.playbooks": "Playbooks", + "nav.insights": "Insights", + "nav.benchmark": "Benchmark", + "nav.utm": "UTM", + "nav.more": "More", + "nav.moreTitle": "More", + "nav.users": "Islanders", + "nav.usage": "Usage & plans", + "nav.profile": "Profile", + "nav.invite": "Invites", + "nav.settings": "Settings", + "nav.logout": "Log out", + "nav.navigate": "Navigate", + "navGroup.workflow": "Workflow", + "navGroup.accounts": "Accounts & brands", + "navGroup.growth": "Growth tools", + + "workspace.label": "Workspace", + "workspace.default": "Default", + "workspace.new": "+ New workspace", + "workspace.newPrompt": "New workspace name", + + "common.save": "Save", + "common.saving": "Saving…", + "common.cancel": "Cancel", + "common.close": "Close", + "common.loading": "Loading…", + "common.retry": "Retry", + "common.back": "Back", + "common.delete": "Delete", + "common.edit": "Edit", + "common.search": "Search", + "common.confirm": "Confirm", + "common.optional": "Optional", + "common.success": "Saved", + "common.error": "Something went wrong", + "common.yes": "Yes", + "common.no": "No", + + "help.open": "Help", + "help.kicker": "This page", + "help.section.what": "What this page is for", + "help.section.how": "How to use it", + "help.section.tips": "Tips", + "help.section.related": "Related", + "help.shortcutHint": "Open via the “?” next to the page title. Shortcut: ? toggles; Esc closes.", + + "help.page.generic.title": "Lapras console", + "help.page.generic.what": "Your work desk. Use the sidebar (or mobile dock) to switch features; the top bar has usage, notifications, and this help.", + "help.page.generic.step1": "Pick a task from the nav (patrol, radar, outbox, …).", + "help.page.generic.step2": "Open Help from the top bar or press ? when you need context.", + "help.page.generic.step3": "Settings, profile, and plans live in the account menu.", + "help.page.generic.tips": "Help never changes your data; open and close anytime.", + + "help.page.today.title": "Today", + "help.page.today.what": "Daily dashboard: opportunities, patrol queue, outbox pulse, and account health so you know what to do first.", + "help.page.today.step1": "Check the opportunities summary; open Radar if there are leads.", + "help.page.today.step2": "Clear pending patrol replies.", + "help.page.today.step3": "Track failed or active Outbox items from here.", + "help.page.today.tips": "When there are no opportunities, the summary guides you to Opportunity policy or watches.", + + "help.page.crew.title": "Accounts (Crew)", + "help.page.crew.what": "Connected Threads accounts, health, and usability. Publishing and outreach start from accounts here.", + "help.page.crew.step1": "Connect at least one usable account via OAuth.", + "help.page.crew.step2": "Check connection and health (avoid auto-send on throttle).", + "help.page.crew.step3": "Tune AI and dev options under Settings if needed.", + "help.page.crew.tips": "Warn/throttle health blocks automatic public sends; copy manually instead.", + + "help.page.studio.title": "Studio", + "help.page.studio.what": "Draft posts, personas, inspiration, and plays; finished work goes to Outbox to publish.", + "help.page.studio.step1": "Pick a persona or brand voice, then draft.", + "help.page.studio.step2": "Use inspiration/mimic tools, then edit by hand.", + "help.page.studio.step3": "Send to Outbox and confirm the schedule there.", + "help.page.studio.tips": "Studio drafts are not published until Outbox succeeds.", + + "help.page.scout.title": "Topic ideas", + "help.page.scout.what": "Find lively Threads topics to join, draft a reply, and mark done. To find people looking for your service, use Demand (daily watch or Explore now).", + "help.page.scout.step1": "Enter topic keywords, generate queries, then edit them.", + "help.page.scout.step2": "Confirm search, then work the queue sorted by post time.", + "help.page.scout.step3": "Draft, open Threads to reply, mark done.", + "help.page.scout.tips": "Topics = content ideas. Demand = finding customers. Use the Demand page for leads.", + + "help.page.radar_today.title": "Demand patrol", + "help.page.radar_today.what": "Run a scheduled or immediate patrol to find pains your product can solve, or new posts. Keep or discard after you read the reason.", + "help.page.radar_today.step1": "Check the patrol desk: whether daily patrol is on, when it last ran, and run one now.", + "help.page.radar_today.step2": "Read why it was recommended. Keep a fit, discard the rest.", + "help.page.radar_today.step3": "Add to contacts only if you want to follow that person. Contacts are optional.", + "help.page.radar_today.tips": "Immediate and daily patrol can both stay on. Turning one off does not hide the other.", + + "help.page.radar_watches.title": "Patrol setup", + "help.page.radar_watches.what": "Pick a product and keywords. Daily patrol runs on a schedule; you can also run one immediately. Results land on Demand.", + "help.page.radar_watches.step1": "Choose brand and product, then fill the pain map.", + "help.page.radar_watches.step2": "Add terms buyers type and excludes.", + "help.page.radar_watches.step3": "Leave daily patrol on, or hit Run now.", + "help.page.radar_watches.tips": "Active slots are plan-capped; pause one to free a slot.", + + "help.page.crm_board.title": "Contact management", + "help.page.crm_board.what": "A searchable, filterable work list of contacts accepted from Today’s demand, with notes, wins, and timelines.", + "help.page.crm_board.step1": "On Today’s demand, hit Add to CRM; people land in New.", + "help.page.crm_board.step2": "Open a contact to move stage, note, or flag follow-up.", + "help.page.crm_board.step3": "Report a win when you close (amount optional).", + "help.page.crm_board.tips": "Remove contacts you no longer need from the work list; opportunity, touch, and conversion history remains.", + + "help.page.crm_followups.title": "Follow-ups", + "help.page.crm_followups.what": "Due revisits: done, snooze, or AI draft for a manual message.", + "help.page.crm_followups.step1": "Check due date and status (including escalated).", + "help.page.crm_followups.step2": "Generate an AI draft if you need copy, then send yourself.", + "help.page.crm_followups.step3": "Mark done or snooze three days.", + "help.page.crm_followups.tips": "Nothing is auto-messaged; drafts are for copy only.", + + "help.page.crm_stats.title": "Conversion stats", + "help.page.crm_stats.what": "Conversion by term, reply variant, and source. No ranking when samples are thin.", + "help.page.crm_stats.step1": "Read absolute counts first.", + "help.page.crm_stats.step2": "Ignore ranking on “insufficient sample” rows.", + "help.page.crm_stats.step3": "Tune watches and reply style from what you learn.", + "help.page.crm_stats.tips": "Rates stay hidden on small samples on purpose.", + + "help.page.outbox.title": "Outbox", + "help.page.outbox.what": "Schedule and send queue — last stop before Threads publish.", + "help.page.outbox.step1": "Review pending and failed items.", + "help.page.outbox.step2": "Retry failures or edit the draft.", + "help.page.outbox.step3": "Track long jobs under Jobs as well.", + "help.page.outbox.tips": "Health throttle blocks automatic send.", + + "help.page.jobs.title": "Jobs", + "help.page.jobs.what": "Background work (scans, sweeps, analysis) with progress and outcomes.", + "help.page.jobs.step1": "Scan status: queued / running / success / failed.", + "help.page.jobs.step2": "Open a job for the progress summary.", + "help.page.jobs.step3": "On failure, retry from the related feature.", + "help.page.jobs.tips": "Radar “Sweep now” creates a radar_sweep job here.", + + "help.page.brands.title": "Brands", + "help.page.brands.what": "Maintain brands and products so demand search knows what you sell and which pains you solve.", + "help.page.brands.step1": "Maintain brand and product basics.", + "help.page.brands.step2": "Add product contexts, pain points, match tags, and capability terms.", + "help.page.brands.step3": "Set pricing, regions, forbidden words, cases, and tone under the standalone Opportunity policy page.", + "help.page.brands.tips": "Richer product data improves query preprocessing and product matching.", + + "help.page.policy.title": "Opportunity policy", + "help.page.policy.what": "Shared qualification and reply policy, kept separate from brand and case management.", + "help.page.policy.step1": "Set services, pricing, and service areas.", + "help.page.policy.step2": "Add forbidden words, cases, FAQs, availability, and tone.", + "help.page.policy.step3": "Save once; qualification, watch activation, and reply generation use this policy.", + "help.page.policy.tips": "Policy is workspace-wide. Maintain individual brands and products on Brands.", + + "help.page.playbooks.title": "Playbooks", + "help.page.playbooks.what": "Share or adopt patrol briefs, personas, and play templates.", + "help.page.playbooks.step1": "Browse templates.", + "help.page.playbooks.step2": "Adopt into your workspace and edit.", + "help.page.playbooks.step3": "Use them in Patrol or Studio.", + "help.page.playbooks.tips": "Templates are starting points — rewrite for your brand.", + + "help.page.insights.title": "Insights", + "help.page.insights.what": "Post and engagement performance so you can double down on what works.", + "help.page.insights.step1": "Sync or review recent post metrics.", + "help.page.insights.step2": "Feed winners back into Studio.", + "help.page.insights.step3": "Compare with Benchmark when available.", + "help.page.insights.tips": "Sync lag depends on the platform; not second-level live.", + + "help.page.benchmark.title": "Benchmark", + "help.page.benchmark.what": "Anonymous site-wide medians when sample size is enough.", + "help.page.benchmark.step1": "Read metrics that have samples.", + "help.page.benchmark.step2": "Don’t over-read thin samples.", + "help.page.benchmark.step3": "Adjust content strategy from the gap.", + "help.page.benchmark.tips": "Meaningful medians usually need sample size ≥ 5.", + + "help.page.utm.title": "UTM", + "help.page.utm.what": "Build UTM links so you can attribute traffic later.", + "help.page.utm.step1": "Fill campaign and source params.", + "help.page.utm.step2": "Use the link in posts or DMs.", + "help.page.utm.step3": "Match results in your analytics tool.", + "help.page.utm.tips": "Keep naming consistent for clean reports.", + + "help.page.settings.title": "Settings", + "help.page.settings.what": "AI providers, search keys, and interface preferences.", + "help.page.settings.step1": "Confirm AI keys (platform or BYOK).", + "help.page.settings.step2": "Search keys are optional — use your own or the platform default.", + "help.page.settings.step3": "Return to a feature page and verify.", + "help.page.settings.tips": "Bad keys show up as generation/scan failures in usage or jobs.", + + "help.page.profile.title": "Profile", + "help.page.profile.what": "Your member account, password, and basic prefs.", + "help.page.profile.step1": "Update display name and basics.", + "help.page.profile.step2": "Change password if needed.", + "help.page.profile.step3": "Language/theme live in UI prefs.", + "help.page.profile.tips": "Member profile is separate from Threads connections (Crew).", + + "help.page.invite.title": "Invites", + "help.page.invite.what": "Invite links, downline relations, and rewards info.", + "help.page.invite.step1": "Copy and share your invite link.", + "help.page.invite.step2": "Review established relations.", + "help.page.invite.step3": "Rewards follow in-product rules.", + "help.page.invite.tips": "Don’t spam invites; abuse can suspend accounts.", + + "help.page.usage.title": "Usage & plan", + "help.page.usage.what": "Credits, meter usage, and plan caps.", + "help.page.usage.step1": "Check each meter.", + "help.page.usage.step2": "Near limits, upgrade or use BYOK.", + "help.page.usage.step3": "Plan details are on the Plans page.", + "help.page.usage.tips": "BYOK usually counts calls only (see on-page labels).", + + "help.page.usage_plans.title": "Plans", + "help.page.usage_plans.what": "Compare and choose a paid plan.", + "help.page.usage_plans.step1": "Compare caps and features.", + "help.page.usage_plans.step2": "Start checkout for a plan.", + "help.page.usage_plans.step3": "Confirm entitlements under Usage.", + "help.page.usage_plans.tips": "Price and caps are those shown at checkout.", + + "help.page.usage_checkout.title": "Checkout", + "help.page.usage_checkout.what": "Pay for the selected plan.", + "help.page.usage_checkout.step1": "Confirm plan and amount.", + "help.page.usage_checkout.step2": "Complete payment.", + "help.page.usage_checkout.step3": "Return to Usage to verify.", + "help.page.usage_checkout.tips": "Keep the receipt if payment fails and contact support.", + + "help.page.admin_users.title": "Members admin", + "help.page.admin_users.what": "Admin tools for members, suspend, and roles.", + "help.page.admin_users.step1": "Search or browse members.", + "help.page.admin_users.step2": "Change status or role carefully.", + "help.page.admin_users.step3": "Record reasons for sensitive actions.", + "help.page.admin_users.tips": "Admins only; mistakes can lock others out.", + + "api.err.unknown": "Something went wrong. Please try again.", + "api.err.studioValidation": "Invalid data. Please check and try again.", + "api.err.crawlerSession": "Sync the Chrome session in Settings, then try again.", + "api.err.network": "Cannot reach the server. Check your network or if the API is running.", + "api.err.400001": "Invalid request", + // Same sentence as backend 400003 / ValidatePasswordPolicy + "api.err.400003": "Password must be at least 12 characters with upper, lower, digit, and symbol", + "password.policy.hint": "Password must be at least 12 characters with upper, lower, digit, and symbol", + "password.policy.minLen": "Password must be at least 12 characters with upper, lower, digit, and symbol", + "password.policy.needUpper": "Password must be at least 12 characters with upper, lower, digit, and symbol", + "password.policy.needLower": "Password must be at least 12 characters with upper, lower, digit, and symbol", + "password.policy.needDigit": "Password must be at least 12 characters with upper, lower, digit, and symbol", + "password.policy.needSymbol": "Password must be at least 12 characters with upper, lower, digit, and symbol", + + "api.err.400004": "Invalid or expired verification code", + "api.err.400020": "You cannot suspend your own account", + "api.err.400021": "Cannot demote or suspend the last active admin", + "auth.unreachable": "Can't reach the server, so your sign-in state is unknown. Check your connection and retry.", + "api.err.401001": "Please sign in", + "api.err.401002": "Session expired. Please sign in again.", + "api.err.401003": "Account not found. Please sign in again.", + "api.err.401010": "Invalid email or password", + "api.err.403001": "Account suspended", + "api.err.403002": "Admin access required", + "api.err.404001": "Not found", + "api.err.404002": "This email is not registered", + "api.err.409001": "This email is already registered", + "api.err.402001": "Monthly platform credits exhausted. Upgrade or wait for next month.", + "usage.err.meterCap": "This feature hit its monthly credit cap. Try another feature or upgrade.", + "usage.err.platformCapacity": "Platform AI/search is busy or rate-limited. Try again later, or add your own API key (BYOK) in Settings.", + "api.err.500000": "Server error. Please try again later.", + "api.err.timeoutAI": "AI timed out. Shorten structure notes, or pick a faster model in Settings", + "api.err.501000": "This feature is not implemented yet", + "api.err.501010": "This capability is not available yet; a later release will ship it", + "api.err.400100": "Some fields do not meet the rules. Please review and resubmit", + + "api.ok.verifySent": "Verification code sent. Please check your email.", + "api.ok.verifyIssued": "Verification code issued", + "api.ok.resetSent": "Reset email sent. Please check your inbox.", + "api.ok.passwordUpdated": "Password updated", + "api.ok.loggedOut": "Signed out", + "api.ok.unbound": "Unbound", + + "topbar.notifications": "Notifications", + "topbar.unread": "{n} unread", + "topbar.markAllRead": "Mark all read", + "topbar.noNotifications": "No notifications", + "topbar.jobsCenter": "Job center", + "topbar.moreOlder": "{n} older", + "topbar.account": "Account menu", + + "role.admin": "Admin", + "role.member": "Member", + "role.verified": "Verified", + "role.unverified": "Unverified", + + "login.title": "Sign in to Lapras", + "login.email": "Email", + "login.password": "Password", + "login.showPassword": "Show password", + "login.hidePassword": "Hide password", + "login.submit": "Sign in", + "login.submitting": "Signing in…", + "login.forgot": "Forgot password?", + "login.mockHint": "Admin demo@harbor.local / demo · member alice@harbor.local / alice", + "login.liveHint": "Live backend: admin@haixun.local / admin123 (gateway :8888)", + + "forgot.title": "Forgot password", + "forgot.submit": "Send reset link", + "forgot.submitting": "Sending…", + "forgot.back": "Back to sign in", + "forgot.hint": "Enter your registered email. We'll send a reset link and code.", + "forgot.mockMail": "Reset email", + "forgot.openReset": "Enter code / reset password", + "forgot.retry": "Try again", + "forgot.nextStep": "Check your email for the code or link, then open the reset page to set a new password.", + + "reset.title": "Set new password", + "reset.hint": "Enter email, verification code (from the email), and a new password.", + "reset.code": "Verification code", + "reset.codePh": "6-digit code from email", + "reset.needEmail": "Email is required", + "reset.needCode": "Verification code is required", + "reset.newPassword": "New password", + "reset.confirm": "Confirm password", + "reset.submit": "Update password", + "reset.submitting": "Updating…", + "reset.missingToken": "Missing token. Please request a new reset link.", + + "verify.title": "Verify email", + "verify.pending": "Not verified", + "verify.body": "You're signed in, but features stay locked until you verify your email with the 6-digit code.", + "verify.code": "Verification code", + "verify.submit": "Verify", + "verify.submitting": "Verifying…", + "verify.resend": "Resend code", + "verify.sending": "Sending…", + "verify.logout": "Log out", + "verify.mockMail": "Verification email", + "verify.mockHint": "Enter the code:", + "verify.after": "After verification you can use Today, Studio, Patrol, and more.", + + "settings.title": "Settings", + "settings.loadFail": "Could not load settings. Refresh to try again.", + "settings.localeCurrency": "Language & currency", + "settings.locale": "Interface language", + "settings.currency": "Display currency", + "settings.currencyHint": "Display only; plan billing stays in TWD.", + "settings.localeSaved": "Language and currency updated", + "settings.appearance": "Appearance", + "settings.theme": "Theme", + "settings.themeLight": "Light", + "settings.themeDark": "Dark", + "settings.themeSystem": "System", + "settings.themeHint": "Light, dark, or follow system appearance.", + "settings.themeSaved": "Theme updated", + "settings.themeToLight": "Switch to light", + "settings.themeToDark": "Switch to dark", + "settings.dataSource": "Data source", + "settings.dataSourceHint": + "Business data always uses live API. Mock keeps only a few local modules (e.g. invite). Prefer Live + gateway.", + "settings.dataSourceMock": "Mock (reduced)", + "settings.dataSourceLive": "Live (backend API)", + "settings.dataSourceCurrent": "Current", + "settings.dataSourceSwitchedLive": + "Switched to Live — re-login with backend account (admin@haixun.local / admin123)", + "settings.dataSourceSwitchedMock": "Switched back to Mock (non-invite still hits live)", + "settings.dataSourceLiveNeedGateway": "Requires gateway on :8888 (or Vite proxy /api)", + "settings.ai": "AI", + "settings.search": "Search", + "settings.threads": "Threads OAuth (platform)", + "settings.threadsHint": + "App ID/Secret live in gateway platform config (yaml/env); Secret is never shown. Paste Callback into Meta app settings and connect via the public https site.", + "settings.threadsProvider": "Mode", + "settings.threadsProviderFake": "Fake (dev OAuth flow)", + "settings.threadsProviderMeta": "Meta production", + "settings.threadsConfigured": "App credentials", + "settings.threadsConfiguredYes": "Configured", + "settings.threadsConfiguredNo": "Not set", + "settings.threadsCallback": "OAuth Callback URL (redirect_uri)", + "settings.threadsCallbackHint": "Paste this exact URL into Meta App → Valid OAuth Redirect URIs (https required)", + "settings.threadsCopy": "Copy", + "settings.threadsCopied": "Callback URL copied", + "settings.threadsCopyFail": "Copy failed — select manually", + "settings.threadsPublicWeb": "Public site origin", + "settings.threadsGoCrew": "Open Crew to connect", + "settings.member": "Account & sign-in", + "settings.usageCard": "AI / search quota", + "settings.usageCardHint": "Platform credits and plan caps; BYOK does not use platform credits.", + "settings.viewUsage": "View usage", + "settings.editProfile": "Edit profile", + "settings.mockLogin": "demo@harbor.local / demo", + "settings.memberHint": "Sign-in account, display name, and notification prefs.", + + "usage.title": "Usage & plans", + "usage.desc": "See platform credits this month, per-feature usage, and change plans.", + "usage.creditsUsed": "Credits used this month", + "usage.remaining": "{n} left", + "usage.percentUsed": "{n}% used", + "usage.breakdown": "Breakdown", + "usage.plans": "Plans", + "usage.current": "Current", + "usage.inUse": "Active", + "usage.switchMock": "Switch plan", + "usage.perMonth": "credits / mo", + "usage.ledger": "Recent usage", + "usage.emptyTitle": "No usage this month", + "usage.emptyDesc": "After you create, patrol, or generate images, usage events show up here.", + "usage.planNote": "Resets each calendar month; unused credits do not roll over.", + "usage.switched": "Switched to {name}", + "usage.meter.times": "{count} runs · {credits} credits", + "usage.meter.timesShort": "{n} runs", + "usage.meter.pt": "pt", + "usage.meter.over": "over", + "usage.meter.locked": "locked", + "usage.meter.cap": "Cap {credits}/{cap} credits", + "usage.side.aiCredits": "AI credits used (copy + research + image)", + "usage.side.searchCredits": "Search credits used", + "usage.byok.title": "BYOK usage", + "usage.byok.hint": "Calls only; does not affect platform credits or progress bars.", + "usage.plan.free.blurb": "Enough to try the full creation flow", + "usage.plan.starter.blurb": "Small teams posting and patrolling daily", + "usage.plan.pro.blurb": "Multi-account, heavy AI and research", + + "profile.title": "Profile", + "profile.desc": "Manage display name, avatar, and password.", + "profile.accountStatus": "Account status", + "profile.basic": "Basics", + "profile.avatar": "Avatar", + "profile.avatarUpload": "Upload avatar", + "profile.avatarRemove": "Remove avatar", + "profile.avatarHint": "JPG / PNG / WebP, up to 5MB. Pick a file then Save basic info; Remove clears immediately.", + "profile.avatarSaved": "Avatar updated", + "profile.avatarCleared": "Avatar removed", + "profile.avatarFail": "Could not read image", + "profile.displayName": "Display name", + "profile.bio": "Bio (optional)", + "profile.timezone": "Timezone", + "profile.notifyEmail": "Email notifications", + "profile.password": "Change password (optional)", + "profile.currentPassword": "Current password", + "profile.newPassword": "New password", + "profile.confirmPassword": "Confirm new password", + "profile.emailVerified": "Email verified", + "profile.emailUnverified": "Email not verified", + "profile.roleAdmin": "Admin", + "profile.roleMember": "Member", + "profile.loginEmail": "Sign-in email: {email}", + "profile.verifiedAt": " · verified {time}", + "profile.goVerify": "Verify email", + "profile.roleTags": "Role tags: {labels}", + "profile.roleNote": "Roles are assigned by the system; re-verify after changing email.", + "profile.saveBasic": "Save profile", + "profile.updatePassword": "Update password", + "profile.saved": "Profile saved", + "profile.savedUnverified": "Saved. Verify your email before using features.", + "profile.saveFail": "Could not save", + "profile.needNewPassword": "Enter a new password", + "profile.passwordMismatch": "New passwords do not match", + "profile.needCurrentPassword": "Enter your current password", + "profile.wrongCurrentPassword": "Current password is incorrect", + "profile.passwordUpdated": "Password updated", + "profile.passwordFail": "Could not change password", + "profile.listJoin": ", ", + "profile.tenantUid": " · tenant {tenant} · uid {uid}", + "profile.inviteBadge": "Invite code", + "profile.inviteHint": "Share with friends to join; later events can use invite relations.", + "profile.gotoInvite": "View invites", + + "invite.title": "Invites", + "invite.desc": "Invite codes and relations; admin view is a tree.", + "invite.tabs": "Invite views", + "invite.tab.mine": "My invites", + "invite.tab.tree": "Tree", + "invite.myCode": "My invite code", + "invite.codeLabel": "Invite code", + "invite.copyCode": "Copy", + "invite.copied": "Copied", + "invite.copyFail": "Copy failed", + "invite.stats": "{direct} direct · {total} in chain", + "invite.rewards.summary": "Reward points total {total} · this month {month} / cap {cap}", + "invite.rewards.title": "Reward history", + "invite.upline": "Invited by", + "invite.downlines": "Direct invites · {n}", + "invite.noUpline": "No inviter", + "invite.noDownline": "None yet", + "invite.directN": "{n} direct", + "invite.claimHint": "If you didn’t enter a code at signup, add your inviter’s code here (cannot change later).", + "invite.claimLabel": "Invite code", + "invite.claimPh": "e.g. HX-DEMO01", + "invite.claimSubmit": "Bind", + "invite.claimOk": "Bound to {name}", + "invite.claimOkGeneric": "Inviter bound", + "invite.claimFail": "Could not bind", + "invite.claimLocked": "Already bound. Contact an admin to change.", + "invite.loadFail": "Failed to load", + "invite.treeEmpty": "No data", + "invite.treeSearch": "Search", + "invite.treeSearchPh": "Name / email / code", + "invite.treeCount": "{n} people", + "invite.treeMatchCount": "{n} matches · {total} total", + "invite.treeNoMatch": "No matches", + "invite.treeNoMatchHint": "Try another keyword", + "invite.clearSearch": "Clear", + "invite.moveTitle": "Reassign", + "invite.newParent": "Inviter", + "invite.root": "(None · standalone)", + "invite.confirmMove": "Confirm", + "invite.moved": "Reassigned “{name}” → “{parent}”", + "invite.moveFail": "Failed", + "invite.openFromAdmin": "Invites", + "invite.parentField": "Invited by", + "invite.orgPickRoot": "Pick a starting root (includes self-joined)", + "invite.orgRoots": "Roots", + "invite.orgPath": "Path", + "invite.orgChainN": "{n} in chain", + "invite.orgFocusStats": "{direct} direct · {total} in chain", + "invite.orgDirects": "Direct invites · {n}", + "invite.orgNoDirects": "No direct invites", + "invite.err.notFound": "Member not found", + "invite.err.notLoggedIn": "Not signed in", + "invite.err.needAdmin": "Admin access required", + "invite.err.memberNotFound": "Islander not found", + "invite.err.selfParent": "Cannot set yourself as inviter", + "invite.err.parentNotFound": "Inviter not found", + "invite.err.cycle": "Cannot place under your own invite chain (would create a cycle)", + "invite.err.repoMissing": "Invite data layer not loaded — please refresh the page", + "invite.err.alreadyBound": "You already have an inviter", + "invite.err.codeRequired": "Enter an invite code", + "invite.err.codeNotFound": "Invite code not found", + "invite.err.codeSelf": "You can’t use your own code", + + "admin.users.title": "Islanders", + "admin.users.desc": "Manage islander accounts, roles, plans, and unlimited flags.", + "admin.users.needAdmin": "Admin access required", + "admin.users.list": "Islanders · {n}", + "admin.users.detail": "Islander detail", + "admin.users.pick": "Select an islander", + "admin.users.search": "Search name / uid", + "admin.users.searchPh": "Name, email, or uid", + "admin.users.create": "Add islander", + "admin.users.suspend": "Suspend", + "admin.users.unsuspend": "Restore", + "admin.users.suspended": "Suspended", + "admin.users.active": "Active", + "admin.users.onboarding": "First-run", + + "crew.title": "Accounts", + "crew.tab.accounts": "Accounts", + "crew.tab.personas": "Personas", + "crew.connect": "Connect account", + "crew.connecting": "Connecting…", + "crew.tokenRenewHint": "Tokens auto-renew via background jobs (~day 30). Check Jobs; no manual refresh.", + "crew.empty": "No accounts yet", + "crew.loadFail": "Could not load accounts", + "crew.unusable": "Unavailable", + "crew.expires": "Expires {time}", + "crew.lastRefresh": "Last extended {time}", + "crew.refreshSession": "Extend token", + "crew.refreshSessionHint": "Uses stored auth to refresh the token — no OAuth page. On failure, reconnect.", + "crew.session.ok": "Token valid", + "crew.session.soon": "Expiring soon", + "crew.session.expired": "Token expired", + "crew.session.unknown": "Expiry unknown", + "crew.connection.connected": "Connected", + "crew.connection.error": "Error", + "crew.connection.disconnected": "Disconnected", + "crew.connection.unknown": "Unknown", + "crew.health.needsReconnect": "Reconnect required", + "crew.health.needsReconnectHint": "Token unusable — use Connect account (not Extend token)", + "crew.health.disconnectedHint": "Unlinked", + "crew.health.expiredHint": "Extend token or reconnect", + "crew.opHealthScore": "Health {n}", + "crew.msg.refreshed": "@{user} token extended with stored auth (~60 days)", + "crew.msg.refreshedAll": "Extended {n} account tokens with stored auth", + "crew.msg.refreshFail": "Extend failed (reconnect if token is invalid)", + "crew.msg.oauthOk": "Threads connected; token renew scheduled ~day 30 (see Jobs)", + "crew.msg.oauthFail": "OAuth connection failed", + "crew.msg.oauthUrlFail": "Could not get authorize URL. Try again or check platform Threads settings.", + "crew.msg.deleted": "Removed @{user}", + "crew.confirmDelete": "Delete account @{user}?\nIt can no longer be used as lead / cast.", + + "today.findTopic": "Find topics", + "today.refreshTopics": "Refresh topics", + "today.reload": "Reload", + "today.loadFail": "Could not load Today", + "today.trendsFail": "Could not refresh topics (quota or search not set)", + "today.trendsUpdated": "Updated {n} topics", + "today.syncPosts": "Sync own posts", + "today.syncPostsFail": "Could not sync posts", + "today.needAccount": "Connect a Threads account first", + "today.outcome.title": "This week's outcomes", + "today.outcome.reach": "Reach", + "today.outcome.conversations": "Conversations", + "today.outcome.follows": "Follows", + "today.outcome.followsHint": "Possibly related, no strong signal yet", + "today.outcome.followsConfirmedHint": " / confirmed {n}", + "today.outcome.conversions": "Conversions", + "today.outcome.emptyHint": "No patrol outreach outcomes this week yet — go give patrol a try?", + "today.checkup.empty": "This week's checkup hasn't been generated yet. It runs automatically next Monday in your timezone.", + "today.checkup.prefix": "Checkup: ", + "today.pendingReplies": "Pending replies", + "today.pendingRepliesN": "Pending · {n}", + "today.newThread": "Compose", + "today.metricsAria": "Today metrics", + "today.metric.pending": "Pending", + "today.metric.pendingHint": "Patrol queue", + "today.metric.doneGoal": "Done / goal", + "today.metric.doneGoalHint": "Patrol completions today / goal (mark published counts)", + "today.metric.sentToday": "Sent today", + "today.metric.running": "{n} in progress", + "today.metric.sentDone": "Completed sends", + "today.metric.failed": "Send issues", + "today.metric.needAction": "Needs action", + "today.metric.ok": "All good", + "today.metric.mentions": "Mentions", + "today.metric.mentionsHint": "Pending mentions", + "today.pending.title": "Patrol pending · {n}", + "today.pending.empty": "Nothing pending — run a patrol", + "today.goScout": "Go patrol", + "today.pending.more": "{n} more →", + "today.pending.handle": "Handle in patrol", + "today.pending.start": "Start handling", + "today.topics.title": "Find topics", + "today.topics.empty": "No topics yet — tap Refresh topics", + "today.goStudio": "Open inspire", + "today.heat": "Heat {n}", + "today.topicAngle": "Good opening angle", + "today.moreInspire": "More inspiration", + "today.useTopic": "Brainstorm topic", + "today.outbox.title": "Today's outbox", + "today.outbox.empty": "No sends today. Try", + "today.outbox.emptyMid": ", then check", + "today.outbox.emptyEnd": ".", + "today.outbox.summary": "Done {sent} · In progress {running} · Issues {failed}", + "today.badge.failed": "Failed", + "today.badge.scheduling": "Scheduled", + "today.badge.sending": "Sending", + "today.badge.drafted": "Drafted", + "today.openOutbox": "Open outbox", + "today.accounts.title": "Account performance", + "today.accounts.empty": "No stats yet — sync own posts", + "today.postsCount": "{n} posts", + "today.views": "Views", + "today.likes": "Likes", + "today.repliesShort": "Replies", + "today.fullInsights": "Full insights · MoM charts", + "today.viewPosts": "View posts", + "today.manageAccounts": "Manage accounts", + + + "outbox.title": "Outbox", + "outbox.tabsAria": "Outbox tabs", + "outbox.tab.active": "Active", + "outbox.tab.history": "History", + "outbox.empty": "No outbox items", + "outbox.activeEmpty": "Nothing in progress", + "outbox.historyEmpty": "No history yet", + "outbox.historyN": "History ({n})", + "outbox.backActive": "Back to active ({n})", + "outbox.progress": "Progress {progress}", + "outbox.detail": "Details", + "outbox.deleting": "Deleting…", + "outbox.confirmDelete": "Delete outbox item “{title}”?\nThis cannot be undone.", + "outbox.deleted": "Deleted “{title}”", + "outbox.deleteFail": "Delete failed", + "outbox.loadFail": "Failed to load outbox", + "outbox.status.scheduling": "Scheduling", + "outbox.status.active": "Sending", + "outbox.status.completed": "Completed", + "outbox.status.partial_failed": "Partial failure", + "outbox.status.cancelled": "Cancelled", + "outbox.detail.missingId": "Missing id", + "outbox.detail.notFound": "Outbox item not found", + "outbox.detail.loadFail": "Could not load this outbox item", + "outbox.detail.loading": "Loading…", + "outbox.detail.sendingHint": "Publishing to Threads (often 10–30s). This page updates automatically…", + "outbox.detail.doneHint": "Published to Threads successfully.", + "outbox.detail.markAllOk": "Mark all success", + "outbox.detail.markRootFail": "Mark root failed", + "outbox.detail.processing": "Working…", + "outbox.detail.delete": "Delete this", + "outbox.detail.back": "Back to list", + "outbox.detail.root": "Root post", + "outbox.detail.replyN": "Reply {n}", + "outbox.detail.retry": "Retry", + "outbox.detail.opFail": "Action failed", + "outbox.step.published": "Published", + "outbox.step.failed": "Failed", + "outbox.step.publishing": "Publishing…", + "outbox.step.scheduled": "Scheduled", + "outbox.step.blocked": "Blocked", + + "studio.title": "Studio", + "studio.account": "Account", + "studio.persona": "Persona", + "studio.personaReady": "Persona ready", + "studio.personaNotReady": "Persona not ready", + "studio.tab.posts": "My posts", + "studio.tab.mentions": "Mentions @", + "studio.tab.compose": "Compose", + "studio.tab.plays": "Plays", + "studio.tab.inspire": "Inspire", + "studio.tab.insights": "Insights", + + "mentions.hint": "Who @ you. {n} pending. You can change account/persona per item (defaults from top bar).", + "mentions.scoutLink": "Patrol outreach", + "mentions.empty": "No mentions", + "mentions.emptyHint": "Click “Sync from Threads” to pull posts/replies/quotes that @ you. Re-connect if missing threads_manage_mentions.", + "mentions.needAccount": "Select a Threads account in the top bar first", + "mentions.sync": "Sync from Threads", + "mentions.syncing": "Syncing…", + "mentions.syncDone": "Synced {n} mentions", + "mentions.syncFail": "Sync failed — reconnect Threads with threads_manage_mentions scope", + "mentions.openThread": "Open post", + "mentions.status.pending": "Pending", + "mentions.status.replied": "Replied", + "mentions.status.skipped": "Skipped", + "mentions.reply": "Reply", + "mentions.skip": "Skip", + "mentions.draftLabel": "Reply draft", + "mentions.repliedPrefix": "Replied: {text}", + "mentions.needPersona": "Pick a ready persona before AI draft", + "mentions.fail": "Failed", + "mentions.marked": "Mention marked as replied", + "mentions.markReplied": "Mark as replied", + "mentions.markingReplied": "Marking…", + "mentions.withImages": " · {n} images", + + "compose.hint": "Publish only: write body and send to Outbox (not a play). For multi-account threads use", + "compose.hintEnd": ".", + "compose.playsLink": "Plays", + "compose.personaOff": "Persona not ready: AI tools (mimic / analyze) disabled.", + "compose.title": "Title (optional)", + "compose.titlePh": "Helps identify in Outbox", + "compose.body": "Body", + "compose.bodyPh": "Write your post…", + "compose.bodyCount": "{n} characters", + "compose.bodyLongWarning": "The full draft is preserved, but it may exceed the Threads single-post limit", + "compose.topicTag": "Topic tag (Threads)", + "compose.topicTagPh": "e.g. petshow (optional #)", + "compose.topicTagHint": "One topic tag per post, 1–50 chars, no . or &. You can also put #tag in the body.", + "compose.whoCanReplyHint": "Applied when the post is published. Threads cannot change this on an existing post via API.", + "compose.tool.mimic": "Mimic", + "compose.tool.viral": "Viral analysis", + "compose.tool.research": "Research", + "compose.tool.image": "Image", + "compose.mimic.title": "Mimic another post", + "compose.mimic.source": "Source text", + "compose.mimic.sourcePh": "Paste the post to mimic…", + "compose.mimic.direction": "New topic or angle (optional)", + "compose.mimic.directionPh": "e.g. Fewer features can make a product easier to use…", + "compose.mimic.directionHint": "This drives the new post. Leave it blank and AI will choose a related but distinctly different angle.", + "compose.mimic.structureNotes": "Structure notes (used in mimic)", + "compose.mimic.structureNotesPh": "From Own posts → Analyze, or paste hooks/structure…", + "compose.mimic.structureNotesHint": "Only the narrative skeleton, turns, and emotional arc are reused; content follows the new direction and selected persona.", + "compose.mimic.broughtAnalysis": "Loaded source + structure analysis — mimic or edit notes", + "compose.mimic.broughtSource": "Loaded source (no structure yet — analyze on Own posts first)", + "compose.mimic.running": "Mimicking in background (you can leave)…", + "compose.mimic.run": "Mimic into body", + "compose.mimic.queued": "Mimic job queued — will fill the body when done", + "compose.mimic.done": "Mimic done — edit as needed", + "compose.mimic.doneWithStructure": "Mimic done (structure applied) — edit as needed", + "compose.mimic.jobFail": "Mimic job failed: {err}", + "compose.viral.title": "Viral analysis", + "compose.viral.hint": "Hooks, structure, and copyable patterns from source or body.", + "compose.viral.source": "Target (empty = use body)", + "compose.viral.running": "Analyzing…", + "compose.viral.run": "Analyze", + "compose.viral.result": "Analysis", + "compose.viral.done": "Viral analysis done", + "compose.viral.needText": "Paste a reference or write the body first", + "compose.research.title": "Research notes", + "compose.research.q": "Keywords", + "compose.research.qPh": "e.g. fragrance-free detergent sensitive skin", + "compose.research.running": "Searching…", + "compose.research.insert": "Insert selected into body", + "compose.research.inserted": "Inserted {n} notes", + "compose.image.title": "Generate image", + "compose.image.prompt": "Scene description", + "compose.image.promptPh": "Empty = summarize from body", + "compose.image.running": "Generating…", + "compose.image.run": "Generate image", + "compose.image.done": "Image added", + "compose.scheduleAt": "Publish at", + "compose.scheduleHint": "Outbox sends at this time; must not be in the past.", + "compose.scheduleHintNow": "Send immediately (timestamp taken at submit).", + "compose.schedulePast": "Scheduled time is in the past. Set to now or a future time.", + "compose.scheduleNow": "Now", + "compose.publish": "Send to Outbox", + "compose.publishing": "Sending…", + "compose.uploadingImages": "Uploading image {n}/{total}…", + "compose.uploadImageFail": "Image upload failed — retry or use a smaller file (≤5MB)", + "compose.waitImageUpload": "Images still uploading — wait a moment.", + "compose.waitImageUploadBtn": "Uploading images…", + "compose.imageUploadNeedRetry": "Some images failed — tap retry on the thumbnail.", + "compose.publishFail": "Send failed", + "compose.fail": "Failed", + "compose.attachN": "{n} images", + "compose.personaStatus": "Persona: {status}", + "compose.ready": "ready", + "compose.notReady": "not ready", + + "posts.sync": "Resync Threads", + "posts.syncing": "Syncing…", + "posts.syncedAt": "Synced {time}", + "posts.notSynced": "Not synced", + "posts.syncDone": "Synced {n} posts from Threads (metrics + replies)", + "posts.syncFail": "Sync failed — reconnect Threads if permissions are missing", + "posts.loadingReplies": "Loading replies…", + "posts.loadRepliesFail": "Could not load replies", + "posts.empty": "No posts yet", + "posts.openThreads": "Open Threads", + "posts.whoCanReply": "Who can reply", + "posts.replyControl.everyone": "Everyone", + "posts.replyControl.accounts_you_follow": "Profiles you follow", + "posts.replyControl.mentioned_only": "Mentioned only", + "posts.replyControl.parent_post_author_only": "Parent post author only", + "posts.replyControl.followers_only": "Followers only", + "posts.replyControlUpdated": "Updated to “{label}”. Open Threads to verify.", + "posts.replyControlFail": "Could not update who can reply", + "posts.replyControlPublishOnly": "Threads can only set who can reply when publishing. Published posts cannot be changed via API — create a new post in Compose.", + "posts.hideReply": "Hide reply", + "posts.unhideReply": "Unhide reply", + "posts.hidingReply": "Working…", + "posts.replyHidden": "Reply hidden on Threads. Open Threads to verify.", + "posts.replyUnhidden": "Reply unhidden on Threads. Open Threads to verify.", + "posts.hideFail": "Hide / unhide failed", + "posts.hiddenBadge": "Hidden", + "posts.insight": "Insight: {text}", + "posts.review": "Review: {text}", + "posts.formulaResult": "Structure analysis", + "posts.analyzedBadge": "Analyzed", + "posts.noText": "(No text / media-only)", + "posts.collapseReplies": "Collapse replies", + "posts.repliesBtn": "Replies ({total}) · pending {pending}", + "posts.replyRoot": "Reply to post", + "posts.analyzing": "Analyzing…", + "posts.reanalyze": "Re-analyze structure", + "posts.analyze": "Analyze structure", + "posts.mimicThis": "Mimic this", + "posts.rootDraft": "Root reply draft", + "posts.filter.pending": "Pending ({n})", + "posts.filter.replied": "Replied ({n})", + "posts.filter.all": "All ({n})", + "posts.noPending": "No pending replies", + "posts.noReplied": "No replied items yet", + "posts.noReplies": "No replies yet", + "posts.status.pending": "Pending", + "posts.status.replied": "Replied", + "posts.likesN": "{n} likes", + "posts.childCount": "{n} child replies", + "posts.mine": "Ours", + "posts.replyThis": "Reply", + "posts.replyAgain": "Reply again", + "posts.replyTo": "Reply to @{user}", + "posts.replyAgainTo": "Reply again to @{user}", + "posts.needPersona": "Pick a ready persona before AI draft", + "posts.genFail": "Generate failed", + "posts.needText": "Generate or type a reply first", + "posts.needAccount": "Pick a Threads account to send", + "posts.sending": "Publishing to Threads…", + "posts.sent": "Sent to Threads as @{user}", + "posts.sentImages": " ({n} images)", + "posts.accountFallback": "account", + "posts.sendFail": "Send failed", + "posts.analyzeDone": "Structure analysis done (manual)", + "posts.analyzeFail": "Analyze failed", + + "wizard.newTitle": "New play", + "wizard.editTitle": "Edit play", + "wizard.prev": "Back", + "wizard.next": "Next", + "wizard.err.topic": "Enter a topic", + "wizard.err.lead": "Select a lead account", + "wizard.err.leadUnusable": "Lead account unavailable", + "wizard.submitFail": "Submit failed", + "wizard.unnamedPlay": "Untitled play", + "wizard.step.topic": "Topic", + "wizard.step.crew": "Cast", + "wizard.step.script": "Script", + "wizard.step.preview": "Preview", + "wizard.step.schedule": "Schedule", + "wizard.step.submit": "Submit", + "wizard.stepperAria": "Wizard steps", + "wizard.topic.title": "1. What is this thread about?", + "wizard.topic.name": "Title (optional)", + "wizard.topic.namePh": "e.g. Weekend coffee", + "wizard.topic.topic": "One-line topic", + "wizard.topic.topicPh": "What should this thread discuss?", + "wizard.topic.aiView": "AI persona view", + "wizard.topic.personaNotReady": "Persona not ready", + "wizard.topic.quickFill": "Quick fill", + "wizard.topic.sampleTitle": "Weekend coffee chat", + "wizard.topic.sampleTopic": "Looking for a reliable café this weekend — lots of outlets, stay-all-day friendly.", + "wizard.crew.title": "2. Cast", + "wizard.crew.lead": "Lead", + "wizard.crew.noUsable": "No usable accounts", + "wizard.crew.unusable": "Unavailable", + "wizard.crew.cast": "Supporting", + "wizard.script.title": "3. Lines", + "wizard.script.persona": "Persona", + "wizard.script.personaNotReady": "Persona not ready", + "wizard.script.root": "Root post", + "wizard.script.replyN": "Reply {n}", + "wizard.script.generating": "Generating…", + "wizard.script.ai": "AI draft", + "wizard.script.account": "Account: {name}", + "wizard.script.noLead": "(no lead)", + "wizard.script.speaker": "Speaker", + "wizard.script.leadTag": "(lead)", + "wizard.script.text": "Copy", + "wizard.script.rootPh": "Root post text…", + "wizard.script.replyPh": "Reply text…", + "wizard.script.addReply": "Add reply step", + "wizard.preview.title": "4. Preview the thread", + "wizard.preview.unknown": "Unknown", + "wizard.preview.unknownAccount": "Unknown account", + "wizard.preview.root": "Root", + "wizard.preview.leadTalk": "lead reply", + "wizard.preview.empty": "(empty)", + "wizard.schedule.title": "5. When to send", + "wizard.schedule.start": "First post (root) time", + "wizard.schedule.interval": "Reply interval (minutes)", + "wizard.schedule.intervalHint": "Relative to previous step; backend adds random jitter so timing is less robotic", + "wizard.submit.title": "6. Submit schedule", + "wizard.submit.body": "Confirm to send “{title}” ({n} steps) to Outbox and publish in order.", + "wizard.submit.unnamed": "Untitled", + "wizard.submit.root": "Root", + "wizard.submit.replyN": "Reply {n}", + "wizard.submit.submitting": "Submitting…", + "wizard.submit.run": "Submit to Outbox", + + "reply.account": "Reply as", + "reply.persona": "Persona", + "reply.notReady": "This persona is not ready for AI draft (you can still type and send).", + "reply.draft": "Reply draft", + "reply.attach": "Attach", + "reply.generating": "Generating…", + "reply.ai": "AI draft", + "reply.sending": "Sending…", + "reply.send": "Send", + + "image.attach": "Attach", + "image.attachFail": "Attach failed", + "image.attachedAria": "Attached images", + "image.alt": "Attachment", + "image.named": "Image {n}", + "image.remove": "Remove image", + "image.full": "Full ({max})", + "image.more": "Add more ({n}/{max})", + "image.uploading": "Uploading", + "image.uploadingN": "Uploading {n} image(s)…", + "image.uploadFail": "Upload failed", + "image.uploadBadUrl": "Invalid upload response", + "image.retry": "Retry", + + "metrics.aria": "Post metrics", + "metrics.like": "Likes", + "metrics.reply": "Replies", + "metrics.repost": "Reposts", + "metrics.quote": "Quotes", + "metrics.view": "Views", + "metrics.share": "Shares", + "metrics.type.quote": "Quote", + "metrics.type.reply": "Reply", + "metrics.type.image": "Image", + "metrics.type.video": "Video", + "metrics.type.carousel": "Carousel", + "metrics.type.repost": "Repost", + "metrics.type.text": "Text", + "metrics.type.post": "Post", + + "jobs.title": "Jobs", + "jobs.desc": "Jobs in three tabs: active, scheduled, and history. Paginated so we never load everything at once.", + "jobs.startDemo": "Create test job", + "jobs.demoHint": "Requires worker; list auto-refreshes while jobs are active.", + "jobs.demoLabel": "Demo test job", + "jobs.demoCreated": "Test job {id}… created — waiting for worker", + "jobs.demoFail": "Could not create test job", + "jobs.template.tokenRenew": "Threads token renew (~every 30 days)", + "jobs.template.tokenRenewCadence": "Runs about every 30 days · no manual action needed", + "jobs.template.tokenRenewBadge": "Recurring · every 30 days", + "jobs.template.personaAnalyzeAccount": "Persona analyze · public posts", + "jobs.template.personaAnalyzeText": "Persona analyze · from text", + "jobs.template.composeMimic": "Mimic post", + "jobs.template.playGenerateScript": "Play full-script AI", + "jobs.template.radarSweep": "Demand patrol", + "jobs.template.unknown": "Other job", + "jobs.stripMore": "+{n} more running…", + "jobs.nextRun": "Next run: {time}", + "jobs.status.pending": "Pending", + "jobs.status.queued": "Scheduled", + "jobs.status.running": "Running", + "jobs.status.succeeded": "Succeeded", + "jobs.status.failed": "Failed", + "jobs.status.cancelled": "Cancelled", + "jobs.status.cancel_requested": "Cancel requested", + "jobs.loadFail": "Could not load jobs", + "jobs.empty": "No jobs yet", + "jobs.empty.active": "No running or ready-to-claim jobs", + "jobs.empty.recurring": "No scheduled / recurring jobs yet", + "jobs.empty.history": "No history yet (succeeded / failed / cancelled)", + "jobs.tabsAria": "Job categories", + "jobs.tab.active": "Active", + "jobs.tab.recurring": "Scheduled", + "jobs.tab.history": "History", + "jobs.recurringHint": "Future schedules (e.g. token renew in ~30 days). When due, they appear under Active.", + "jobs.total": "{n} total", + "jobs.showing": " · showing first {n}", + "jobs.detail": "Details", + "jobs.loadMore": "Load more ({n} more)", + "jobs.notFound": "Job not found", + "jobs.detailLoadFail": "Could not load this job", + "jobs.progress": "Progress {n}%", + "jobs.updated": "Updated {time}", + "jobs.backList": "Back to list", + "jobs.backCompose": "Back to compose", + "jobs.mimicApplyCompose": "Apply to compose", + "jobs.mimicApplyHint": "Mimic finished. Use the button below to return to single-post compose with the draft filled in.", + "jobs.mimicNoResult": "No mimic result found — run mimic again", + "jobs.mimicApplyFail": "Could not apply result", + "jobs.delete": "Delete", + "jobs.deleteConfirm": "Delete “{name}”? This cannot be undone.", + "jobs.deleted": "Job deleted", + "jobs.deleteFail": "Could not delete job", + "jobs.deleteRunningHint": "Running jobs cannot be deleted. Wait until they finish.", + "jobs.retentionHint": "Terminal jobs are auto-removed after about 2 days", + + "plans.title": "Change plan", + "plans.current": "Current plan", + "plans.perMonth": "/mo", + "plans.monthlyCredits": "{n} credits / month", + "plans.usageLink": "Usage", + "plans.inUse": "Active", + "plans.recommended": "Recommended", + "plans.creditsPerMonth": "{n} credits / month", + "plans.manage": "Manage subscription", + "plans.loadFail": "Could not load your subscription", + + "plan.cta.current": "Current plan", + "plan.cta.upgrade": "Upgrade", + "plan.cta.downgrade": "Downgrade", + "plan.cta.switch": "Switch", + + "plan.free.headline": "Enough to try the full product", + "plan.free.bullet1": "{n} credits / month (~2–3 weeks light use)", + "plan.free.bullet2": "Full product: Studio, Patrol, Outbox, images", + "plan.free.bullet3": "Feel the value, then upgrade to Starter", + "plan.free.bullet4": "When platform is busy, add your own key", + "plan.free.right1": "Full access to accounts, Studio, Patrol, Outbox, jobs, and inspiration.", + "plan.free.right2": "Enough credits for real copy, search, and a few images—not a hollow demo.", + "plan.free.right3": "After the cap, upgrade to Starter—or set BYOK so you don't use platform credits.", + "plan.free.quota1": "{n} credits allocated each month.", + "plan.free.note1": "Free requires no payment; paid subscribers manage cancellation in the billing portal.", + "plan.free.note2": "After cancellation, the plan changes on the date shown in the billing portal.", + + "plan.starter.headline": "Small teams posting and patrolling daily", + "plan.starter.bullet1": "{n} credits / month (about 5× Free)", + "plan.starter.bullet2": "Steady posting, replies, and patrol", + "plan.starter.bullet3": "Fits a 1–3 person cadence · main paid tier", + "plan.starter.bullet4": "Takes effect right after payment", + "plan.starter.right1": "After payment this account becomes Starter; the month uses the new quota.", + "plan.starter.right2": "Credits support regular posts, reply drafts, and scheduled patrol.", + "plan.starter.right3": "Same features as Free; you buy more headroom. Best starting paid plan.", + "plan.starter.quota1": "{n} credits / month · {price}.", + "plan.starter.note1": "Plan changes only after successful payment.", + "plan.starter.note2": "Resets on calendar month; unused credits do not roll over.", + + "plan.pro.headline": "Multi-account, heavy AI and research", + "plan.pro.bullet1": "{n} credits / month (about 3× Starter)", + "plan.pro.bullet2": "High-volume copy, research, and images · heavy ceiling", + "plan.pro.bullet3": "Built for agencies and multi-brand work", + "plan.pro.bullet4": "Takes effect right after payment", + "plan.pro.right1": "After payment this account becomes Pro; the month uses Pro quota.", + "plan.pro.right2": "Fits multi-account replies and deep research with less risk of running out mid-month.", + "plan.pro.right3": "Same features; you buy capacity. Beyond this, use BYOK—still works when platform is limited.", + "plan.pro.quota1": "{n} credits / month · {price}.", + "plan.pro.note1": "Failed payment does not change your plan.", + "plan.pro.note2": "After payment you can find receipts in billing history.", + + "plan.quota2": "Per-feature credits: copy {copy} (~{copyCalls} uses), research {research} (~{researchCalls}), search {search} (~{searchCalls}), image {image} (~{imageCalls} images).", + "plan.softCapsLine": "Credits: copy {copy} · research {research} · search {search} · image {image}", + "plan.approxCallsLine": "About {copyCalls} copy · {researchCalls} research · {searchCalls} search · {imageCalls} images", + + "checkout.title": "Confirm plan", + "checkout.pickFirst": "Please choose a plan first.", + "checkout.viewPlans": "View plans", + "checkout.fail": "Could not complete", + "checkout.confirmFree": "Confirm switch to Free", + "checkout.payAndAction": "{action} and pay {price}", + "checkout.subscribe": "Subscription", + "checkout.perMonth": "/mo", + "checkout.monthlyCredits": "{n} credits per month", + "checkout.youGet": "What you get", + "checkout.quota": "Quota", + "checkout.notes": "Notes", + "checkout.amountDue": "Amount due", + "checkout.billedMonthly": "{name} · billed monthly", + "checkout.already": "Already on this plan", + "checkout.processing": "Processing…", + "checkout.currentPlan": "Current plan", + "checkout.pickOther": "Choose another plan", + "checkout.cancel": "Cancel", + "checkout.invalidUrl": "The billing service returned an unsafe URL. No redirect was made.", + "checkout.redirecting": "Taking you to Stripe's secure checkout…", + "checkout.redirectingPortal": "Taking you to the Stripe subscription portal…", + "checkout.redirectFailed": "Stripe could not be opened. Check your browser or network settings and try again.", + "checkout.networkError": "Could not connect to the billing service. Check your network and try again.", + "checkout.unavailable": "Billing is not enabled or is temporarily unavailable. Please try again later.", + "checkout.sessionExpired": "Your session has expired. Sign in again and retry.", + "checkout.portalUnavailable": "There is no Stripe subscription to manage yet. Choose a paid plan first.", + "checkout.verifying": "Confirming payment and plan activation…", + "checkout.pollFail": "Could not check the payment status. Please retry.", + "checkout.missingId": "The checkout ID is missing, so the payment result cannot be verified.", + "checkout.terminalFail": "Checkout did not complete ({status}). Choose a plan and try again.", + "checkout.timeout": "Payment may still be processing, but the plan was not activated within 30 seconds. Retry the status check; do not pay again.", + "checkout.retry": "Retry status check", + "checkout.canceledTitle": "Checkout canceled", + "checkout.canceledBody": "Your plan was not changed and no payment action was performed.", + "checkout.manageInstead": "Manage this change in the billing portal to avoid a duplicate subscription.", + + "usage.widget.titleUsed": "{name} · used {used}/{cap} credits", + "usage.widget.titleUnlimited": "{name} · unlimited", + "usage.widget.ariaUsed": "Used {used} of {cap} credits", + "usage.widget.dialog": "Plan and usage", + "usage.widget.currentPlan": "Current plan", + "usage.widget.unlimited": "Unlimited", + "usage.widget.perMonth": "/mo", + "usage.widget.monthUsage": "This month", + "usage.widget.remaining": "{n} credits left", + "usage.widget.leftShort": "{n} left", + "usage.widget.usedOfCap": "{used}/{cap}", + "usage.widget.overShort": "Over", + "usage.widget.upgradeShort": "Upgrade", + "usage.widget.upgrade": "Upgrade", + "usage.widget.includes": "This plan includes", + "usage.widget.nudge": "You're running low this month. Upgrade for more credits right away.", + "usage.widget.changePlan": "Change plan", + "usage.widget.usageDetail": "Usage details", + + "usage.meter.ai_copy": "AI copy", + "usage.meter.ai_research": "AI research", + "usage.meter.web_search": "Search", + "usage.meter.ai_image": "AI image", + "usage.meter.barAria": "{label} {credits}/{cap} credits ({count} runs)", + "usage.ledger.costAria": "Used {n} credits", + "usage.event.keyMode.platform": "Platform credits", + "usage.event.keyMode.byok": "Your own key", + "usage.event.cost.credits": "−{n}", + "usage.event.cost.byok": "BYOK", + "usage.event.cost.byokAria": "Own key — no platform credits charged", + "usage.event.label.unknown": "Usage", + "usage.event.label.genericAi": "AI call", + "usage.event.label.personaAnalyzeText": "Persona analyze · text", + "usage.event.label.personaAnalyzeAccount": "Persona analyze · account", + "usage.event.label.composeMimic": "Mimic post", + "usage.event.label.composeViral": "Viral analysis", + "usage.event.label.personaPreview": "Persona preview", + "usage.event.label.ownPostReply": "Own post · reply draft", + "usage.event.label.mentionReply": "Mention · reply draft", + "usage.event.label.inspireChat": "Inspire chat", + "usage.event.label.researchSearch": "Research search", + "usage.event.label.generateImage": "Generate image", + "usage.event.label.search": "Web search", + "usage.event.label.aiComplete": "AI complete", + "usage.event.source.personaAnalyzeText": "Persona analyze · text", + "usage.event.source.personaAnalyzeAccount": "Persona analyze · account", + "usage.event.source.composeMimic": "Mimic post", + "usage.event.source.composeViral": "Viral analysis", + "usage.event.source.personaPreview": "Persona preview", + "usage.event.source.ownPostAnalyze": "Own post · structure analysis", + "usage.event.source.ownPostReply": "Own post · reply draft", + "usage.event.source.mentionReply": "Mention · reply draft", + "usage.event.source.inspireChat": "Inspire chat", + "usage.event.source.researchSearch": "Research search", + "usage.event.source.generateImage": "Generate image", + "usage.event.source.proxySearch": "Web search", + "usage.event.source.proxyAi": "AI complete", + + "usage.chart.period": "Period", + "usage.chart.allocated": "Allocated", + "usage.chart.consumed": "Used", + "usage.chart.pctTitle": "Usage as share of allocation", + "usage.chart.aria": "Allocation and usage", + "usage.chart.colAria": "{label}: allocated {purchased}, used {consumed}", + + "settings.provider": "Provider", + "settings.model": "Model", + "settings.aiUnifiedHint": "Copy, research, and expand all share one provider and model.", + "settings.fetchModels": "Fetch models", + "settings.fetchingModels": "Loading…", + "settings.apiKey": "API key", + "settings.configured": "Configured", + "settings.notConfigured": "Not set", + "settings.platformKeyOk": "Platform key available (optional override)", + "settings.modelsHint": "Models", + "settings.modelsCached": "Models from cache (~5 min)", + "settings.modelsLoaded": "Loaded models for {provider}", + "settings.aiSaved": "AI settings saved", + "settings.searchSaved": "Search settings saved", + "settings.clearAiKey": "Clear personal AI key", + "settings.aiKeyCleared": "Personal AI key cleared", + "settings.clearExaKey": "Clear Exa key", + "settings.exaKeyCleared": "Exa key cleared", + "settings.searchProvider": "Search provider", + "settings.expand": "Expand strategy", + "settings.exaKey": "Exa API key", + "settings.devMode": "Test patrol (local session)", + "settings.devModeHint": "When on, test patrol can use a synced Chrome sign-in. Live publish/replies still use the official API.", + "settings.ext.title": "Chrome extension", + "settings.ext.desc": "Install this extension (v1.2.0+) to sync a Threads sign-in from Chrome for test patrol.", + "settings.ext.step1": "Download and unzip to get the haixun-threads-sync folder", + "settings.ext.step2": "Open chrome://extensions and enable Developer mode", + "settings.ext.step3": "Load unpacked (or click Reload if already installed)", + "settings.ext.step4": "Set this site’s URL in extension options (must match address bar), then refresh Lapras", + "settings.ext.download": "Download extension (ZIP)", + "settings.ext.sessionTitle": "Chrome session (test patrol)", + "settings.ext.sessionHint": "Sync sign-in from a logged-in Threads tab for test patrol. Live publish still uses the official API.", + "settings.ext.pageOrigin": "This tab: {origin}", + "settings.ext.detected": "Extension detected", + "settings.ext.notReady": "Extension not detected", + "settings.ext.synced": "Session synced", + "settings.ext.notSynced": "Session not synced", + "settings.ext.syncBtn": "Sync session from Chrome", + "settings.ext.recheck": "Recheck", + "settings.ext.syncOk": "Chrome session synced for test patrol", + "settings.ext.syncFail": "Chrome session sync failed", + "settings.ext.needLogin": "Please sign in to Lapras before syncing.", + "settings.ext.notDetected": "Chrome extension not found on {origin}. Use v1.2.1+: reload in chrome://extensions → set Options Server URL to the same origin and Save → F5 this tab.", + "settings.ext.reloadHint": "After install/update, reload the extension and press F5 on this page.", + "settings.ext.detectSteps": "Installed but not detected? ① chrome://extensions → enable and Reload (v1.2.1+) ② Extension Options → set Server URL to {origin} and Save (Allow) ③ Hard-refresh this tab (F5). Do not mix localhost and 127.0.0.1.", + + "forgot.fail": "Request failed", + "forgot.mockHint": "In production this goes to email; here is a direct link:", + "forgot.checkInbox": "Please check your inbox (and spam).", + + "reset.mismatch": "Passwords do not match", + "reset.fail": "Reset failed", + "reset.cardTitle": "Reset password", + "reset.redirecting": "Redirecting to sign in…", + "reset.loginNow": "Sign in now", + "reset.passwordPh": "Password must be at least 12 characters with upper, lower, digit, and symbol", + "reset.forgotLink": "Request a new reset", + + "verify.sendFail": "Could not send", + "verify.fail": "Verification failed", + "verify.success": "Email verified. You can use Lapras now.", + "verify.codePh": "6-digit code", + "verify.currentAccount": "Account: {email}", + + "login.brandTitle": "巡樓 · Lapras", + + "home.navLabel": "Public navigation", + "public.localeLabel": "Language", + "home.heroTitle": "Find clients on Threads. Follow through to a win.", + "home.heroLead": "Daily demand lists, replies, and follow-ups on one desk.", + "home.outcomesTitle": "What you get", + "home.outcome.find.title": "People looking for you", + "home.outcome.find.body": "Keyword watches refresh daily; run a manual patrol when you need it.", + "home.outcome.reply.title": "Replies that fit", + "home.outcome.reply.body": "Drafts follow your services and tone; forbidden words stay out.", + "home.outcome.close.title": "Follow-ups that close", + "home.outcome.close.body": "One contact per person, with stages and reminders.", + "home.productTitle": "What you’ll use", + "home.productLead": "Demand, patrol, replies, CRM, and send — one desk.", + "home.preview.radar.title": "Today’s demand", + "home.preview.radar.caption": "Keyword watches refresh a daily list of needs", + "home.preview.scout.title": "Patrol", + "home.preview.scout.caption": "Run a manual sweep when you want hits now", + "home.preview.studio.title": "Replies & Studio", + "home.preview.studio.caption": "Drafts match your services and tone", + "home.preview.crm.title": "Pipeline", + "home.preview.crm.caption": "One contact card per person — stages and follow-ups", + "home.preview.outbox.title": "Outbox", + "home.preview.outbox.caption": "Scheduled, ready, and sent in one queue", + "home.preview.mock.radar.badge": "Today · high match", + "home.preview.mock.radar.meta": "3 new needs", + "home.preview.mock.radar.row1": "Taipei · moving quote", + "home.preview.mock.radar.row2": "Looking for home cleaning", + "home.preview.mock.radar.row3": "Freelance design · budget ready", + "home.preview.mock.scout.badge": "Keyword scan", + "home.preview.mock.scout.hit": "Anyone recommend an accountant?", + "home.preview.mock.scout.snippet": "Need someone for small-company tax…", + "home.preview.mock.scout.hit2": "Has anyone tried that SaaS?", + "home.preview.mock.studio.tab1": "Inspire", + "home.preview.mock.studio.tab2": "Reply", + "home.preview.mock.studio.tab3": "Schedule", + "home.preview.mock.studio.draft1": "Hi — saw you’re looking for movers.", + "home.preview.mock.studio.draft2": "We cover metro Taipei; happy to estimate first.", + "home.preview.mock.studio.draft3": "(From your service profile & forbidden words)", + "home.preview.mock.crm.col1": "New", + "home.preview.mock.crm.col2": "Talking", + "home.preview.mock.crm.col3": "Won", + "home.preview.mock.crm.foot": "Due follow-ups & reminders", + "home.preview.mock.outbox.r1": "Reply · tonight 20:00", + "home.preview.mock.outbox.r2": "Post · needs review", + "home.preview.mock.outbox.r3": "Sent · syncing metrics", + "home.pricingTitle": "Plans", + "home.pricingLead": "Same features; monthly credits differ.", + "home.pricingHoverHint": "Hover the credit line to see what each plan covers.", + "home.ctaLogin": "Sign in", + "home.privacyLink": "Privacy", + "home.termsLink": "Terms", + "home.dataDeletionLink": "Data deletion", + "legal.footerNav": "Legal links", + + "privacy.navLabel": "Privacy policy navigation", + "privacy.title": "Privacy Policy", + "privacy.updated": "Last updated: 2026-07-31", + "privacy.intro": + "This Privacy Policy explains how Lapras (巡樓, “the Service”, “we”) collects, uses, stores, shares, and deletes your personal data and Meta/Threads platform data. It covers the Lapras web console, public tools, and processing related to Threads accounts you connect via OAuth. Using the Service means you acknowledge this policy.", + "privacy.section.overview.title": "1. Controller and scope", + "privacy.section.overview.body": + "The Lapras operator is the controller of personal data described here.\nThis policy covers: member accounts, workspace content, usage and payment identifiers, and platform data we obtain from Meta/Threads APIs after you authorize us.\nIf you join someone else’s workspace, that owner may have extra rules; they do not replace this notice for platform- and member-level data.\nThis is our own policy, not Meta’s, Threads’, or Instagram’s. See Meta’s Privacy Center for how Meta processes data in its products.", + "privacy.section.collect.title": "2. What information we collect", + "privacy.section.collect.body": + "(A) Data you provide\n• Account: email, display name, password hash (never plaintext passwords), role, email verification status.\n• Content you enter: personas, brands, patrol intents/keywords, drafts, reply copy, Outbox items, workspace settings, and uploaded media if any.\n• Support/privacy requests: emails and request details you send us.\n\n(B) Data collected automatically\n• Login sessions, IP, basic browser/device metadata (security, debugging, abuse prevention).\n• UI preferences (language, theme).\n• Feature usage and job logs (quotas, diagnostics).\n\n(C) Meta/Threads platform data (only after you connect)\n• Threads user identifiers (e.g. user id, username) and profile summary within granted scopes.\n• Posts, replies, media metadata, and API responses needed to read/send/insights within granted permissions.\n• Access tokens (and related refresh credentials) used only to call Threads APIs on your behalf; stored separately from member JWTs.\n\n(D) Billing (if enabled)\n• Plan tier, quota usage, payment transaction identifiers (we do not store full card numbers).\n\nWe do not require unrelated government ID for ordinary use.", + "privacy.section.use.title": "3. How we use data and why", + "privacy.section.use.body": + "We process data to:\n• Provide accounts, workspaces, permissions, and sign-in security.\n• Run core features: patrol/outreach, create and AI assist, Outbox schedule/send, account and post sync, insights and metering.\n• Call Meta/Threads APIs with your tokens (read authorized content, publish content you confirm, etc., subject to granted scopes).\n• Send verification codes, password resets, and service notices.\n• Prevent abuse, keep the Service reliable, debug and improve (de-identified where practical).\n• Comply with law or valid legal requests.\n\nWe do not sell Meta/Threads platform data to data brokers, and we do not use that data to build independent marketing profiles unrelated to providing the Service.", + "privacy.section.threads.title": "4. Threads / Meta platform data", + "privacy.section.threads.body": + "Connection: you authorize Lapras via Meta OAuth for Threads. We request and use only permissions needed for product features (e.g. basic profile, content publish, and other scopes shown in the authorization UI).\n\nUse limits: Platform Data from Meta is used only to provide, maintain, and improve Lapras features you use (connect accounts, sync posts, schedule/send replies or posts, show status, debug). We do not sell Platform Data or use it for advertising targeting unrelated to your authorization.\n\nStorage: Threads credentials are kept separate from member sign-in credentials; access is permission-controlled by workspace/account.\n\nYou may at any time:\n• Disconnect Threads inside Lapras; and/or\n• Revoke the app in Meta/Threads/Instagram settings.\nAfter disconnect we stop new API access and handle deletion/anonymization under Section 8.\n\nMeta’s own processing is governed by Meta’s policies; we do not control Meta’s systems.", + "privacy.section.share.title": "5. Sharing and processors", + "privacy.section.share.body": + "We may share data with or have it processed by:\n• Infrastructure: hosting, databases, object storage, email delivery, monitoring/logs (to run the Service).\n• AI/search providers: when you use inspire, analysis, or suggestion features (see Section 6).\n• Payment processors: when billing is enabled.\n• Legal: when required by law or valid authority.\n• Business transfers: merger/acquisition under applicable law and notice.\n\nProcessors may only process under our instructions with reasonable security. We do not sell personal data.", + "privacy.section.ai.title": "6. AI and automated processing", + "privacy.section.ai.body": + "Some features send prompts, post samples, structured notes, or summaries of authorized public content to AI/search providers for suggestions, style analysis, or retrieval.\nYou may use platform defaults or BYOK keys; with BYOK, requests go to your provider—read their terms too.\nWe do not use your content as public marketing or advertised training material without separate consent.", + "privacy.section.retention.title": "7. Retention", + "privacy.section.retention.body": + "• Account and workspace data: while the account is active.\n• Threads tokens: while connected; invalidated/removed after disconnect or deletion requests.\n• Job logs, usage, security records: for a reasonable period for operations, disputes, and law, then delete or anonymize.\n• Backups: may retain residual copies until rotation completes.\nSee Section 8 and the Data deletion page for how to request deletion.", + "privacy.section.deletion.title": "8. How to request deletion of your data", + "privacy.section.deletion.body": + "You can request deletion of personal data and platform data we hold about you as follows:\n\nOption 1 (recommended): Sign in to Lapras → disconnect Threads in account/settings, then use any in-product account/data deletion flow if available.\n\nOption 2: Email the Service operator from your registered email address. Subject: “Data Deletion Request”. Include:\n• Registered email\n• Threads username or in-app account id if known\n• Scope (entire account / Threads connection data only / specific workspace)\n\nOption 3: If you remove our app in Meta and request deletion, follow Meta’s flow. We also process platform deletion callbacks when provided; otherwise still use Option 2.\n\nFull steps are on the Data deletion page (/data-deletion).\nAfter identity verification we delete or anonymize within a reasonable period (typically 30 days, or sooner/later if law requires), except records we must retain by law.", + "privacy.section.rights.title": "9. Your rights", + "privacy.section.rights.body": + "Where applicable law allows, you may request access, correction, portability, deletion, restriction, or objection to certain processing. You may sign out, edit profile data, disconnect Threads, or stop using the Service. Contact us per Section 12; we may need to verify identity.", + "privacy.section.security.title": "10. Security", + "privacy.section.security.body": + "We use reasonable technical and organizational measures, including HTTPS, password hashing, permission isolation, and separating member credentials from Threads tokens. No system is perfectly secure; we notify of incidents as required by law or policy.", + "privacy.section.children.title": "11. Children", + "privacy.section.children.body": + "The Service is intended for creators and business operators, not children. We do not knowingly collect personal data from children under the applicable age (e.g. 13, or a higher age where required). Contact us under Section 12 if you believe we collected a child’s data; we will delete it promptly.", + "privacy.section.contact.title": "12. Contact us", + "privacy.section.contact.body": + "For privacy questions or access/correction/deletion requests, contact the Service operator using your registered email. Subject: “Privacy / Data Request”. Include your account email and details so we can verify identity.\nYou may also use in-product profile/settings after sign-in.", + "privacy.section.updates.title": "13. Updates", + "privacy.section.updates.body": + "We may update this policy for features, Meta platform rules, or law. We change the “Last updated” date; for material changes we may also notify in-product or by email. Continued use after an update means you acknowledge the revised policy to the extent permitted by law.", + "privacy.backHome": "Back to intro", + + "terms.navLabel": "Terms of service navigation", + "terms.title": "Terms of Service", + "terms.updated": "Last updated: 2026-07-31", + "terms.intro": + "Welcome to Lapras (巡樓). These Terms of Service govern your use of the web console, public tools, and related features. By using the Service you agree to these Terms. If you do not agree, do not use the Service.", + "terms.section.acceptance.title": "1. Acceptance", + "terms.section.acceptance.body": + "You must have legal capacity to contract and comply with applicable law. If you use the Service for an organization, you represent you have authority to bind that organization.", + "terms.section.service.title": "2. The Service", + "terms.section.service.body": + "Lapras provides Threads operations tooling, including account linking, patrol/outreach, create and send, personas/brands, usage and plans. Features may change. We may suspend or modify features for maintenance, security, or legal reasons.", + "terms.section.accounts.title": "3. Accounts and security", + "terms.section.accounts.body": + "Provide accurate information, protect credentials, and you are responsible for activity under your account. Notify us of unauthorized use. We may limit or suspend accounts for security, abuse, or violations.", + "terms.section.threads.title": "4. Threads / Meta connection", + "terms.section.threads.body": + "Some features require Meta OAuth to connect Threads. You must have the right to connect that account and must comply with Meta, Threads, and Instagram terms and community standards.\nYou are responsible for lawful, non-infringing content and for following platform rules. Platform enforcement, API denials, or third-party claims arising from your content or account use are your responsibility. Lapras is a tool and does not guarantee platform approval or performance outcomes.\nYou may disconnect or revoke Meta authorization at any time; some features will stop working after disconnect.", + "terms.section.content.title": "5. Your content", + "terms.section.content.body": + "You retain rights in content you upload or create. You grant us a non-exclusive, worldwide, royalty-free license to store, process, transmit, and display content as needed to provide the Service to you and authorized workspace members, including calling necessary third-party APIs. You warrant content is lawful and non-infringing.", + "terms.section.acceptable.title": "6. Acceptable use", + "terms.section.acceptable.body": + "You may not: abuse APIs or automate harm to others; spam, fraud, hate, or illegal content; bypass quotas or security; reverse engineer or access systems without authorization; use the Service in ways that violate Meta platform policies; or infringe others’ privacy or IP.", + "terms.section.billing.title": "7. Plans and usage", + "terms.section.billing.body": + "Paid plans, credits, and limits are as shown in product. Non-payment, overuse, or abuse may reduce or suspend features. Refunds follow the purchase terms and applicable law.", + "terms.section.disclaimer.title": "8. Disclaimers and liability", + "terms.section.disclaimer.body": + "The Service is provided “as is”. To the fullest extent permitted by law, we do not warrant uninterrupted or error-free operation, or continued availability of third platforms (including Threads/Meta/AI providers). We are not liable for indirect, incidental, or lost-profit damages except where liability cannot be limited. Our aggregate liability is capped at fees you paid for the Service in the twelve months before the claim (or zero if free), except where law forbids the cap.", + "terms.section.termination.title": "9. Termination", + "terms.section.termination.body": + "You may stop using the Service and request deletion under the Privacy Policy. We may suspend or terminate for breach, abuse, legal requirements, or if we discontinue the Service. Data handling after termination follows the Privacy Policy.", + "terms.section.contact.title": "10. Contact and governing law", + "terms.section.contact.body": + "Contact the operator via your registered email for terms questions. Governing law and venue are those of our primary place of operation unless mandatory law says otherwise. Privacy: /privacy. Data deletion: /data-deletion.", + + "deletion.navLabel": "Data deletion navigation", + "deletion.title": "User Data Deletion Instructions", + "deletion.updated": "Last updated: 2026-07-31", + "deletion.intro": + "This page explains how to ask Lapras (巡樓) to delete personal data and Threads platform data associated with you.", + "deletion.section.summary.title": "1. Summary", + "deletion.section.summary.body": + "If you stop using the Service, or remove our app in Meta and want us to delete data we hold, follow the steps below. After identity verification we delete or anonymize deletable data, except where law or legitimate retention requires keeping records.", + "deletion.section.steps.title": "2. How to request deletion", + "deletion.section.steps.body": + "Step 1: If you can still sign in, disconnect all Threads accounts in Lapras (settings/accounts).\nStep 2: Email the Service operator from your registered email address.\nStep 3: Subject line: “Data Deletion Request”.\nStep 4: Include:\n• Registered email (required)\n• Display name or member id if known\n• Threads username or in-app account id if you connected Threads\n• Scope: entire account / Threads connection + synced data only / a specific workspace\nStep 5: We confirm receipt, process the request, and email completion when possible.", + "deletion.section.threads.title": "3. Threads / Meta authorization", + "deletion.section.threads.body": + "Deleting Lapras data does not delete your posts or account on Threads/Instagram/Meta.\nAlso revoke our app in Meta/Threads settings to clear authorization on Meta’s side.\nIf the platform sends us a deletion callback, we process matching data; otherwise use the email process on this page.", + "deletion.section.scope.title": "4. What we delete", + "deletion.section.scope.body": + "Where reasonably possible we delete or anonymize:\n• Member personal data and credentials\n• Workspace content you created (personas, drafts, patrol, Outbox, etc., per request scope)\n• Threads connection identifiers, tokens, and sync caches\n• Non-essential job and analytics records\n\nWe may retain for a limited time or by law:\n• Transaction/billing records required by law\n• Security and anti-abuse logs (limited period)\n• Backup copies until rotation completes", + "deletion.section.timeline.title": "5. Timeline", + "deletion.section.timeline.body": + "We typically complete deletion or anonymization within 30 days after a verifiable request. If more time is needed (complex workspaces or legal review), we will communicate an estimate. Backup purge may finish afterward.", + "deletion.section.contact.title": "6. Contact", + "deletion.section.contact.body": + "Email the Service operator from your registered address with subject “Data Deletion Request”. Full privacy policy: /privacy. Terms: /terms.", + + "common.listSep": ", ", + "common.dash": "—", + + "scout.title": "Topic ideas", + "scout.topic.intro": "Enter a title, person, event, or work direction. Scout infers whether you want trending discussion, recommendations, or public work signals. Use Demand to manage customer opportunities.", + "scout.topic.termHint": "Up to 3 intent-aware queries are preselected. You can uncheck, edit, or add your own wording.", + "scout.topic.termsNeedShort": "{n} term(s) break Threads short-query rules — shorten them before searching.", + "scout.topic.noTerms": "No usable terms — try a clearer topic (e.g. Taipei market, nanny recs).", + "scout.topic.termsReadyPrimary": "Intent understood. Up to 3 high-confidence queries are selected, with {n} more available to adjust.", + "scout.topic.workshopHintSelect": "Queries run separately and are deduplicated. Up to 3 high-confidence queries are preselected for review.", + "scout.topic.primaryTerm": "Primary (recommended)", + "scout.topic.variantTerm": "Variant {n}", + "scout.topic.useTerm": "Include this term in search", + "scout.today": "Today's sortie", + "scout.purposeValue": "Pain-point replies", + "scout.purposeDemand": "Find demand pains", + "scout.purposeProvider": "Find solution providers", + "scout.purposeActivity": "Activity short replies", + "scout.goal": "Daily goal (posts)", + "scout.progress": "Progress {done}/{goal}", + "scout.intent": "What to find / reply to", + "scout.keyword": "Keywords", + "scout.intentPh": "e.g. seasonal scalp itch, truly fragrance-free, outlets on weekends", + "scout.keywordPh": "e.g. backend freelance engineer, Demon Slayer", + "scout.productOptional": "Product (optional)", + "scout.productRequired": "Product to solve (required)", + "scout.selectProduct": "Select a product", + "scout.noProduct": "No product", + "scout.brandFallback": "Brand", + "scout.placement": "Placement: {label}", + "scout.providerProduct": "Product: {label}", + "scout.painPart": " · pain “{pain}”", + "scout.noProductsBefore": "No products yet. Add some under", + "scout.noProductsAfter": ".", + "scout.start": "Start", + "scout.startMore": "Fetch more", + "scout.fetching": "Fetching…", + "scout.planKeywords": "Generate keywords", + "scout.planning": "Planning keywords…", + "scout.workshop": "Search keywords (editable)", + "scout.workshopHint": "Search runs only after you confirm. Each line is its own query — remove weak ones and add phrases that work.", + "scout.workshopEmpty": "Keep at least one keyword to search", + "scout.addTerm": "Add", + "scout.addTermPh": "Add another search phrase", + "scout.removeTerm": "Remove", + "scout.confirmScan": "Search with these", + "scout.startImmediate": "Start patrol now", + "scout.immediateHint": "Start now first prepares suggested phrases, then searches with up to 3 focused queries.", + "scout.openDailySchedule": "View daily schedule", + "scout.scheduleHint": "Daily automatic patrols are configured under Demand watches.", + "scout.replan": "Regenerate", + "scout.clearWorkshop": "Cancel", + "scout.termsReady": "{n} keywords ready — review before searching.", + "scout.runs": "Patrol batches", + "scout.runCount": "Batch ({n})", + "scout.runCreatedAt": "Created {time}", + "scout.runSelectAria": "Switch patrol batch", + "scout.runPending": "{n} pending · ", + "scout.runDone": "Cleared · ", + "scout.runTotal": " ({n} posts)", + "scout.runStatus.queued": "Queued", + "scout.runStatus.running": "Scanning", + "scout.runStatus.succeeded": "Complete", + "scout.runStatus.failed": "Failed", + "scout.runStatus.cancelled": "Cancelled", + "scout.shortfall": "{n} posts short", + "scout.shortfallReason.source_exhausted": "Sources exhausted", + "scout.shortfallReason.duplicate_exhausted": "Too many duplicates", + "scout.shortfallReason.relevance_exhausted": "Not enough relevance", + "scout.shortfallReason.source_unavailable": "Source unavailable", + "scout.shortfallReason.limit_reached": "Limit reached", + "scout.shortfallReason.unknown": "Insufficient source results", + "scout.refreshRuns": "Refresh batches", + "scout.refreshingRuns": "Refreshing…", + "scout.runsRefreshed": "Batches refreshed", + "scout.newRunReady": "A new patrol batch is ready. Your current reading position is unchanged; refresh batches to view it.", + "scout.deleteRun": "Delete this batch", + "scout.deleting": "Deleting…", + "scout.now": "Now this post", + "scout.emptyBatch": "Nothing pending in this batch. Want a daily auto demand list? Open Demand and add keyword watches.", + "scout.draft": "Reply draft", + "scout.draftPhActivity": "Short reply…", + "scout.draftPhValue": "Empathize → suggest…", + "scout.sendAccount": "Send from account", + "scout.noAccount": "No usable accounts", + "scout.personaForRegen": "Persona (for regen)", + "scout.notReady": " (not ready)", + "scout.skip": "Skip", + "scout.regen": "Regen", + "scout.send": "Send", + "scout.openThreadsReply": "Open Threads to reply", + "scout.markManualDone": "Replied, mark complete", + "scout.manualReplyHint": "Review the draft, open the original Threads post, and reply there. The draft will be copied when possible. Return here to mark it complete.", + "scout.openedAndCopied": "Threads opened and the draft was copied. Paste the reply, then return to mark it complete.", + "scout.openedManual": "Threads opened. Return here after replying to mark it complete.", + "scout.noPermalink": "This result has no Threads permalink to open.", + "scout.manualDone": "Manual reply marked complete · today {done}/{goal}", + "scout.manualDoneFail": "Failed to mark the manual reply complete", + "scout.resend": "Resend", + "scout.sending": "Sending…", + "scout.needAccountBefore": "Connect a usable account under", + "scout.needAccountAfter": "first.", + "scout.loadingKnowledge": "Preparing related knowledge…", + "scout.product": "Product", + "scout.queue": "Matches · {n}", + "scout.valueQueue": "Value replies · {n}", + "scout.providerQueue": "Solution providers · {n}", + "scout.demandQueue": "Demand pains · {n}", + "scout.activityQueue": "Activity short replies · {n}", + "scout.noMatchesInQueue": "No matches in this queue", + "scout.collapseQueue": "Collapse queue", + "scout.expandQueue": "Expand queue", + "scout.noOtherPending": "No other pending", + "scout.unnamedRun": "Unnamed batch", + "scout.thisRun": "this batch", + "scout.confirmDeleteRun": "Delete patrol batch “{label}”?\\nHits and related knowledge will be removed. This cannot be undone.", + "scout.deletedRun": "Deleted batch “{label}”", + "scout.deleteRunFail": "Failed to delete batch", + "scout.err.runBusy": "This batch is still scanning or already finished and cannot be deleted yet.", + "scout.err.notFound": "This batch or post no longer exists. Refresh the batches.", + "scout.err.sourceUnavailable": "The patrol source is unavailable right now. Try again later.", + "scout.needKeyword": "Enter keywords first", + "scout.needIntent": "Write what you're looking for", + "scout.productMissing": "Selected product is not in the list. Please reselect.", + "scout.providerSetupRequired": "Solution matching needs pain points and at least one tag or provider capability term. Complete them on the Brands page first.", + "scout.defaultLabel": "Patrol", + "scout.newRunActivity": "New batch “{label}” · {n} pending", + "scout.newRunValue": "New batch “{label}” · {n} posts · handle “Now this post”", + "scout.knowledgeReady": "“{label}” knowledge ready · {n} notes", + "scout.patrolFail": "This patrol failed", + "scout.loadFail": "Scout data could not be loaded. Refresh and try again.", + "scout.scanQueued": "Patrol job “{label}” queued. You’ll be notified when a new batch is ready.", + "scout.workerWaiting": "The patrol job is still waiting for a worker. Check that apps/backend worker is running.", + "scout.scanReady": "Patrol complete. Found {n} pending replies.", + "scout.queued": "Queued in @{who}'s Outbox for delivery · today {done}/{goal}", + "scout.scanJob": "Patrol job", + "scout.scanInProgress": "Scanning", + "scout.crawlerSessionRequired": "Test patrol needs a valid Chrome session. Sync it from a signed-in Threads tab in Settings.", + "scout.openSettings": "Open Settings", + "scout.source": "Source: Threads Keyword Search", + "scout.resultKeyword": "Keyword: {tag}", + "scout.classification": "Class: {classification}", + "scout.postedAt": "Posted: {time}", + "scout.postedUnknown": "Post time unknown", + "scout.scannedAt": "Scanned {time}", + "scout.createdAt": "Created {time}", + "scout.openPermalink": "Open original on Threads", + "scout.draftFail": "Draft failed", + "scout.skipped": "Skipped", + "scout.status.new": "To handle", + "scout.status.drafted": "Drafted", + "scout.status.queued": "Queued", + "scout.status.published": "Sent", + "scout.status.skipped": "Skipped", + "scout.noDraft": "No draft to send", + "scout.accountFallback": "account", + "scout.sent": "Sent (@{who}) · today {done}/{goal}", + "scout.sendFail": "Send failed", + "scout.confirmDeletePost": "Delete this hit?", + "scout.deletedPost": "Deleted this hit", + "scout.stanceActivity": "Short reply · activity", + "scout.stanceDemand": "Demand pain · replyable", + "scout.stanceProvider": "Solution matching · no product promotion", + "scout.demandHint": "These posts show people seeking help or comparing solutions. Read the original post before responding helpfully.", + "scout.providerHint": "These are solution-provider candidates. Review the original post and proof of capability before contacting them.", + "scout.stanceProduct": "Empathy · soft product", + "scout.stanceRelation": "Engage · build rapport", + + "brands.title": "Brands", + "brands.railAria": "Brand list", + "brands.railLabel": "Your brands", + "brands.add": "Add", + "brands.brandName": "Brand name", + "brands.brandNamePh": "e.g. your brand", + "brands.creating": "Creating…", + "brands.createBrand": "Create brand", + "brands.searchAria": "Search brands", + "brands.searchPh": "Search brands…", + "brands.empty": "No brands yet", + "brands.noMatch": "No matches", + "brands.selectAria": "Select brand", + "brands.pickOne": "Pick a brand", + "brands.inUseHint": "Active · used for patrol and studio", + "brands.inUse": "Active", + "brands.tabBrands": "Brands", + "brands.tabInfo": "Brand info", + "brands.tabProducts": "Products", + "brands.tabProductsN": "Products ({n})", + "brands.displayName": "Name", + "brands.brief": "Summary", + "brands.briefPh": "One line about this brand", + "brands.audience": "Audience", + "brands.audiencePh": "Who cares and why", + "brands.goals": "Goals", + "brands.goalsPh": "What you want on Threads", + "brands.saving": "Saving…", + "brands.deleteBrand": "Delete brand", + "brands.searchProductAria": "Search products", + "brands.searchProductPh": "Search products…", + "brands.addProduct": "Add product", + "brands.noProducts": "No products yet", + "brands.hasLink": "Has link", + "brands.painLabel": "Pains ", + "brands.editProduct": "Edit product", + "brands.newProduct": "New product", + "brands.importFromUrl": "Import from product URL", + "brands.fetching": "Fetching…", + "brands.fetch": "Fetch", + "brands.pains": "Pain points", + "brands.painsPh": "One per line", + "brands.tags": "Tags", + "brands.tagsPh": "Comma-separated", + "brands.intro": "Description", + "brands.providerCapabilities": "Capabilities / services that solve the pain", + "brands.providerCapabilitiesPh": "e.g. dermatology, allergen testing, sensitive-skin consultation", + "brands.providerExcludes": "Same-category exclusions", + "brands.providerExcludesPh": "e.g. shampoo, hair-care products", + "brands.link": "Link", + "brands.update": "Update", + "brands.createItem": "Add", + "brands.needName": "Enter a name", + "brands.created": "Created “{name}”", + "brands.createFail": "Create failed", + "brands.saved": "Saved", + "brands.saveFail": "Save failed", + "brands.confirmDelete": "Delete “{name}”?", + "brands.deleted": "Deleted", + "brands.deleteFail": "Delete failed", + "brands.fetchFail": "Fetch failed", + "brands.needLabelContext": "Name and description are required", + "brands.productUpdated": "Updated", + "brands.productAdded": "Added", + "brands.confirmDeleteProduct": "Delete this product?", + + "insights.title": "Account performance", + "insights.account": "Account", + "insights.noAccount": "No accounts", + "insights.syncing": "Syncing…", + "insights.syncPosts": "Sync posts", + "insights.myPosts": "My posts", + "insights.pickAccount": "Select an account", + "insights.goAccounts": "Accounts", + "insights.kpiMonth": "This month", + "insights.monthViews": "Views this month", + "insights.monthLikes": "Likes this month", + "insights.monthReplies": "Replies this month", + "insights.engRate": "Engagement", + "insights.vsPrev": "vs last month", + "insights.avgNear": "Recent avg {rate}", + "insights.trendTitle": "Trends & analysis · @{user}", + "insights.metricViews": "Views", + "insights.metricLikes": "Likes", + "insights.metricReplies": "Replies", + "insights.metricPosts": "Posts", + "insights.metricPostsFull": "Posts", + "insights.chartMetrics": "Chart metric", + "insights.barsAria": "Recent months {metric}; click a bar for analysis", + "insights.barsLabel": "{metric} · last {n} months", + "insights.clickBar": " · click bar for analysis", + "insights.pickMonthAria": "Select month", + "insights.barTitle": "{label}: {value}{est} · click for analysis", + "insights.est": " (est.)", + "insights.monthSuffix": "{m}", + "insights.sparkAria": "Trend line; click a node to select month", + "insights.analysisOf": "{label} analysis", + "insights.producedAt": "Generated {time}", + "insights.hasEstimate": " · includes estimates", + "insights.viewsVsPrev": " · views vs prev {delta}", + "insights.statPosts": "Posts", + "insights.statViews": "Views", + "insights.statLikes": "Likes", + "insights.statReplies": "Replies", + "insights.conclusions": "Takeaways", + "insights.recommendations": "Recommendations", + "insights.highlights": "Month highlights", + "insights.findTopics": "Find topics", + "insights.goScout": "Go patrol", + "insights.selectMonth": "Select a month", + "insights.topPosts": "Top posts", + "insights.noPosts": "No posts yet", + "insights.postStats": "Views {views} · likes {likes} · replies {replies}", + "insights.openThreads": "Open Threads", + "insights.zeroPct": "0%", + "insights.panelHint": "Aggregates metrics from synced “My posts”", + "insights.lastSynced": "Last sync {time}", + "insights.neverSynced": "Not synced yet", + "insights.emptyTitle": "No post data yet", + "insights.emptyDesc": "Tap “Sync posts” to pull your Threads posts and insights, then review charts and analysis.", + "insights.syncDone": "Synced {n} posts · insights updated", + "insights.syncFail": "Sync failed", + "insights.loadFail": "Failed to load posts", + "insights.zeroViewsHint": "Posts found but views are mostly 0 — Insights scope may be missing, or stats not ready. Try sync again.", + "insights.postsInMonth": "{n} posts", + "insights.kpiForMonth": "{label} metrics", + "insights.topPostsOfMonth": "{label} · top posts", + "insights.noPostsInMonth": "No posts in {label}", + "insights.pastMonthEmptyHint": "No synced posts for this month (past months are fetched once). Sync manually or pick another month.", + "insights.pastBackfillDone": "Backfilled {n} posts (past months fetch once)", + "insights.autoRefreshDone": "Current month refreshed ({n} posts)", + "insights.noDataNoAnalysis": "No posts in {label} — no analysis.", + "insights.noAnalysisYet": "No analysis for {label} yet (need synced posts).", + "insights.thisMonth": "This month", + "insights.narrative.summary": "{when} summary: {posts} posts, {views} views, {likes} likes, {replies} replies (from synced posts).", + "insights.narrative.viewsDelta": "Views {delta} vs last month ({prev} → {curr}).", + "insights.narrative.viewsFlat": "Views roughly flat vs last month ({delta}).", + "insights.narrative.repliesUp": "Replies {delta}; conversation is heating up.", + "insights.narrative.repliesDelta": "Replies {delta}.", + "insights.narrative.engRate": "Engagement about {pct}% (likes+replies+reposts+quotes+shares / views).", + "insights.narrative.engLow": "Engagement is low: try ending with a specific question.", + "insights.narrative.engGood": "Engagement looks solid: reuse a high-performing structure for 1–2 more posts.", + "insights.narrative.zeroViews": "This month has likes/replies but 0 views (Insights may still be pending or missing permission).", + "insights.narrative.highlight": "One stronger post: {snippet}", + "insights.narrative.smallSample": "Few posts this month — treat month-over-month as a hint only.", + + "plays.tabOwn": "My posts", + "plays.tabLink": "Threads link", + "plays.noPosts": "No posts yet", + "plays.targetPost": "Target post", + "plays.likesSuffix": " ({n} likes)", + "plays.linkCard": "Paste Threads link", + "plays.postLink": "Post URL", + "plays.resolving": "Resolving…", + "plays.resolve": "Resolve link", + "plays.resolveHint": "After resolve, schedule your accounts to reply under that post.", + "plays.targetOwn": "Target post (yours)", + "plays.openThreads": "Open Threads", + "plays.external": "External post", + "plays.addScheme": "New scheme", + "plays.schemeCount": "{n} schemes for this target", + "plays.noSchemes": "No schemes yet", + "plays.replyCount": "{n} replies", + "plays.editTitle": "Edit: {title}", + "plays.schemeName": "Scheme name", + "plays.schemeNamePh": "e.g. Scheme A · soft engage", + "plays.speakersOwn": "Accounts (post owner always included)", + "plays.speakers": "Accounts", + "plays.postOwner": " (post owner)", + "plays.noAccounts": "No usable accounts. Connect Threads first.", + "plays.interval": "Interval (min)", + "plays.applyInterval": "Apply interval", + "plays.aiEmpty": "AI fill empty", + "plays.aiBusy": "Generating…", + "plays.aiFail": "AI generate failed", + "plays.aiStepDone": "Step filled — edit as needed", + "plays.aiNoneFilled": "No empty steps to fill (or persona not ready)", + "plays.needPersonaForStep": "Pick a ready persona on this step first", + "plays.aiEmptyResult": "AI returned empty — retry or pick a faster model", + "plays.saveBeforeAi": "Save the play first, then generate the full script (needs play id)", + "plays.scriptJobQueued": "Full-script job queued — you can leave; steps fill when done", + "plays.scriptJobDone": "Script generated — steps filled (edit as needed)", + "plays.scriptJobDoneReload": "Script done — reopen the play to see steps", + "plays.replies": "Replies ({n})", + "plays.stepN": "Reply {n}", + "plays.who": "Who", + "plays.personaOpt": "Persona (optional)", + "plays.brandOpt": "Brand (optional)", + "plays.reply": "Reply", + "plays.attach": "Images", + "plays.addOne": "Add one", + "plays.saving": "Saving…", + "plays.saveScheme": "Save scheme", + "plays.submitting": "Submitting…", + "plays.submitOutbox": "Submit to Outbox", + "plays.closeEdit": "Close editor", + "plays.noTarget": "No target post yet", + "plays.resolved": "Link resolved", + "plays.resolveFail": "Resolve failed", + "plays.filled": "Generated {n}", + "plays.needTarget": "Select a target post first", + "plays.saved": "Scheme saved", + "plays.saveFail": "Save failed", + "plays.submitted": "Sent to Outbox", + "plays.submitFail": "Submit failed", + "plays.confirmDelete": "Delete this scheme?", + "plays.accountFallback": "account", + + "inspire.loading": "Loading…", + "inspire.trendsAria": "Topic ideas", + "inspire.trendsLabel": "Topic ideas", + "inspire.trendsHint": "Web-sourced ideas · costs a search credit", + "inspire.trendsSeed": "sample", + "inspire.topicSeed": "I want a Threads post about “{topic}”. Help me with openings and angles.", + "inspire.trendsEmpty": "Tap “Find ideas” to search (uses credits)", + "inspire.refreshConfirm": "Finding ideas costs 1 search credit. Continue?", + "inspire.refreshOk": "Updated {n} topic ideas", + "inspire.refreshFail": "Could not find ideas (quota or search failed)", + "inspire.refresh": "Find ideas", + "inspire.clearChat": "New chat", + "inspire.clearedNewSession": "New chat started", + "inspire.sessionsAria": "Inspiration chats", + "inspire.session": "Chat", + "inspire.sessionNew": "New chat", + "inspire.newSession": "+ New", + "inspire.newSessionOk": "New chat started (previous kept in list)", + "inspire.deleteSession": "Delete current chat", + "inspire.deleteSessionShort": "Delete", + "inspire.confirmDeleteSession": "Delete this chat permanently?", + "inspire.deletedSession": "Deleted — switched to another chat", + "inspire.pinAsElement": "Apply as element", + "inspire.you": "You", + "inspire.ai": "AI", + "inspire.system": "System", + "inspire.useDraft": "Use this draft", + "inspire.openPlay": "Open play", + "inspire.thinking": "Thinking…", + "inspire.stop": "Stop generating", + "inspire.stopped": "Generation stopped", + "inspire.pinnedAria": "This-round references (for the AI)", + "inspire.pinned": "References", + "inspire.pinnedCount": "· {n}", + "inspire.pinsLocalShort": "this session", + "inspire.pinsSessionLocal": "References apply to this session only and will be included with your next message.", + "inspire.pickRight": "Pick on the right = pin for AI; click name to insert", + "inspire.unpinTitle": "Remove reference", + "inspire.insertPinTitle": "Insert into input", + "inspire.insertBrand": "Talk about “{name}”", + "inspire.insertedPin": "Inserted “{name}” into the input", + "inspire.flowStep1": "Prep", + "inspire.flowStep2": "Ideate", + "inspire.flowStep3": "Persona draft", + "inspire.emptyTitle": "Ideate first, then draft in persona", + "inspire.emptyDesc": "Pick a path. You don’t need every control.", + "inspire.entryTopic": "Start from a topic", + "inspire.entryPaste": "I have a draft — rewrite in persona", + "inspire.startTopicHint": "Type a topic below and press Enter", + "inspire.pasteDraftHint": "Paste your draft into the box, then rewrite", + "inspire.needMaterialOrPaste": "No chat material yet — paste text to rewrite", + "inspire.flowOneLiner": "Talk it through, search when needed, then turn the conversation into a post.", + "inspire.showTopics": "Topics", + "inspire.hideTopics": "Hide topics", + "inspire.showLibrary": "Library", + "inspire.hideLibrary": "Hide library", + "inspire.showAdvanced": "Advanced", + "inspire.hideAdvanced": "Hide advanced", + "inspire.readyToWrite": "{n} turns in — ready to draft", + "inspire.inputAria": "Talk to AI", + "inspire.inputPh": "What to explore? Enter to send · Shift+Enter newline", + "inspire.previewTitle": "Full payload that will be sent", + "inspire.previewPrompt": "Full prompt (persona / brands / pins / chat — same as AI)", + "inspire.previewSections": "Included sections", + "inspire.previewPinnedCount": "{n} pinned elements", + "inspire.previewNoPins": "No pinned elements yet", + "inspire.runes": "chars", + "inspire.copyAll": "Copy all", + "inspire.copied": "Full prompt copied", + "inspire.copyFail": "Copy failed", + "inspire.rawPrompt": "Raw text sent to AI", + "inspire.verifyHow": "To verify: open ? for fingerprint → send without changes → status says match.", + "inspire.verifyHowShort": "Type → ? (previews Send path) → send unchanged → should match. Generate uses different mode (fingerprint will differ).", + "inspire.previewModeNote": "Assembled for mode={mode} (same as sending with that mode).", + "inspire.lastSentFp": "Last sent fingerprint", + "inspire.matchOk": "Matches last send ✓", + "inspire.matchBad": "Differs from preview (input/pins/persona/mode)", + "inspire.matchBadShort": "≠ last sent {sent}", + "inspire.fpMatch": "Sent fingerprint {fp} matches preview", + "inspire.fpMismatch": "Different: preview {preview} ≠ sent {sent} (text/mode changed?)", + "inspire.fpSent": "Sent fingerprint {fp}", + "inspire.viewSent": "View sent prompt", + "inspire.viewSentShort": "Sent", + "inspire.sentPrompt": "Prompt actually sent", + "inspire.sentPromptNote": "Exact prompt the backend sent to the AI (from stream done).", + "inspire.send": "Send", + "inspire.generate": "Turn into post", + "inspire.generateHint": "Turn the whole conversation into a publish-ready post using the selected persona and strong engagement principles", + "inspire.webSearch": "Search web", + "inspire.webSearchOn": "Web search: on", + "inspire.webSearchHint": "The next message will search Exa before AI responds", + "inspire.needConversation": "Share one thought first, then turn it into a post.", + "inspire.needReadyPersona": "Select a persona that has finished analysis first.", + "inspire.generating": "Rewriting…", + "inspire.generateOk": "Draft rewritten in persona", + "inspire.materialTitle": "Lock content to write", + "inspire.materialHint": "Generate only rewrites this block (editable). Chat is for ideation; this is for the final voice.", + "inspire.materialLabel": "Content to rewrite", + "inspire.materialPh": "Key points, angle, what you want to say…", + "inspire.rewriteNotes": "Rewrite notes (optional)", + "inspire.rewriteNotesPh": "e.g. shorter, more casual, end with a question", + "inspire.rewriteDefault": "Rewrite as a Threads post in persona voice", + "inspire.confirmRewrite": "Rewrite in persona", + "inspire.needMaterial": "Chat a bit first, or paste text to rewrite", + "inspire.library": "Element library", + "inspire.addNew": "+ Add", + "inspire.kind": "Type", + "inspire.kindRole": "Role prompt", + "inspire.kindSnippet": "Snippet", + "inspire.kindTrendNote": "Trend note", + "inspire.kindBrand": "Brand", + "inspire.kindTrend": "Trend", + "inspire.name": "Name", + "inspire.namePh": "e.g. pro Threads writer", + "inspire.body": "Content (goes into prompt)", + "inspire.bodyPh": "You are a…", + "inspire.saveElement": "Save to library", + "inspire.citeBrand": "Cite brand", + "inspire.applied": "Applied", + "inspire.clickApply": "Click to apply", + "inspire.noBrands": "No brands yet", + "inspire.appliedToggle": "Applied · click again to remove", + "inspire.deleteAria": "Delete", + "inspire.needInput": "Type what you want to explore", + "inspire.fail": "Failed", + "inspire.wantWrite": "Want to write about {label}: {summary}", + "inspire.trendBody": "Topic: {label}. {summary}", + "inspire.pinnedTrend": "Applied trend {label}", + "inspire.needTitleBody": "Name and content required", + "inspire.added": "Added to library", + "inspire.addFail": "Add failed", + "inspire.confirmRemove": "Remove this from the library?", + "inspire.confirmClear": "Start a new chat? (previous chats stay in the list)", + "inspire.genMessage": "Rewrite as a Threads post in persona voice", + + "persona.add": "New persona", + "persona.empty": "No personas yet", + "persona.emptyDesc": "Add one and analyze it for drafting.", + "persona.statusReady": "ready", + "persona.statusAnalyzing": "analyzing", + "persona.statusPending": "pending", + "persona.default": "Default", + "persona.backList": "← Personas", + "persona.tabOverview": "Overview", + "persona.tabAnalyze": "Analyze", + "persona.tabFingerprint": "Fingerprint", + "persona.tabPreview": "Preview", + "persona.name": "Name", + "persona.brief": "Brief", + "persona.briefPh": "Who, for whom, core message…", + "persona.avoid": "Guardrails · banned words (comma-separated)", + "persona.guardChars": "{n} chars", + "persona.banAi": " · ban AI tone", + "persona.notReadySuffix": " · not ready", + "persona.setDefault": "Set as default", + "persona.modeAccount": "Public account", + "persona.modeText": "Paste text", + "persona.username": "Threads username", + "persona.fromBound": "From linked account", + "persona.select": "Select…", + "persona.crawlAnalyze": "Analyze public posts", + "persona.crawlBusy": "Analyzing…", + "persona.refText": "Reference text (--- separates posts)", + "persona.refTextPh": "First post…\n\n---\n\nSecond post…", + "persona.sourceLabel": "Source note (optional)", + "persona.sourcePh": "My old posts", + "persona.analyzeText": "Analyze from text", + "persona.analyzeBusy": "Analyzing…", + "persona.sampleMeta": "Samples {n}", + "persona.sourceManual": "Pasted", + "persona.analyzeHint": "After analysis, 8D summaries appear here.", + "persona.fingerprintHint": "Main voice for drafting. Edit catchphrases, rhythm, bans; Studio/replies use this.", + "persona.fingerprint": "Language fingerprint", + "persona.fingerprintPh": "Filled after analysis…", + "persona.saveFingerprint": "Save fingerprint", + "persona.tryGen": "Preview root + reply", + "persona.previewHint": "Write a sample root post and reply in this fingerprint style. Uses a live news headline as inspiration (rewritten in-character, not a news dump).", + "persona.previewRunning": "Generating…", + "persona.previewDone": "Preview done · topic: {topic} ({source})", + "persona.previewFail": "Preview failed — try again", + "persona.previewTopicLabel": "Topic seed: {topic} · {source}", + "persona.topicNews": "live news", + "persona.topicManual": "manual", + "persona.topicFallback": "everyday seed", + "persona.notReadyMsg": "Persona not ready", + "persona.rootPost": "Root post", + "persona.reply": "Reply", + "persona.hidePrompt": "Hide prompt block", + "persona.showPrompt": "Show injected prompt", + "persona.promptBlock": "prompt block (post)", + "persona.pickOne": "Pick a persona", + "persona.pickDesc": "Or add one to start analyzing.", + "persona.created": "Created — finish account analysis or paste text under Analyze", + "persona.saved": "Saved", + "persona.textDone": "Text analysis done · {n} segments → ready", + "persona.analyzeFail": "Analysis failed", + "persona.reading": "Reading public posts…", + "persona.accountDone": "@{user} · {n} posts → ready", + "persona.jobQueued": "Queued as a background job · you can leave; results save automatically", + "persona.jobQueuedCrawl": "Analyze job queued · you can leave; persona updates when done", + "persona.jobRunning": "Analyzing in background… will refresh when ready (see Jobs for progress)", + "persona.jobDone": "Background analysis finished · fingerprint saved", + "persona.jobFailed": "Background analysis failed — check Jobs for the error or retry", + "persona.loadFail": "Could not load personas", + "persona.openJob": "Open job details", + "persona.setDefaultMsg": "“{name}” set as default", + "persona.confirmDelete": "Delete persona “{name}”?", + "persona.deleted": "Persona deleted", + "persona.needReady": "Finish analysis first (ready)", + "persona.dim.d1Tone": "D1 Tone", + "persona.dim.d2Structure": "D2 Structure", + "persona.dim.d3Interaction": "D3 Interaction", + "persona.dim.d4Topics": "D4 Topics", + "persona.dim.d5Rhythm": "D5 Rhythm", + "persona.dim.d6Visual": "D6 Visual", + "persona.dim.d7Conversion": "D7 Conversion", + "persona.dim.d8Risk": "D8 Risk", + + "admin.users.loadFail": "Load failed", + "admin.users.created": "Added islander “{name}” · copy the password below", + "admin.users.createFail": "Create failed", + "admin.users.unlimitedOn": "“{name}” set to unlimited (usage still counted)", + "admin.users.unlimitedOff": "“{name}” back to plan limits", + "admin.users.updateFail": "Update failed", + "admin.users.planSet": "“{name}” plan → {plan}", + "admin.users.confirmSuspend": "Suspend “{name}”?\\nThey will not be able to sign in.", + "admin.users.confirmUnsuspend": "Restore “{name}”?\\nThey can sign in again.", + "admin.users.didSuspend": "Suspended “{name}”", + "admin.users.didUnsuspend": "Restored “{name}”", + "admin.users.suspendFail": "Suspend failed", + "admin.users.unsuspendFail": "Restore failed", + "admin.users.markedVerified": "Marked {name} as email verified", + "admin.users.markedUnverified": "Marked {name} as unverified", + "admin.users.rolesUpdated": "Updated roles for {name}: {roles}", + "admin.users.rolesFail": "Role update failed", + "admin.users.confirmReset": "Reset password for “{name}”?\\nTemp password stays visible until you close it (survives refresh).", + "admin.users.resetDone": "Reset password for {name} (shown below — copy then close)", + "admin.users.resetFail": "Reset failed", + "admin.users.copied": "Copied to clipboard", + "admin.users.copyFail": "Copy failed — select the password manually", + "admin.users.confirmDismissTemp": "After close, this temp password won't show again (copy first if needed). Close?", + "admin.users.tempPwNew": "New islander temp password", + "admin.users.tempPw": "Temp password", + "admin.users.tempPwPersist": "(stays visible · survives refresh)", + "admin.users.copyPw": "Copy password", + "admin.users.close": "Close", + "admin.users.createTitle": "Add islander", + "admin.users.memberName": "Display name", + "admin.users.displayNamePh": "Display name", + "admin.users.email": "Email", + "admin.users.initPassword": "Initial password (optional)", + "admin.users.initPasswordPh": "Leave blank to auto-generate; if set, must meet policy", + "admin.users.markVerifiedCheck": "Mark email verified (usable immediately)", + "admin.users.alsoAdmin": "Also make admin", + "admin.users.creating": "Creating…", + "admin.users.createSubmit": "Create islander", + "admin.users.clear": "Clear", + "admin.users.searchActive": "Search “{query}” · matches name, email, uid", + "admin.users.noMatch": "No matches", + "admin.users.none": "No islanders yet", + "admin.users.you": "You", + "admin.users.status": "Status", + "admin.users.role": "Roles", + "admin.users.emailVerify": "Email verification", + "admin.users.bio": "Bio", + "admin.users.timezone": "Timezone", + "admin.users.notifyEmail": "Email notifications", + "admin.users.on": "On", + "admin.users.off": "Off", + "admin.users.createdAt": "Created", + "admin.users.updatedAt": "Updated", + "admin.users.accountStatus": "Account status", + "admin.users.updating": "Updating…", + "admin.users.usageTitle": "Usage & plan", + "admin.users.usageLiveSkip": "Plan/quota are Usage domain — not on live backend yet (M3). Editable in mock only.", + "admin.users.plan": "Plan", + "admin.users.planOption": "{name} ({credits} credits / mo)", + "admin.users.unlimited": "Unlimited", + "admin.users.byPlan": "By plan", + "admin.users.setUnlimited": "Set unlimited", + "admin.users.setLimited": "Enforce plan limits", + "admin.users.unlimitedHint": "Unlimited: can keep using past plan cap; AI/Search counts and credits still track.", + "admin.users.loadingUsage": "Loading usage prefs…", + "admin.users.assignRoles": "Assign roles", + "admin.users.memberBase": "{role} (base, always on)", + "admin.users.adminDesc": "{role} — manage islanders and system", + "admin.users.saving": "Saving…", + "admin.users.saveRoles": "Save roles", + "admin.users.markUnverifiedBtn": "Mark unverified", + "admin.users.markVerifiedBtn": "Mark verified", + "admin.users.resetting": "Resetting…", + "admin.users.resetTemp": "Reset password (temp)", + "admin.users.customPw": "Or set a password (optional)", + "admin.users.customPwPh": "Password must be at least 12 characters with upper, lower, digit, and symbol", + "admin.users.resetWithCustom": "Reset with this password", + + "usage.tabMine": "My usage", + "usage.tabTenant": "All usage", + "usage.currentPlan": "Current plan", + "usage.planMeta": "/ mo · {n} credits monthly", + "usage.changePlan": "Change plan", + "usage.upgradePlan": "Upgrade plan", + "usage.outcome.title": "This month's outcomes", + "usage.outcome.summary": "Reach {reach} · Conversations {conversations} · Conversions {conversions}", + "usage.outcome.amount": " (~${amount})", + "usage.outcome.emptyHint": "No attributable outcomes this month yet — try patrol or posting.", + "usage.warn.unlimitedOver": "Used {used} credits this month (plan {cap}; limits off — you can continue).", + "usage.warn.exhausted": "Monthly credits are used up. Upgrade or wait for next month.", + "usage.warn.high": "You've used {pct}% of monthly credits.", + "usage.warn.meterNear": "{label} is near its cap ({credits}/{cap} credits).", + "usage.usedThisMonth": "Used this month", + "usage.remainLabel": "Remaining", + "usage.ledgerToggle": "Usage log", + "usage.collapse": "Collapse", + "usage.eventsCount": "{n} events", + "usage.granularity": "Granularity", + "usage.day": "Day", + "usage.monthUnit": "Month", + "usage.year": "Year", + "usage.from": "From", + "usage.to": "To", + "usage.callCounts": "Call counts", + "usage.noMembers": "No members yet", + "usage.planAria": "{name} plan", + "usage.unlimitedTitle": "Unlimited", + "usage.setLimited": "Enforce limits", + "usage.setUnlimited": "Set unlimited", + "usage.limitShort": "Cap", + "usage.subscribed": "Subscribed to {name}", + "usage.unlimitedSet": "Set unlimited", + "usage.limitedSet": "Limits enforced", + "usage.planUpdated": "Plan updated to {name}", + "usage.fail": "Failed", + + "currency.TWD": "New Taiwan Dollar (TWD)", + "currency.USD": "US Dollar (USD)", + "currency.JPY": "Japanese Yen (JPY)", + "currency.EUR": "Euro (EUR)", + "currency.HKD": "Hong Kong Dollar (HKD)", + + "locale.zh-TW": "繁體中文", + "locale.en": "English", + + "pager.nav": "Pagination", + "pager.pageSize": "Items per page", + "pager.perPage": "{n}/page", + "pager.prev": "Previous", + "pager.next": "Next", + + "plays.defaultTitle": "New play", + "plays.topicOnPost": "On: {snippet}", + "plays.topicOnExternal": "On: {label} · {snippet}", + "plays.externalFallback": "External post", + + "persona.newName": "New persona", + "persona.previewTopic": "Looking for a café I can sit in for hours", + "persona.previewReplySample": "The one in Da’an is fine but crowded", + + "inspire.playTitle": "Inspired thread", + + "play.err.needLead": "Pick a lead account", + "play.err.needRoot": "Add at least one root post", + "play.err.firstMustRoot": "The first step must be the root post", + "play.err.rootMustLead": "Root post must use the lead account", + "play.err.rootEmpty": "Root post text cannot be empty", + "play.err.replyAccount": "Replies can only use lead or selected cast accounts", + "play.err.replyEmpty": "Reply text cannot be empty", + "play.err.needTarget": "Pick one of your posts, or paste a Threads link", + "play.err.needReplies": "Add at least one reply", + "play.err.needReplyAccounts": "Pick at least one reply account", + "play.err.stepAccount": "Every reply needs a usable account", + "play.err.stepEmpty": "Reply text cannot be empty", + "play.err.notFound": "Play not found", + + "time.justNow": "Just now", + "time.minAgo": "{n}m ago", + "time.hourAgo": "{n}h ago", + "time.dayAgo": "{n}d ago", + "time.min": "{n} min", + "time.hour": "{n} hr", + "time.day": "{n} day", + "time.expired": "Expired {span}", + "time.remaining": "{span} left", + "time.sessionUnknown": "Session not recorded", + "time.sessionExpired": "Session expired · {absolute}", + "time.sessionSoon": "Session expiring · {relative} ({absolute})", + "time.sessionOk": "Session OK · {relative} ({absolute})", + + "policy.title": "Opportunity and reply policy", + "policy.subtitle": "Set service scope, forbidden words, cases, FAQs, and tone. Qualification and generated replies share this policy.", + "radar.profile.title": "Service profile", + "radar.profile.subtitle": + "The radar uses this to judge whether an opportunity is worth answering, and replies quote these prices and tone.", + "radar.profile.firstTimeHint": + "Fill this in before turning on a radar watch. The more specific it is, the better the scoring and replies.", + "radar.profile.updatedAt": "Last updated: {at}", + "radar.profile.saved": "Service profile saved", + "radar.profile.services": "Services and pricing", + "radar.profile.servicesHint": + "At least one. Leave prices empty to mean negotiable; if filled they show up in replies.", + "radar.profile.serviceName": "Service name", + "radar.profile.serviceNamePh": "e.g. Interior measuring and layout", + "radar.profile.priceMin": "Price from", + "radar.profile.priceMax": "Price to", + "radar.profile.addService": "+ Add service", + "radar.profile.areas": "Service areas", + "radar.profile.areasHint": + "Pick the cities you can actually serve; mismatched leads score lower. Tick remote below if location does not matter.", + "radar.profile.remoteOk": "Remote work is fine (any location)", + "radar.profile.forbidden": "Forbidden words", + "radar.profile.forbiddenHint": "One per line. These never appear in any generated reply.", + "radar.profile.forbiddenPh": "guaranteed\ncheapest\nnumber one", + "radar.profile.cases": "Case studies", + "radar.profile.casesHint": "Optional. Replies cite these when proof helps; if empty they cite nothing.", + "radar.profile.caseTitle": "Case title", + "radar.profile.caseSummary": "One-line summary", + "radar.profile.addCase": "+ Add case", + "radar.profile.faq": "FAQ", + "radar.profile.faqHint": "Optional. Replies follow these answers when a similar question comes up.", + "radar.profile.faqQuestion": "Question", + "radar.profile.faqAnswer": "Answer", + "radar.profile.addFaq": "+ Add FAQ", + "radar.profile.availability": "Availability", + "radar.profile.availabilityHint": "e.g. Can start within two weeks, weekends only", + "radar.profile.toneNote": "Tone note", + "radar.profile.toneNoteHint": "e.g. Be direct, skip pleasantries, no exclamation marks", + + "radar.watches.title": "Demand watches", + "radar.watches.subtitle": + "Subscribe to keywords for a daily auto list. Unlike Patrol, you don’t re-run a manual scan each time.", + "radar.watches.needProfile": "Fill in the service profile before enabling demand watches", + "radar.watches.needProfileHint": + "Scoring needs your service profile; without it every judgment is a guess.", + "radar.watches.goProfile": "Go to service profile", + "radar.watches.quota": "Active {used} / {max}", + "radar.watches.quotaFull": "Plan limit reached — pause or archive one to add another", + "radar.watches.add": "+ New watch", + "radar.watches.newTitle": "New demand watch", + "radar.watches.editTitle": "Edit demand watch", + "radar.watches.requiredHint": "Required field", + "radar.watches.terms": "Terms", + "radar.watches.termsHint": "One short query per line. Max 2 tokens, CJK 2–4 chars each — long sentences are shortened on save so Threads can search them.", + "radar.watches.threadsWarn": "Some terms break Threads short-query rules: max 2 words, CJK 2–4 chars each, ≤12 chars total, no punctuation/#/emoji. Save shortens them into searchable queries.", + "radar.watches.threadsRequired": "These terms cannot be shortened into Threads-searchable queries. Use at most 2 tokens, CJK 2–4 chars each.", + "radar.watches.termsPh": "interior design\nfind designer", + "radar.watches.excludeTerms": "Exclude terms", + "radar.watches.excludeHint": "A hit here skips the post entirely, e.g. job ads or giveaways.", + "radar.watches.excludePh": "hiring\ngiveaway", + "radar.watches.regionsHint": + "Leave regions empty to reuse the service profile areas; only narrow it down for this watch.", + "radar.watches.regionsFromProfile": "Regions from service profile", + "radar.watches.enableNow": "Activate right away (uses an active slot)", + "radar.watches.filterStatus": "Status", + "radar.watches.statusAll": "All", + "radar.watches.status.active": "Active", + "radar.watches.status.paused": "Paused", + "radar.watches.status.archived": "Archived", + "radar.watches.pause": "Pause", + "radar.watches.resume": "Resume", + "radar.watches.archive": "Archive", + "radar.watches.confirmArchive": "Archived watches stop sweeping and cannot be restored. Continue?", + "radar.watches.deleteArchived": "Delete archived watch", + "radar.watches.deletingArchived": "Deleting…", + "radar.watches.confirmDeleteArchived": "Permanently delete this archived watch setting? Existing opportunities, sweeps, and statistics stay, but this setting will no longer appear in the list.", + "radar.watches.deletedArchived": "Archived watch setting deleted", + "radar.watches.created": "Watch created", + "radar.watches.createdFirstSweep": "Watch created — first sweep queued. Check today's demand in a few minutes (it runs automatically every day after this).", + "radar.watches.updated": "Watch updated", + "radar.watches.paused": "Watch paused", + "radar.watches.resumed": "Watch resumed", + "radar.watches.archived": "Watch archived", + "radar.watches.lastSwept": "Last sweep: {at}", + "radar.watches.neverSwept": "Never swept", + "radar.watches.empty": "No demand watches yet", + "radar.watches.emptyHint": + "Add short buyer phrases (e.g. “find designer”); the system sweeps daily. For a one-off pain/topic sortie, use Patrol.", + "radar.watches.emptyFiltered": "No watches in this status", + "radar.watches.scheduleTitle": "Daily patrol: 06:00 Taipei (22:00 UTC)", + "radar.watches.scheduleHint": "Active watches run once a day. Use Run now for an extra pass. Turning off Run now does not stop the daily patrol.", + "radar.watches.openToday": "Back to findings", + "radar.watches.sweepNow": "Run now", + "radar.watches.sweepQueued": "Demand sweep queued", + "radar.watches.sweepStarted": "Demand sweep started (job {job}…)", + + "today.radar.title": "Today's demand", + "today.radar.total": "Found", + "today.radar.high": "High", + "today.radar.mid": "Mid", + "today.radar.low": "Low", + "today.radar.open": "Open today's demand", + "today.radar.empty": "No demand yet. Add keyword watches for a daily auto list (different from Patrol’s manual scan).", + "today.radar.goProfile": "Fill service profile", + "today.radar.goWatches": "Add keyword watches", + + "firstRun.title": "Connect a Threads account first", + "firstRun.subtitle": "After you connect, the rest of the app unlocks. Tap the button to open Accounts.", + "firstRun.skip": "Skip — I'll look around", + "firstRun.go": "Connect", + "firstRun.progress": "Step {current} of {total}", + "firstRun.done": "Done", + "firstRun.aria": "First-run setup", + "firstRun.step.crew": "Connect a Threads account", + "firstRun.step.crewHint": "Needed later if you want to send a reply.", + "firstRun.step.brands": "Set brand and product", + "firstRun.step.brandsHint": "Fill audience, pains, and product capabilities.", + "firstRun.step.watch": "Create a daily patrol", + "firstRun.step.watchHint": "Pick the product, adopt suggested keywords, and save.", + "firstRun.step.radar": "Review the first demand", + "firstRun.step.radarHint": "No need to wait until tomorrow — use Explore now.", + "firstRun.status.pending": "Onboarding", + "firstRun.status.skipped": "Onboarding skipped", + "firstRun.status.completed": "Onboarding done", + + "radar.suggest.title": "Term suggestions", + "radar.suggest.hint": + "Terms drawn from your service profile. Adopt them one by one or all at once; you still need to save to create the watch.", + "radar.suggest.ask": "Get suggestions", + "radar.suggest.again": "Suggest more", + "radar.suggest.asking": "Thinking…", + "radar.suggest.adopt": "Adopt", + "radar.suggest.adopted": "Adopted", + "radar.suggest.adoptAll": "Adopt all", + "radar.suggest.include": "Term", + "radar.suggest.exclude": "Exclude", + "radar.suggest.none": "Nothing usable came back. Make the service profile more specific and try again.", + + "radar.today.title": "Today's demand", + "radar.today.subtitle": "Daily auto list from your watches (not Patrol’s one-off scan).", + "radar.today.link.watches": "Demand watches", + "radar.today.link.crm": "CRM board", + "radar.today.stats.total": "Found today", + "radar.today.stats.high": "High intent", + "radar.today.stats.mid": "Mid intent", + "radar.today.stats.low": "Low intent", + "radar.today.truncated": "Daily cap reached; {n} lower-intent leads were not listed", + "radar.today.lastSwept": "Last sweep: {at}", + "radar.today.band.high": "High", + "radar.today.band.mid": "Mid", + "radar.today.band.low": "Low", + "radar.today.status.accepted": "Added to CRM", + "radar.today.status.dismissed": "Dismissed", + "radar.today.status.qualified": "Open", + "radar.today.status.rejected": "Rejected", + "radar.today.status.judging": "Judging", + "radar.today.regionUnknown": "Region unknown", + "radar.today.group.high": "High intent", + "radar.today.group.mid": "Mid intent", + "radar.today.group.low": "Low intent", + "radar.today.group.empty": "None in this band", + "radar.today.group.expand": "Expand", + "radar.today.group.collapse": "Collapse", + "radar.today.action.open": "Original", + "radar.today.action.accept": "Add to CRM", + "radar.today.action.dismiss": "Dismiss", + "radar.today.action.reply": "Generate reply", + "radar.today.action.hideReply": "Hide reply", + "radar.today.action.reasons": "Why this score", + "radar.today.action.hideReasons": "Hide reasons", + "radar.today.action.override": "Override band", + "radar.today.reply.hint": "Pick a variant. DM is copy-only — never auto-sent.", + "radar.today.reply.copy": "Copy draft", + "radar.today.reply.variant.public_comment": "Public comment", + "radar.today.reply.variant.dm": "DM", + "radar.today.reply.variant.no_sales": "No-sales", + "radar.today.reply.variant.professional": "Professional", + "radar.today.reply.variant.humorous": "Light", + "radar.today.dim.authenticity": "Authenticity", + "radar.today.dim.intent": "Intent", + "radar.today.dim.region": "Region", + "radar.today.dim.freshness": "Freshness", + "radar.today.dim.fit": "Fit", + "radar.today.empty.title": "No demand for today yet", + "radar.today.empty.fallback": "Check back later, or review demand watches and the service profile.", + "radar.today.empty.goProfile": "Fill service profile", + "radar.today.empty.goWatches": "Add keyword watches", + "radar.today.empty.goAll": "View all results", + "radar.today.empty.reason.no_profile": "No service profile yet — fit cannot be scored.", + "radar.today.empty.reason.no_watch": "No demand watches yet; create keywords for daily auto sweeps (not Patrol’s manual scan).", + "radar.today.empty.reason.all_watches_paused": "All watches are paused. Resume one to keep daily sweeps.", + "radar.today.empty.reason.not_swept_yet": "Daily patrol hasn’t finished yet; you can also hit Run now on Demand.", + "radar.today.empty.reason.sweep_failed": "This auto sweep failed — check demand watches and retry.", + "radar.today.empty.reason.no_hit": "Swept but no matching demand. Loosen terms or exclusions.", + "radar.today.msg.accepted": "Added to CRM", + "radar.today.msg.dismissed": "Dismissed", + "radar.today.msg.replyReady": "Reply draft ready", + "radar.today.msg.overridden": "Band updated", + "radar.today.msg.copied": "Copied to clipboard", + "radar.today.msg.copyFail": "Could not copy — select the text manually", + "radar.today.msg.marked": "Marked as sent/copied", + "radar.today.msg.sent": "Sent — check progress in the outbox queue", + "radar.today.msg.needReply": "Generate a reply draft first", + "radar.today.sendAccount": "Send from", + "radar.today.reply.markCopy": "Mark as copied & sent", + "radar.today.reply.markOutbox": "Send now (Outbox)", + "radar.today.reply.needAccount": "Connect a Threads account first to send", + "radar.today.reply.used": "Already marked used", + "radar.today.reply.usedOutbox": "Sent (check the outbox queue)", + + "radar.reconnectSearch": "Reconnect search source", + "radar.patrol.searchFallback": "If search is temporarily unavailable, a fallback source is used. Credits are charged only on a successful result.", + "radar.empty.sweepFailedHint": "Patrol failed. Run it again, or switch to last 7 days / all.", + "radar.inbox.title": "Demand", + "radar.inbox.patrolAria": "Patrol status", + "radar.inbox.scheduledOn": "Daily patrol: on", + "radar.inbox.scheduledOff": "Daily patrol: off", + "radar.inbox.scheduleHint": "Runs every day at 06:00 Taipei time. Turning off Run now does not stop the daily patrol.", + "radar.inbox.lastSweep": "Last patrol: {time}", + "radar.inbox.neverSwept": "Not patrolled yet", + "radar.inbox.activeWatches": "{n} watches on", + "radar.inbox.allPaused": "All watches are paused. Run now also needs at least one on.", + "radar.inbox.noWatches": "No product or keywords to patrol yet", + "radar.inbox.sweepNow": "Run now", + "radar.inbox.sweeping": "Patrolling…", + "radar.inbox.sweepAgain": "Run again", + "radar.inbox.setupWatches": "Set up patrol", + "radar.inbox.introTitle": "Pain points land here", + "radar.inbox.introBody": "Read why it was recommended, then keep or discard. Adding to CRM is optional.", + "radar.inbox.resultsAria": "Demand results", + "radar.inbox.tabsAria": "Result state", + "radar.inbox.tab.pending": "New", + "radar.inbox.tab.completed": "Reviewed", + "radar.inbox.tab.removed": "Discarded", + "radar.inbox.total": "{n} total", + "radar.inbox.clearFilters": "Clear filters", + "radar.inbox.defaultToday": "Showing what was found today by default", + "radar.inbox.timeScope": "Time range", + "radar.inbox.time.today": "Today", + "radar.inbox.time.7d": "Last 7 days", + "radar.inbox.time.all": "All", + "radar.inbox.sort": "Sort by", + "radar.inbox.sort.recommended": "Best product fit", + "radar.inbox.sort.newest": "Newest posts", + "radar.inbox.sort.oldest": "Oldest posts", + "radar.inbox.sort.productFit": "Closest product match", + "radar.inbox.sort.demandIntent": "Clearest demand", + "radar.inbox.moreFilters": "More filters", + "radar.inbox.moreFiltersN": "More filters ({n})", + "radar.inbox.hideFilters": "Hide extra filters", + "radar.inbox.moreFiltersAria": "More filters", + "radar.inbox.brand": "Brand", + "radar.inbox.allBrands": "All brands", + "radar.inbox.product": "Product", + "radar.inbox.allProducts": "All products", + "radar.inbox.band": "Intent", + "radar.inbox.allBands": "All intent", + "radar.inbox.band.high": "High", + "radar.inbox.band.mid": "Medium", + "radar.inbox.band.low": "Low", + "radar.inbox.match": "Product match", + "radar.inbox.allStates": "All states", + "radar.inbox.state.eligible": "Follow up", + "radar.inbox.state.weak": "Weak fit", + "radar.inbox.state.excluded": "Excluded", + "radar.inbox.state.generic": "No product", + "radar.inbox.state.stale": "Older than 14 days", + "radar.inbox.loading": "Preparing patrol results…", + "radar.inbox.prevPage": "Previous", + "radar.inbox.nextPage": "Next", + "radar.inbox.pageOf": "Page {page} of {pages}", + "radar.inbox.goCrm": "Open CRM", + "radar.inbox.see7d": "See last 7 days", + "radar.inbox.empty.filteredPending": "No results for this filter", + "radar.inbox.empty.filteredCompleted": "No reviewed results yet", + "radar.inbox.empty.filteredRemoved": "No discarded results yet", + "radar.inbox.empty.filteredHint": "Clear filters or change the time range. Fresh patrol results may also be under Last 7 days or All.", + "radar.inbox.empty.noCompleted": "Nothing reviewed yet", + "radar.inbox.empty.noCompletedHint": "Switch back to New to keep going through pain points.", + "radar.inbox.empty.noRemoved": "Nothing discarded yet", + "radar.inbox.empty.noRemovedHint": "Switch back to New to keep going through pain points.", + "radar.inbox.empty.noWatchesTitle": "No patrol set up", + "radar.inbox.empty.noWatchesHint": "Pick a product and the keywords customers search. Then you can run now; daily patrol will follow.", + "radar.inbox.empty.pausedTitle": "Daily patrol is off", + "radar.inbox.empty.pausedHint": "Run now and daily patrol both stay on this page. Turn at least one watch back on to use either.", + "radar.inbox.empty.openSchedule": "Turn on daily patrol", + "radar.inbox.empty.neverTitle": "Not patrolled yet", + "radar.inbox.empty.neverHint": "Daily patrol is on. You can also press Run now. This is not an empty inbox — the first round has not finished.", + "radar.inbox.empty.failedTitle": "The last patrol did not finish", + "radar.inbox.empty.noHitsTitle": "Search found no posts", + "radar.inbox.empty.noHitsHint": "It was empty before scoring: keywords too long, too product-named, or Threads returned nothing. Use 2–4 character pain words customers type.", + "radar.inbox.empty.editTerms": "Edit keywords", + "radar.inbox.empty.noFitTitle": "Patrol ran, but no matching pain points", + "radar.inbox.empty.noFitStats": "Search hits {hits}, judged {judged}, created {created}. Older posts may show under Last 7 days or All.", + "radar.inbox.empty.noFitHint": "New posts or product-fit demand will appear here. You can also check Last 7 days or All, or change keywords.", + "radar.inbox.msg.kept": "Kept. No CRM contact was created.", + "radar.inbox.msg.removed": "Discarded. You can restore it from Discarded.", + "radar.inbox.msg.restored": "Restored.", + "radar.inbox.msg.accepted": "Added to CRM. This step is optional — go there later if you want to follow up.", + "radar.inbox.msg.widened7d": "Not every patrol result was posted today. Switched to last 7 days ({n}). Job judge/create counts include rematches, so they may not all be new cards.", + "radar.inbox.msg.widenedAll": "No pending results in the last 7 days. Switched to all ({n}).", + "radar.inbox.msg.alreadyReviewed": "The {n} judged this round are already in Reviewed, so New is empty. Job numbers include rematched older posts.", + "radar.inbox.msg.waitingWorker": "Run now queued · waiting for a worker", + "radar.inbox.err.noActive": "No daily patrol is on. Set product keywords first, or resume a watch.", + "radar.inbox.err.noJob": "No patrol job was queued.", + "radar.inbox.msg.running": "Patrol is running… pain points appear here when it finishes.", + "radar.inbox.err.failed": "Patrol failed.", + "radar.inbox.err.cancelled": "Patrol was cancelled, so it is not marked complete. You can run it again.", + "radar.inbox.msg.queuedN": "Queued {n} patrols still running in the background. You can leave this page; results will stay here.", + "radar.inbox.msg.doneWithSummary": "{summary} Posts not from today also stay in the list.", + "radar.inbox.msg.done": "This patrol finished. Matching pain points are listed below.", + "radar.card.priority.high": "Follow first", + "radar.card.priority.review": "Worth a look", + "radar.card.priority.low": "Low priority", + "radar.card.fitProduct": "Fit · {label}", + "radar.card.noProduct": "No product match", + "radar.card.intent": "Intent {n}", + "radar.card.why": "Why recommended: ", + "radar.card.unknownAuthor": "Unknown author", + "radar.card.openOriginal": "View original on Threads", + "radar.card.actionsAria": "Opportunity actions", + "radar.card.keep": "Keep", + "radar.card.discard": "Discard", + "radar.card.whyBtn": "Why recommended", + "radar.card.accept": "Add to CRM (optional)", + "radar.card.busy": "Working…", + "radar.card.restore": "Restore", + "radar.card.accepted": "In CRM", + "radar.card.done": "Handled", + "radar.card.removeAria": "Mark as not a fit", + "radar.card.removeTitle": "Why discard?", + "radar.card.removeHint": "After you pick a reason it leaves New, and later patrols will not surface the same post. This does not use credits.", + "radar.card.reason": "Reason", + "radar.card.reason.pain_mismatch": "Does not match product pain", + "radar.card.reason.provider_or_ad": "Vendor / ad post", + "radar.card.reason.stale": "Demand is stale", + "radar.card.reason.already_solved": "Already solved", + "radar.card.reason.duplicate": "Duplicate", + "radar.card.reason.other": "Other", + "radar.card.note": "Notes", + "radar.card.duplicateHint": "In the detail drawer you can pick which original to keep.", + "radar.card.confirmRemove": "Discard", + "radar.drawer.aria": "Opportunity detail", + "radar.drawer.title": "Opportunity detail", + "radar.drawer.noProduct": "No product", + "radar.drawer.close": "Close", + "radar.drawer.closeAria": "Close opportunity detail", + "radar.drawer.intent": "Intent {n}", + "radar.drawer.priority": "Priority {n}", + "radar.drawer.evidence": "Demand evidence", + "radar.drawer.matches": "Product match and risks", + "radar.drawer.generic": "No product assigned. This stays as generic demand.", + "radar.drawer.judge": "Original judgment", + "radar.drawer.openOriginal": "Open original on Threads", + "radar.drawer.hint": "Read the pain and product reasons first. Keep or discard. Add to CRM only if you want to follow this person.", + "radar.sweep.aria": "Patrol funnel", + "radar.sweep.title": "Where this patrol stopped", + "radar.sweep.hint": "Merged matches are not counted as new opportunities.", + "radar.sweep.status.complete": "Done", + "radar.sweep.status.partial_budget": "Paused on budget", + "radar.sweep.status.blocked_budget": "Out of credits", + "radar.sweep.status.failed": "Failed", + "radar.sweep.hits": "Hits", + "radar.sweep.deduped": "Deduped", + "radar.sweep.prefilterPass": "Prefilter pass", + "radar.sweep.prefilterReject": "Prefilter out", + "radar.sweep.cached": "Cached judge", + "radar.sweep.aiJudge": "AI judge", + "radar.sweep.created": "New opportunities", + "radar.sweep.deferred": "Budget deferred", + "radar.sweep.credits": "Credits: search {search} · demand map {map} · judge {judge}", + "radar.sweep.total": "Total {n}", + "radar.sweep.budgetHint": "Unused candidates stay for the next run. Already-judged items are not charged again.", + "radar.cost.aria": "Credit preview", + "radar.cost.title": "Confirm credits before running", + "radar.cost.hint": "The preview itself is free. Credits are used only when the provider returns a result.", + "radar.cost.byok": "BYOK · 0 platform credits", + "radar.cost.platform": "Platform credits", + "radar.cost.fixed": "Fixed", + "radar.cost.range": "Estimated range", + "radar.cost.calls": "Search calls", + "radar.cost.remaining": "Remaining", + "radar.cost.until": "Preview until {time}", + "radar.cost.ceiling": "Max credits for this run", + "radar.cost.ceilingHint": "At least {min}, at most {max}", + "radar.cost.invalid": "The ceiling must be between the fixed cost and the estimated max.", + "radar.cost.confirm": "Confirm and run", + "radar.cost.starting": "Starting…", + "radar.readiness.title": "Product context completeness", + "radar.readiness.audience": "Audience", + "radar.readiness.context": "Context", + "radar.readiness.pain": "Pain", + "radar.readiness.capability": "Capability terms", + "radar.readiness.ok": "Filled", + "radar.readiness.todo": "Missing", + "radar.readiness.hint": "Thin context lowers fit confidence, but you can still create a product radar.", + "radar.match.why": "Why it fits", + "radar.match.hide": "Hide evidence", + "radar.match.basis": "Product basis: {text}", + "radar.match.risks": "Risks: {text}", + "radar.today.empty.goBrands": "Set brand and product", + "radar.today.introTitle": "Start with people worth following up, then decide how to reply", + "radar.today.introBody": "We match Threads posts to your product pains, merge duplicates, and rank by demand score.", + "radar.today.navAria": "Demand radar navigation", + "radar.today.manageWatches": "Manage daily patrol", + "radar.today.viewAll": "View all results", + "radar.today.filterAria": "Filter today's demand", + "radar.today.filterTitle": "Filter today's demand", + "radar.today.filterHint": "Start with everything; narrow by brand or product when the list gets long.", + "radar.today.fit": "Product fit", + "radar.today.allFit": "All fit levels", + "radar.today.fit.strong": "Strong fit", + "radar.today.fit.possible": "Possible", + "radar.today.fit.weak": "Weak fit", + "radar.today.needMore": "Not seeing a post you want?", + "radar.today.setupAria": "First-time demand radar setup", + "radar.today.setupTitle": "First time here — three steps", + "radar.today.setupHint": "After this, daily patrol runs automatically.", + "radar.today.setupStep": "Step {n} of 3", + "radar.today.setup.1.title": "Set brand and product", + "radar.today.setup.1.body": "Fill audience, pains, and product capabilities.", + "radar.today.setup.1.cta": "Go to settings", + "radar.today.setup.2.title": "Create a daily patrol", + "radar.today.setup.2.body": "Pick a product, then adopt suggested keywords.", + "radar.today.setup.2.cta": "Create patrol", + "radar.today.setup.3.title": "Come back and work the list", + "radar.today.setup.3.body": "Start with high scores, then review product evidence.", + "radar.today.productEyebrow": "Recommended product", + "radar.today.noPrimary": "No primary product yet", + "radar.today.fitScore": "Fit {n}", + "radar.today.overridden": "Manually set", + "radar.today.hideEvidence": "Hide product evidence", + "radar.today.showMatches": "View {n} product matches", + "radar.today.genericJudge": "No product set (generic demand scoring)", + "radar.today.msg.primarySet": "Primary product saved. Later high-score matches will not overwrite this.", + + "radar.primary.empty": "No product matches yet", + "radar.primary.label": "Primary product", + "radar.primary.placeholder": "Choose a product", + "radar.primary.needsReason": " · reason required", + "radar.primary.reasonAria": "Primary-product reason", + "radar.primary.reasonPh": "Optional: why this product this time", + "radar.primary.submit": "Set primary", + "radar.primary.defaultReason": "Chosen from product evidence", + + "radar.watches.needBrandProduct": "Pick a brand and product before enabling a product radar.", + "radar.watches.needDemandMap": "Finish the product demand map before starting a product patrol.", + "radar.watches.needBrandProductShort": "Pick a brand and product first.", + "radar.watches.assigned": "Product attached. You cannot swap the product on this watch later.", + "radar.watches.assigning": "Attaching…", + "radar.watches.assign": "Attach this product", + "radar.watches.productGone": "Product unavailable — create a new watch", + "radar.watches.pickBrand": "Pick a brand first", + "radar.watches.pickProduct": "Pick a product under this brand", + + "radar.explore.needProduct": "Pick a brand and product before exploring, so generic hits are not treated as product demand.", + "radar.explore.pickBrand": "Pick a brand", + "radar.explore.pickProduct": "Pick a product", + "radar.explore.productStats": "New demand {created} · merged matches {merged} · scored {matched}", + + "radar.import.needProduct": "Pick a brand and product before importing.", + + "radar.suggest.basis": "Based on: {text}", + + "radar.demand.title": "Product demand map", + "radar.demand.hint": "Confirm real product pains first, then use them to narrow patrol results. Source stays next to each term.", + "radar.demand.aria": "{label} demand map", + "radar.demand.ready": "Ready", + "radar.demand.incomplete": "Needs more", + "radar.demand.version": "Version {n}", + "radar.demand.loading": "Building the demand map…", + "radar.demand.pain": "User pains", + "radar.demand.painHint": "Problems the product solves, e.g. leaks or messy collaboration", + "radar.demand.scenario": "Use scenarios", + "radar.demand.scenarioHint": "How people describe the situation", + "radar.demand.outcome": "Desired outcomes", + "radar.demand.outcomeHint": "The result or improvement they want", + "radar.demand.solution": "Solution signals", + "radar.demand.solutionHint": "Words that show you can help", + "radar.demand.exclusion": "Exclusion signals", + "radar.demand.exclusionHint": "Hiring, ads, and other posts that should not become demand", + "radar.demand.custom": "Custom notes", + "radar.demand.customHint": "Custom notes do not overwrite product data; later product updates still keep the source.", + "radar.demand.save": "Save demand map", + "radar.demand.saving": "Saving…", + "radar.demand.origin.product": "Product", + "radar.demand.origin.ai": "AI suggestion", + "radar.demand.origin.user": "Manual", + "radar.demand.customBasis": "Manual note", + + "radar.query.aria": "Query plan preview", + "radar.query.title": "These short terms will be searched", + "radar.query.hint": "At most two words per group, CJK 2–4 characters each, so Threads can find them. Product names are only supporting context.", + "radar.query.meta": "Input {input} · map v{map}", + "radar.query.group": "Query group {n}", + "radar.query.basis": "Based on: {text}", + "radar.query.exclude": "Exclude: {text}", + "radar.query.adopt": "Use these queries", + "radar.query.empty": "The demand map does not have enough searchable pains or scenarios to build query groups yet.", + "radar.query.defaultBasis": "Product pain", + + "brands.loadFail": "Could not load brands. Try again later.", + "brands.deleteImpact": "This will pause {n} related demand watches. Historical demand and touch records stay.", + + "crm.board.touchProduct": " · Product: {label}", + "crm.board.primaryProduct": "Primary product: {label}", + "crm.board.primaryProductWithBrand": "Primary product: {label} ({brand})", + "crm.board.noProduct": "No product set", + + "utm.title": "UTM tracking links", + "utm.new": "New", + "utm.dest": "Destination URL", + "utm.label": "Label", + "utm.create": "Create", + "utm.empty": "No links yet", + "utm.track": "Track: {url}", + "utm.destLine": "Destination: {url}", + "utm.clicks": "Clicks: {n}", + "utm.copy": "Copy link", + "utm.created": "Tracking link created", + "utm.copied": "Tracking link copied", + "utm.copyFail": "Could not copy — select the URL manually", + + "tools.pain.title": "Pain-keyword generator", + "tools.pain.subtitle": "No login: describe the product to generate patrol terms and pains.", + "tools.pain.brief": "Product brief", + "tools.pain.audience": "Audience (optional)", + "tools.pain.run": "Generate keywords", + "tools.pain.running": "Generating…", + "tools.pain.result": "Result", + "tools.pain.keywords": "Keywords", + "tools.pain.pains": "Pains", + "tools.pain.scan": "Scan terms", + "tools.pain.login": "Sign in to patrol", + + "tools.style.title": "Style fingerprint quiz", + "tools.style.subtitle": "No login: paste a few Threads posts to see tone and rhythm.", + "tools.style.samples": "Post samples", + "tools.style.run": "Analyze", + "tools.style.running": "Analyzing…", + "tools.style.result": "Result", + "tools.style.tone": "Tone: {v}", + "tools.style.rhythm": "Rhythm: {v}", + "tools.style.hooks": "Hooks: {v}", + "tools.style.avoid": "Watch-outs: {v}", + "tools.style.login": "Sign in to Lapras", + + "inspire.source": "Source: {label}", + + "bench.title": "Benchmark", + "bench.reload": "Refresh", + "bench.sample": "Sample {n}", + "bench.median": "Median engagement {eng}% · median views {views}", + "bench.yours": "Your engagement {eng}% · avg views {views}", + "bench.insufficient": "Not enough sample", + + "insights.summaryTitle": "Last 3 months", + "insights.summaryStats": "Posts {posts} · views {views} · likes {likes} · replies {replies} · avg engagement {eng}%", + "insights.summaryTop": "Stronger posts", + "insights.topPostLine": "{eng}% · {views} views · {text}", + + "playbooks.title": "Playbook market", + "playbooks.allKinds": "All types", + "playbooks.kind.brief": "Patrol brief", + "playbooks.kind.persona": "Persona", + "playbooks.kind.play": "Reply play", + "playbooks.nichePh": "Niche (skincare / parenting…)", + "playbooks.mineOnly": "Mine only", + "playbooks.publish": "Publish template", + "playbooks.cancel": "Cancel", + "playbooks.publishCard": "Publish", + "playbooks.fieldTitle": "Title", + "playbooks.fieldNiche": "Niche", + "playbooks.fieldBody": "Content", + "playbooks.anonymous": "Anonymous", + "playbooks.submit": "Submit", + "playbooks.empty": "No templates yet", + "playbooks.imports": "Imported {n}", + "playbooks.import": "Import", + "playbooks.published": "Published", + "playbooks.imported": "Imported into your playbooks", + + "radar.import.open": "Manual import", + "radar.import.close": "Hide manual import", + "radar.import.title": "Manually import demand", + "radar.import.hint": "Paste a Threads/Facebook post URL and its text; it runs the same five-question judge. Arbitrary URLs are not fetched automatically, so paste the text yourself. Use this to top up today's list when volume is low.", + "radar.import.url": "Post URL", + "radar.import.text": "Post text", + "radar.import.author": "Author (optional)", + "radar.import.addRow": "+ Add row", + "radar.import.removeRow": "Remove", + "radar.import.submit": "Import", + "radar.import.submitting": "Importing…", + "radar.import.needRow": "Fill in at least one row with a URL and text", + "radar.import.csvOpen": "Paste CSV instead", + "radar.import.csvClose": "Hide CSV", + "radar.import.csvLabel": "Paste CSV", + "radar.import.csvHint": "Format: url,text,author (author optional). A header row with url/text is auto-detected; otherwise columns are read as url,text,author.", + "radar.import.csvApply": "Apply to rows below", + "radar.import.csvEmpty": "Couldn't parse any rows — check the format", + "radar.import.status.qualified": "Added to today's demand", + "radar.import.status.rejected": "Judged as not a fit (kept for reference)", + "radar.import.status.skipped": "Already imported — skipped", + "radar.import.status.failed": "Import failed", + + "radar.explore.open": "Explore now", + "radar.explore.close": "Hide explore", + "radar.explore.title": "Explore now", + "radar.explore.hint": "Search Threads right away for people looking for your service. Results go through the same five-question judge into today’s list. Max 2 words per query, 2–4 CJK chars each.", + "radar.explore.loadingSuggest": "Loading suggested terms…", + "radar.explore.suggestions": "Suggested short terms (click to add)", + "radar.explore.selected": "Selected terms", + "radar.explore.emptyTerms": "No terms yet — pick a suggestion or add your own.", + "radar.explore.removeChip": "Click to remove", + "radar.explore.addLabel": "Add a short term", + "radar.explore.addPh": "e.g. nanny recommend", + "radar.explore.add": "Add", + "radar.explore.run": "Start explore", + "radar.explore.running": "Exploring…", + "radar.explore.needTerms": "Pick at least one term", + "radar.explore.result": "Found {hits}, judged {judged}, added {created}", + "radar.explore.resultZeroHint": "Nothing new this run. Try more conversational short terms, or adjust daily watches.", + "radar.explore.resultTruncated": "{n} skipped by daily cap — try again tomorrow or upgrade.", + "radar.explore.termError.empty": "Enter a term", + "radar.explore.termError.tooLong": "At most 12 characters without spaces (long Threads queries often return nothing)", + "radar.explore.termError.tooManyTokens": "At most 2 words (space-separated)", + "radar.explore.termError.invalidToken": "No punctuation, emoji, AND/OR, or tokens that are too short/long", + "radar.explore.termError.duplicate": "Already added", + "radar.explore.termError.max": "At most 6 terms per run", + + "scout.promote": "Save to Demand", + "scout.promoted": "Copied into Today’s demand ({band} · {score}) — follow up under Demand", + "scout.promoteFail": "Could not save to Demand", + + "crm.board.title": "CRM board", + "crm.board.subtitle": "Seven stages plus follow-up. Contacts land here after you accept an opportunity.", + "crm.board.link.today": "Today's opportunities", + "crm.board.link.followups": "Follow-ups", + "crm.board.link.stats": "Conversion stats", + "crm.board.filters": "Contact filters", + "crm.board.search": "Search contacts", + "crm.board.searchPlaceholder": "Search name or Threads handle", + "crm.board.stageFilter": "Stage", + "crm.board.allStages": "All stages", + "crm.board.sort": "Sort", + "crm.board.sortRecent": "Recent touch first", + "crm.board.sortIntent": "Intent score first", + "crm.board.clearFilters": "Clear filters", + "crm.board.results": "{n} contacts", + "crm.board.noResults": "No matching contacts", + "crm.board.noResultsHint": "Try another name or handle, or clear the stage filter.", + "crm.board.empty": "No contacts yet", + "crm.board.emptyHint": "Accept an opportunity on the radar to add someone here.", + "crm.board.oppCount": "opportunities", + "crm.board.lastTouch": "Last touch {time}", + "crm.board.noTouch": "No touch time yet", + "crm.board.stage": "Current stage", + "crm.board.conversion": "Conversion", + "crm.board.amount": "Amount (optional)", + "crm.board.reportWon": "Report won", + "crm.board.notes": "Notes", + "crm.board.noteLabel": "Add a note", + "crm.board.addNote": "Save note", + "crm.board.timeline": "Timeline", + "crm.board.timelineEmpty": "No touches yet", + "crm.board.opps": "Related opportunities", + "crm.board.markFollowUp": "Mark follow-up", + "crm.board.clearFollowUp": "Clear follow-up", + "crm.board.msg.stage": "Stage updated", + "crm.board.msg.followUp": "Follow-up updated", + "crm.board.msg.won": "Conversion reported", + "crm.board.msg.note": "Note added", + "crm.board.deleteTitle": "Remove from working list", + "crm.board.deleteHint": "It disappears and reminders stop; opportunity, touch, and conversion history stays.", + "crm.board.delete": "Remove this contact", + "crm.board.deleting": "Removing…", + "crm.board.confirmDelete": "Remove “{name}” from the list? Existing opportunities, touches, and conversion history stay.", + "crm.board.msg.deleted": "Removed from the list", + + "crm.stage.new_found": "New", + "crm.stage.engaged": "Engaged", + "crm.stage.dm_sent": "DM sent", + "crm.stage.replied": "Replied", + "crm.stage.quoted": "Quoted", + "crm.stage.won": "Won", + "crm.stage.lost": "Lost", + "crm.stage.needs_follow_up": "Follow-up", + + "crm.followups.title": "Follow-ups", + "crm.followups.subtitle": "Due visits, snooze, and AI drafts.", + "crm.followups.link.board": "CRM board", + "crm.followups.link.stats": "Conversion stats", + "crm.followups.empty": "Nothing due", + "crm.followups.emptyHint": "Mark a contact for follow-up or send a reply to create one.", + "crm.followups.due": "Due", + "crm.followups.openContact": "Open contact", + "crm.followups.aiMessage": "AI follow-up draft", + "crm.followups.done": "Done", + "crm.followups.snooze": "Snooze 3 days", + "crm.followups.escalatedHint": "Notified twice with no action — consider marking lost.", + "crm.followups.status.scheduled": "Scheduled", + "crm.followups.status.notified": "Notified", + "crm.followups.status.done": "Done", + "crm.followups.status.snoozed": "Snoozed", + "crm.followups.status.escalated": "Escalated", + "crm.followups.msg.done": "Done", + "crm.followups.msg.snoozed": "Snoozed 3 days", + "crm.followups.msg.drafted": "Follow-up draft ready", + + "crm.stats.title": "Conversion stats", + "crm.stats.subtitle": "Terms, reply variants, and sources. No ranking when samples are thin.", + "crm.stats.terms": "Term conversion", + "crm.stats.variants": "Variant success", + "crm.stats.sources": "Win sources", + "crm.stats.emptyDim": "Not enough data yet", + "crm.stats.unavailableDim": "This dimension is not available yet (still being built, not missing data)", + "crm.stats.insufficient": "Insufficient sample", + "crm.stats.col.term": "Term", + "crm.stats.col.variant": "Variant", + "crm.stats.col.source": "Source", + "crm.stats.col.accepted": "Accepted", + "crm.stats.col.replied": "Replied", + "crm.stats.col.won": "Won", + "crm.stats.col.used": "Used", + "crm.stats.col.rate": "Rate", +}; diff --git a/apps/web/src/lib/i18n/catalog.zhTW.ts b/apps/web/src/lib/i18n/catalog.zhTW.ts new file mode 100644 index 0000000..21ee54e --- /dev/null +++ b/apps/web/src/lib/i18n/catalog.zhTW.ts @@ -0,0 +1,2901 @@ +import type { MessageDict } from "./types"; + +export const zhTW: MessageDict = { + "app.name": "巡樓", + "app.nameEn": "Lapras", + "app.nameZh": "巡樓", + "app.tagline": "Threads 海巡好幫手", + "app.taglineEn": "Patrol Threads with ease", + "nav.today": "今日", + "nav.crew": "帳號", + "nav.studio": "創作", + /** 每日自動需求名單(vs 海巡=手動掃一輪) */ + "nav.radar": "商機", + "nav.crm": "名單", + /** 手動掃場外展(vs 商機=訂閱後每天自動) */ + "nav.scout": "話題", + "nav.outbox": "發送", + "nav.jobs": "任務", + "nav.brands": "品牌", + "nav.policy": "商機政策", + "nav.playbooks": "市集", + "nav.insights": "成效", + "nav.benchmark": "基準", + "nav.utm": "追蹤", + "nav.more": "更多", + "nav.moreTitle": "更多功能", + "nav.users": "島民管理", + "nav.usage": "用量與方案", + "nav.profile": "會員資料", + "nav.invite": "邀請關係", + "nav.settings": "系統設定", + "nav.logout": "登出", + "nav.navigate": "導覽", + "navGroup.workflow": "主流程", + "navGroup.accounts": "帳號品牌", + "navGroup.growth": "成長工具", + + "workspace.label": "工作區", + "workspace.default": "預設", + "workspace.new": "+ 新增工作區", + "workspace.newPrompt": "新工作區名稱", + + "common.save": "儲存", + "common.saving": "儲存中…", + "common.cancel": "取消", + "common.close": "關閉", + "common.loading": "載入中…", + "common.retry": "重試", + "common.back": "返回", + "common.delete": "刪除", + "common.edit": "編輯", + "common.search": "搜尋", + "common.confirm": "確定", + "common.optional": "選填", + "common.success": "已儲存", + "common.error": "發生錯誤", + "common.yes": "是", + "common.no": "否", + + // 本頁說明(頂欄抽屜,不進頁面正文) + "help.open": "說明", + "help.kicker": "本頁說明", + "help.section.what": "這頁在做什麼", + "help.section.how": "怎麼用", + "help.section.tips": "小提醒", + "help.section.related": "相關頁面", + "help.shortcutHint": "入口在頁面標題旁的「?」。快捷鍵:? 開關;Esc 關閉。", + + "help.page.generic.title": "巡樓主控台", + "help.page.generic.what": "這是巡樓的工作桌面。左側(或手機底欄)切換功能,頂欄可看用量、通知與本頁說明。", + "help.page.generic.step1": "從側欄選你要做的事(海巡、雷達、發送等)。", + "help.page.generic.step2": "需要幫助時點頂欄「說明」或按 ?。", + "help.page.generic.step3": "設定、帳號與方案在右上角選單。", + "help.page.generic.tips": "說明不會改你的資料,可隨時開關。", + + "help.page.today.title": "今日", + "help.page.today.what": "今日儀表板:一眼看商機、海巡待回、發送與帳號脈動,決定今天先做哪件事。", + "help.page.today.step1": "看「今日商機」摘要,有名單就點進雷達處理。", + "help.page.today.step2": "海巡待回區處理需要回覆的貼文。", + "help.page.today.step3": "發送失敗或進行中的 Outbox 可從這裡追蹤。", + "help.page.today.tips": "沒有商機時摘要會引導你去填商機政策或訂閱關鍵字。", + + "help.page.crew.title": "帳號(Crew)", + "help.page.crew.what": "管理已連結的 Threads 帳號、健康度與可用性,發文/外展都從這裡的帳號出發。", + "help.page.crew.step1": "用 OAuth 連結至少一個可用帳號。", + "help.page.crew.step2": "確認連線狀態與健康度(throttle 時勿硬送)。", + "help.page.crew.step3": "需要時到設定頁調整 AI 與開發選項。", + "help.page.crew.tips": "帳號健康偏黃/限速時,公開留言自動送出會被擋,請改手動。", + + "help.page.studio.title": "創作", + "help.page.studio.what": "寫貼文、做人設、靈感與劇本;完成後送進 Outbox 排程發送。", + "help.page.studio.step1": "選人設或品牌語氣後開始草稿。", + "help.page.studio.step2": "用靈感/仿寫輔助,再人工改到可發。", + "help.page.studio.step3": "送出到發送匣,到 Outbox 確認排程。", + "help.page.studio.tips": "創作頁不直接等同已發佈;真正送出在 Outbox。", + + "help.page.scout.title": "話題靈感", + "help.page.scout.what": "用關鍵字找 Threads 上可跟的活躍話題,產出草稿、回完就結。找「正在找你的人」請用側欄「商機」的每日巡或立即探索。", + "help.page.scout.step1": "寫話題關鍵字,按「產出關鍵字」檢視/增刪 query。", + "help.page.scout.step2": "確認後「用這些詞開始搜」,在佇列依發文時間處理。", + "help.page.scout.step3": "寫草稿、開 Threads 回覆、標記完成。", + "help.page.scout.tips": "話題=內容靈感;商機=找需求客戶。找客戶請用商機頁,不要在這裡掃痛點。", + + "help.page.radar_today.title": "商機巡邏", + "help.page.radar_today.what": "定期或立刻巡邏,找出產品能解決的痛點或新文章。看懂理由後留下或丟掉即可。", + "help.page.radar_today.step1": "看頁頂巡邏狀態:每日定時是否開著、上次何時巡、要不要立即再巡一輪。", + "help.page.radar_today.step2": "讀「為什麼推薦」。對得上就「留下」,不是你的就「丟掉」。", + "help.page.radar_today.step3": "只有真的要追這個人時才「加入名單」。名單不是看結果的必要步驟。", + "help.page.radar_today.tips": "立即巡邏與每日定時可同時開著。關掉其中一個不會藏掉另一個。", + + "help.page.radar_watches.title": "設定巡邏", + "help.page.radar_watches.what": "選定產品與關鍵字後,每日定時巡邏會自動跑;也可隨時按立即巡邏。結果回到側欄「商機」。", + "help.page.radar_watches.step1": "選品牌與產品,補需求地圖裡的痛點。", + "help.page.radar_watches.step2": "填客人會搜的關鍵字與排除詞。", + "help.page.radar_watches.step3": "打開每日定時,或按「立即巡邏」現在跑一輪。", + "help.page.radar_watches.tips": "啟用數有方案上限;滿了要先暫停一組。", + + "help.page.crm_board.title": "名單管理", + "help.page.crm_board.what": "從「今日商機」加入後的聯絡人工作清單,可搜尋、篩選、排序、備註、成交與查看時間軸。", + "help.page.crm_board.step1": "在今日商機按「加入名單」後,人會出現在「新發現」。", + "help.page.crm_board.step2": "點聯絡人推進階段、加備註或標記待追蹤。", + "help.page.crm_board.step3": "成交時用回報成交(金額可留空)。", + "help.page.crm_board.tips": "不再需要的聯絡人可從工作名單移除;既有商機、接觸與成交歷史會保留。", + + "help.page.crm_followups.title": "待追蹤", + "help.page.crm_followups.what": "到期要回訪的清單:完成、延後、或產生 AI 追蹤草稿。", + "help.page.crm_followups.step1": "看到期日與狀態(含需升級處理)。", + "help.page.crm_followups.step2": "需要文案時用 AI 草稿,再手動複製送出。", + "help.page.crm_followups.step3": "做完按完成,或延後 3 天。", + "help.page.crm_followups.tips": "系統不會替你自動傳訊;草稿僅供複製。", + + "help.page.crm_stats.title": "轉換統計", + "help.page.crm_stats.what": "從關鍵字、回覆版本、來源看轉換;樣本不足時不下結論。", + "help.page.crm_stats.step1": "先看各表的絕對數。", + "help.page.crm_stats.step2": "標「樣本不足」的列不要拿來排名。", + "help.page.crm_stats.step3": "回到看板或雷達調整關鍵字與回覆策略。", + "help.page.crm_stats.tips": "樣本少時比率會隱藏,這是刻意設計。", + + "help.page.outbox.title": "發送", + "help.page.outbox.what": "排程與發送佇列:草稿真正送上 Threads 前的最後一站。", + "help.page.outbox.step1": "檢查待發送與失敗項目。", + "help.page.outbox.step2": "失敗可看原因後重試或改草稿。", + "help.page.outbox.step3": "進行中任務也可在任務中心追蹤。", + "help.page.outbox.tips": "帳號健康限速時自動送出會被擋。", + + "help.page.jobs.title": "任務", + "help.page.jobs.what": "背景工作(掃描、巡檢、分析等)的進度與結果。", + "help.page.jobs.step1": "列表看狀態:排隊/執行中/成功/失敗。", + "help.page.jobs.step2": "點進詳情看進度摘要。", + "help.page.jobs.step3": "失敗時依摘要回對應功能重試。", + "help.page.jobs.tips": "雷達「立即巡」會在這裡產生 radar_sweep 任務。", + + "help.page.brands.title": "品牌", + "help.page.brands.what": "集中維護品牌與產品資料,讓商機搜尋知道你在賣什麼、能解決哪些痛點。", + "help.page.brands.step1": "維護品牌與產品基本資料。", + "help.page.brands.step2": "為產品補上情境、痛點、比對標籤與服務能力詞。", + "help.page.brands.step3": "需要價格、地區、禁語、案例與口吻時,到獨立的「商機政策」設定。", + "help.page.brands.tips": "產品資料越具體,搜尋前處理與產品配對越準。", + + "help.page.policy.title": "商機政策", + "help.page.policy.what": "集中設定商機判定與回覆共用的服務政策,不再混在品牌/案例管理裡。", + "help.page.policy.step1": "填服務項目、價格與可服務地區。", + "help.page.policy.step2": "補上禁語、案例、FAQ、可接案時間與口吻。", + "help.page.policy.step3": "儲存後,商機判定、訂閱啟用與回覆生成會共用這份政策。", + "help.page.policy.tips": "Policy 是工作區共用設定;品牌與產品本身請回品牌頁維護。", + + "help.page.playbooks.title": "市集(Playbooks)", + "help.page.playbooks.what": "分享或引用海巡 brief、人設與劇本模板。", + "help.page.playbooks.step1": "瀏覽可用模板。", + "help.page.playbooks.step2": "引用到你的工作區後再編輯。", + "help.page.playbooks.step3": "回到海巡或創作實際使用。", + "help.page.playbooks.tips": "模板是起點,發文前請依你的品牌改寫。", + + "help.page.insights.title": "成效", + "help.page.insights.what": "貼文與互動成效管線,用來複盤什麼內容有效。", + "help.page.insights.step1": "同步或整理近期貼文成效。", + "help.page.insights.step2": "對照高表現內容調整創作。", + "help.page.insights.step3": "需要全站基準可看基準頁。", + "help.page.insights.tips": "數據延遲取決於平台同步,非即時秒級。", + + "help.page.benchmark.title": "基準", + "help.page.benchmark.what": "全站匿名聚合中位數,樣本足夠才顯示,用來對照自己的水準。", + "help.page.benchmark.step1": "查看有樣本的指標。", + "help.page.benchmark.step2": "樣本不足的項目不要過度解讀。", + "help.page.benchmark.step3": "回到成效與創作調整策略。", + "help.page.benchmark.tips": "通常樣本 ≥ 5 才顯示有意義的中位數。", + + "help.page.utm.title": "追蹤(UTM)", + "help.page.utm.what": "建立帶 UTM 的連結,方便之後看流量來源。", + "help.page.utm.step1": "填活動與來源參數。", + "help.page.utm.step2": "產生連結後用於貼文或私訊。", + "help.page.utm.step3": "在分析工具對照成效。", + "help.page.utm.tips": "參數命名保持一致,後續報表才好彙總。", + + "help.page.settings.title": "系統設定", + "help.page.settings.what": "AI 供應商、搜尋金鑰、介面偏好等系統級選項。", + "help.page.settings.step1": "確認 AI 與金鑰(平台或 BYOK)。", + "help.page.settings.step2": "搜尋金鑰可選填自備,或使用平台預設。", + "help.page.settings.step3": "改完後回業務頁驗證。", + "help.page.settings.tips": "錯誤的金鑰會讓生成與掃描失敗,訊息多在用量或任務裡。", + + "help.page.profile.title": "會員資料", + "help.page.profile.what": "你的帳號資料、密碼與基本偏好。", + "help.page.profile.step1": "更新顯示名稱等基本資料。", + "help.page.profile.step2": "需要時修改密碼。", + "help.page.profile.step3": "語言/主題可在介面偏好調整。", + "help.page.profile.tips": "這是會員層設定,與 Threads 連帳不同(連帳在帳號頁)。", + + "help.page.invite.title": "邀請關係", + "help.page.invite.what": "邀請連結、下線關係與獎勵相關資訊。", + "help.page.invite.step1": "複製邀請連結分享。", + "help.page.invite.step2": "查看已建立的邀請關係。", + "help.page.invite.step3": "獎勵規則以產品內標示為準。", + "help.page.invite.tips": "請勿濫發邀請;違規可能被停權。", + + "help.page.usage.title": "用量與方案", + "help.page.usage.what": "看點數、分項用量與目前方案額度。", + "help.page.usage.step1": "對照各 meter 的使用量。", + "help.page.usage.step2": "接近上限時考慮升級或 BYOK。", + "help.page.usage.step3": "方案細節在方案頁。", + "help.page.usage.tips": "BYOK 通常只計呼叫次數,不占平台點數(以頁面標示為準)。", + + "help.page.usage_plans.title": "方案", + "help.page.usage_plans.what": "比較與選擇付費方案。", + "help.page.usage_plans.step1": "比較額度與功能。", + "help.page.usage_plans.step2": "選方案進入結帳。", + "help.page.usage_plans.step3": "完成後回用量頁確認。", + "help.page.usage_plans.tips": "價格與額度以結帳當下標示為準。", + + "help.page.usage_checkout.title": "結帳", + "help.page.usage_checkout.what": "完成方案購買的付款流程。", + "help.page.usage_checkout.step1": "確認方案與金額。", + "help.page.usage_checkout.step2": "依指示完成付款。", + "help.page.usage_checkout.step3": "成功後回用量確認額度。", + "help.page.usage_checkout.tips": "付款異常請保留單號並聯絡支援。", + + "help.page.admin_users.title": "島民管理", + "help.page.admin_users.what": "管理員維護會員帳號、停權與角色。", + "help.page.admin_users.step1": "搜尋或瀏覽會員。", + "help.page.admin_users.step2": "調整狀態或角色(慎用)。", + "help.page.admin_users.step3": "重大操作保留紀錄與原因。", + "help.page.admin_users.tips": "僅管理員可見;誤操作可能影響他人登入。", + + // 後端 envelope code(apps/backend response + middleware) + "api.err.unknown": "操作失敗,請稍後再試", + "api.err.studioValidation": "資料不符合規則,請檢查後再試", + "api.err.crawlerSession": "請先到設定同步 Chrome 工作階段後再試", + "api.err.network": "無法連線後端,請確認網路或服務是否啟動", + "api.err.400001": "請求格式不正確", + // 與後端 400003 / ValidatePasswordPolicy 同一句(勿各寫各的) + "api.err.400003": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + "password.policy.hint": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + "password.policy.minLen": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + "password.policy.needUpper": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + "password.policy.needLower": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + "password.policy.needDigit": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + "password.policy.needSymbol": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + + "api.err.400004": "驗證碼無效或已過期", + "api.err.400020": "不能停權自己的帳號", + "api.err.400021": "不能移除或停權最後一位管理員", + "auth.unreachable": "連不上伺服器,無法確認登入狀態。請檢查網路後重試。", + "api.err.401001": "請先登入", + "api.err.401002": "登入已失效,請重新登入", + "api.err.401003": "找不到會員資料,請重新登入", + "api.err.401010": "Email 或密碼錯誤", + "api.err.403001": "帳號已停權", + "api.err.403002": "需要管理員權限", + "api.err.404001": "找不到資料", + "api.err.404002": "查無此信箱,請確認後再試", + "api.err.409001": "此 Email 已被註冊", + "api.err.402001": "本月平台點數已用完,請升級方案或下月再試", + "usage.err.meterCap": "此功能本月點數已達上限,請改用其他功能或升級方案", + "usage.err.platformCapacity": "平台 AI/搜尋目前忙碌或已限流。可稍後再試,或到設定填入自己的 API Key(BYOK)繼續使用", + "api.err.500000": "伺服器發生錯誤,請稍後再試", + "api.err.timeoutAI": "AI 回應逾時(模型思考太久)。可縮短結構備註後再試,或到設定換較快的模型", + "api.err.501000": "此功能尚未實作", + "api.err.501010": "這個功能還沒開通,之後的版本會補上", + "api.err.400100": "填寫內容不符合規則,請檢查後再送出", + + // 後端 data.message 成功文案 + "api.ok.verifySent": "驗證碼已寄出,請查收信箱", + "api.ok.verifyIssued": "驗證碼已產生", + "api.ok.resetSent": "重設信已寄出,請查收信箱", + "api.ok.passwordUpdated": "密碼已更新", + "api.ok.loggedOut": "已登出", + "api.ok.unbound": "已解除綁定", + + "topbar.notifications": "通知", + "topbar.unread": "{n} 未讀", + "topbar.markAllRead": "全已讀", + "topbar.noNotifications": "目前沒有通知", + "topbar.jobsCenter": "任務中心", + "topbar.moreOlder": "還有 {n} 則較舊", + "topbar.account": "帳號選單", + + "role.admin": "管理員", + "role.member": "一般會員", + "role.verified": "已驗證", + "role.unverified": "未驗證", + + "login.title": "登入巡樓", + "login.email": "Email", + "login.password": "密碼", + "login.showPassword": "顯示密碼", + "login.hidePassword": "隱藏密碼", + "login.submit": "登入", + "login.submitting": "登入中…", + "login.forgot": "忘記密碼?", + "login.mockHint": "管理員 demo@harbor.local / demo · 一般會員 alice@harbor.local / alice", + "login.liveHint": "Live 後端:admin@haixun.local / admin123(gateway :8888)", + + "forgot.title": "忘記密碼", + "forgot.submit": "寄送重設連結", + "forgot.submitting": "送出中…", + "forgot.back": "返回登入", + "forgot.hint": "輸入註冊用 Email,我們會寄送重設連結與驗證碼。", + "forgot.mockMail": "重設信", + "forgot.openReset": "前往輸入驗證碼/重設密碼", + "forgot.retry": "再試一次", + "forgot.nextStep": "請查收信箱中的驗證碼或連結,再到重設頁完成新密碼。", + + "reset.title": "設定新密碼", + "reset.hint": "請輸入信箱、驗證碼(信內 6 碼或連結參數)與新密碼。", + "reset.code": "驗證碼", + "reset.codePh": "信內 6 位數字", + "reset.needEmail": "請輸入 Email", + "reset.needCode": "請輸入驗證碼", + "reset.newPassword": "新密碼", + "reset.confirm": "確認新密碼", + "reset.submit": "更新密碼", + "reset.submitting": "更新中…", + "reset.missingToken": "連結缺少 token。請從「忘記密碼」重新申請。", + + "verify.title": "驗證信箱", + "verify.pending": "尚未驗證", + "verify.body": "帳號已開通登入,但信箱未驗證前無法使用功能。請輸入寄到你信箱的 6 位驗證碼。", + "verify.code": "驗證碼", + "verify.submit": "完成驗證", + "verify.submitting": "驗證中…", + "verify.resend": "重新寄送驗證碼", + "verify.sending": "寄送中…", + "verify.logout": "登出", + "verify.mockMail": "驗證信", + "verify.mockHint": "請輸入驗證碼:", + "verify.after": "驗證完成後即可使用今日、創作、海巡等功能。", + + "settings.title": "設定", + "settings.loadFail": "載入設定失敗,請重新整理再試", + "settings.localeCurrency": "語言與幣別", + "settings.locale": "介面語言", + "settings.currency": "顯示幣別", + "settings.currencyHint": "金額顯示用;方案計費仍以台幣為準。", + "settings.localeSaved": "語言與幣別已更新", + "settings.appearance": "外觀", + "settings.theme": "主題", + "settings.themeLight": "淺色", + "settings.themeDark": "深色", + "settings.themeSystem": "跟隨系統", + "settings.themeHint": "可選淺色、深色,或跟隨系統外觀。", + "settings.themeSaved": "主題已更新", + "settings.themeToLight": "切換成淺色", + "settings.themeToDark": "切換成深色", + "settings.dataSource": "資料來源", + "settings.dataSourceHint": + "業務資料一律走 live API。Mock 僅保留邀請關係等少數本機模組;請維持 Live 並連 gateway。", + "settings.dataSourceMock": "Mock(已縮)", + "settings.dataSourceLive": "Live(後端 API)", + "settings.dataSourceCurrent": "目前", + "settings.dataSourceSwitchedLive": "已切到 Live — 請用後端帳號重新登入(admin@haixun.local / admin123)", + "settings.dataSourceSwitchedMock": "已切回 Mock(邀請外仍打 live)", + "settings.dataSourceLiveNeedGateway": "需 gateway 在 :8888(或 Vite proxy /api)", + "settings.ai": "AI", + "settings.search": "搜尋", + "settings.threads": "Threads 連帳(平台)", + "settings.threadsHint": + "App ID/Secret 在 gateway 平台設定(yaml/env),Secret 不會顯示。請把 Callback 貼到 Meta 後台,並用 https 公開站連帳。", + "settings.threadsProvider": "目前模式", + "settings.threadsProviderFake": "Fake(開發/本機可測 OAuth 流程)", + "settings.threadsProviderMeta": "Meta 正式", + "settings.threadsConfigured": "App 憑證", + "settings.threadsConfiguredYes": "已設定", + "settings.threadsConfiguredNo": "未設定", + "settings.threadsCallback": "OAuth Callback URL(redirect_uri)", + "settings.threadsCallbackHint": "Meta App → Valid OAuth Redirect URIs 請貼這一整行(必須 https)", + "settings.threadsCopy": "複製", + "settings.threadsCopied": "已複製 Callback URL", + "settings.threadsCopyFail": "複製失敗,請手動選取", + "settings.threadsPublicWeb": "公開站 origin", + "settings.threadsGoCrew": "前往 Crew 連帳", + "settings.member": "會員與登入", + "settings.usageCard": "AI/搜尋額度", + "settings.usageCardHint": "平台代付點數與方案上限;自備 Key 不占平台點。", + "settings.viewUsage": "查看用量", + "settings.editProfile": "編輯會員資料", + "settings.mockLogin": "demo@harbor.local / demo", + "settings.memberHint": "登入帳號、顯示名稱與通知偏好。", + + "usage.title": "用量與方案", + "usage.desc": "查看本月平台點數、分項用量,並變更方案。", + "usage.creditsUsed": "本月已用點數", + "usage.remaining": "剩餘 {n} 點", + "usage.percentUsed": "{n}% 已使用", + "usage.breakdown": "分項用量", + "usage.plans": "方案", + "usage.current": "目前", + "usage.inUse": "使用中", + "usage.switchMock": "切換方案", + "usage.perMonth": "點/月", + "usage.ledger": "最近使用紀錄", + "usage.emptyTitle": "本月還沒有扣點", + "usage.emptyDesc": "開始創作、海巡或生圖後,這裡會出現扣點紀錄。", + "usage.planNote": "自然月重置;未用完點數不累積至下月。", + "usage.switched": "已切換為 {name}", + "usage.meter.times": "{count} 次 · {credits} 點", + "usage.meter.timesShort": "{n} 次", + "usage.meter.pt": "點", + "usage.meter.over": "已超出", + "usage.meter.locked": "已鎖", + "usage.meter.cap": "單項上限 {credits}/{cap} 點", + "usage.side.aiCredits": "AI 相關已用點數(文案+研究+生圖)", + "usage.side.searchCredits": "搜尋已用點數", + "usage.byok.title": "自備 Key 用量", + "usage.byok.hint": "只計呼叫次數,不占平台點數與分項進度。", + "usage.plan.free.blurb": "夠用試用,體驗完整創作流程", + "usage.plan.starter.blurb": "小團隊日常發文與海巡", + "usage.plan.pro.blurb": "多帳、重度 AI 與研究", + + "profile.title": "會員資料", + "profile.desc": "管理顯示名稱、頭像與密碼。", + "profile.accountStatus": "帳號開通狀態", + "profile.basic": "基本資料", + "profile.avatar": "頭像", + "profile.avatarUpload": "上傳頭像", + "profile.avatarRemove": "移除頭像", + "profile.avatarHint": "JPG/PNG/WebP,5MB 內。選圖後按「儲存基本資料」寫入;「移除頭像」會立刻清除。", + "profile.avatarSaved": "頭像已更新", + "profile.avatarCleared": "已移除頭像", + "profile.avatarFail": "無法讀取圖片", + "profile.displayName": "顯示名稱", + "profile.bio": "簡介(選填)", + "profile.timezone": "時區", + "profile.notifyEmail": "Email 通知", + "profile.password": "變更密碼(選填)", + "profile.currentPassword": "目前密碼", + "profile.newPassword": "新密碼", + "profile.confirmPassword": "確認新密碼", + "profile.emailVerified": "信箱已驗證", + "profile.emailUnverified": "信箱未驗證", + "profile.roleAdmin": "管理員", + "profile.roleMember": "一般會員", + "profile.loginEmail": "登入信箱:{email}", + "profile.verifiedAt": " · 驗證於 {time}", + "profile.goVerify": "去驗證信箱", + "profile.roleTags": "權限標籤:{labels}", + "profile.roleNote": "角色由系統指派;改信箱後需重新驗證。", + "profile.saveBasic": "儲存基本資料", + "profile.updatePassword": "更新密碼", + "profile.saved": "會員資料已儲存", + "profile.savedUnverified": "資料已儲存。信箱尚未驗證或已變更,請完成驗證後才能繼續使用功能。", + "profile.saveFail": "儲存失敗", + "profile.needNewPassword": "請輸入新密碼", + "profile.passwordMismatch": "兩次新密碼不一致", + "profile.needCurrentPassword": "請輸入目前密碼", + "profile.wrongCurrentPassword": "目前密碼不正確", + "profile.passwordUpdated": "密碼已更新", + "profile.passwordFail": "變更密碼失敗", + "profile.listJoin": "、", + "profile.tenantUid": " · tenant {tenant} · uid {uid}", + "profile.inviteBadge": "邀請碼", + "profile.inviteHint": "分享給朋友加入;日後活動可依邀請關係計算。", + "profile.gotoInvite": "查看邀請關係", + + "invite.title": "邀請關係", + "invite.desc": "邀請碼與邀請關係;管理端為樹狀結構。", + "invite.tabs": "邀請檢視", + "invite.tab.mine": "我的邀請", + "invite.tab.tree": "關係樹", + "invite.myCode": "我的邀請碼", + "invite.codeLabel": "邀請碼", + "invite.copyCode": "複製", + "invite.copied": "已複製", + "invite.copyFail": "複製失敗", + "invite.stats": "直邀 {direct} · 延伸 {total}", + "invite.rewards.summary": "邀請贈點累計 {total} · 本月 {month} /上限 {cap}", + "invite.rewards.title": "邀請回饋紀錄", + "invite.upline": "邀請人", + "invite.downlines": "直邀成員 · {n}", + "invite.noUpline": "無邀請人", + "invite.noDownline": "尚無直邀", + "invite.directN": "直邀 {n}", + "invite.claimHint": "若註冊時沒填,可在此補上邀請人的邀請碼(綁定後不可自行更改)。", + "invite.claimLabel": "邀請碼", + "invite.claimPh": "例如 HX-DEMO01", + "invite.claimSubmit": "確認綁定", + "invite.claimOk": "已綁定邀請人:{name}", + "invite.claimOkGeneric": "已綁定邀請人", + "invite.claimFail": "綁定失敗", + "invite.claimLocked": "已綁定,如需調整請聯絡管理員。", + "invite.loadFail": "載入失敗", + "invite.treeEmpty": "尚無資料", + "invite.treeSearch": "搜尋", + "invite.treeSearchPh": "名稱 / Email / 邀請碼", + "invite.treeCount": "{n} 人", + "invite.treeMatchCount": "命中 {n} · 共 {total}", + "invite.treeNoMatch": "沒有符合的島民", + "invite.treeNoMatchHint": "換個關鍵字試試", + "invite.clearSearch": "清除", + "invite.moveTitle": "調整歸屬", + "invite.newParent": "邀請人", + "invite.root": "(無 · 獨立)", + "invite.confirmMove": "確認", + "invite.moved": "已調整「{name}」→「{parent}」", + "invite.moveFail": "調整失敗", + "invite.openFromAdmin": "邀請關係", + "invite.parentField": "邀請人", + "invite.orgPickRoot": "選一個起點(含無人邀請、自己進來的)", + "invite.orgRoots": "起點", + "invite.orgPath": "路徑", + "invite.orgChainN": "延伸 {n}", + "invite.orgFocusStats": "直邀 {direct} · 延伸 {total}", + "invite.orgDirects": "直邀成員 · {n}", + "invite.orgNoDirects": "沒有直邀成員", + "invite.err.notFound": "找不到會員", + "invite.err.notLoggedIn": "尚未登入", + "invite.err.needAdmin": "需要管理員權限", + "invite.err.memberNotFound": "找不到島民", + "invite.err.selfParent": "不能把自己設為邀請人", + "invite.err.parentNotFound": "找不到指定的邀請人", + "invite.err.cycle": "不可掛到自己邀請鏈的下層(會形成環)", + "invite.err.repoMissing": "邀請資料層尚未載入,請重新整理頁面", + "invite.err.alreadyBound": "你已有邀請人,無法再補填", + "invite.err.codeRequired": "請輸入邀請碼", + "invite.err.codeNotFound": "找不到此邀請碼", + "invite.err.codeSelf": "不能填自己的邀請碼", + + "admin.users.title": "島民管理", + "admin.users.desc": "管理島民帳號、權限、方案與不擋額度。", + "admin.users.needAdmin": "需要管理員權限", + "admin.users.list": "島民列表 · {n}", + "admin.users.detail": "島民詳情", + "admin.users.pick": "選擇島民", + "admin.users.search": "搜尋名稱 / uid", + "admin.users.searchPh": "島民名稱、Email 或 uid", + "admin.users.create": "新增島民", + "admin.users.suspend": "停權", + "admin.users.unsuspend": "復權", + "admin.users.suspended": "已停權", + "admin.users.active": "正常", + "admin.users.onboarding": "第一次引導", + + "crew.title": "帳號", + "crew.tab.accounts": "帳號", + "crew.tab.personas": "人設", + "crew.connect": "連接帳號", + "crew.connecting": "連線中…", + "crew.tokenRenewHint": "Token 由背景任務自動延長(約第 30 天),可在「任務」查看;無需手動刷新。", + "crew.empty": "尚無帳號", + "crew.loadFail": "載入帳號失敗", + "crew.unusable": "不可用", + "crew.expires": "到期 {time}", + "crew.lastRefresh": "上次延長 {time}", + "crew.refreshSession": "延長 token", + "crew.refreshSessionHint": "用已存授權換新 token,不開授權頁;失敗請「連接帳號」重授權", + "crew.session.ok": "token 有效", + "crew.session.soon": "即將到期", + "crew.session.expired": "token 已過期", + "crew.session.unknown": "未記錄到期", + "crew.connection.connected": "已連線", + "crew.connection.error": "異常", + "crew.connection.disconnected": "已斷開", + "crew.connection.unknown": "未知", + "crew.health.needsReconnect": "需重新連帳", + "crew.health.needsReconnectHint": "token 無法使用,請按「連接帳號」重新授權(不是延長 token)", + "crew.health.disconnectedHint": "已解除綁定", + "crew.health.expiredHint": "請延長 token 或重新連帳", + "crew.opHealthScore": "操作 {n}", + "crew.msg.refreshed": "@{user} 已用現有授權延長 token(約 60 天)", + "crew.msg.refreshedAll": "已用現有授權延長 {n} 個帳號 token", + "crew.msg.refreshFail": "延長失敗(token 失效時請重新「連接帳號」)", + "crew.msg.oauthOk": "Threads 帳號已連線;已排程約 30 天後自動延長 token(見任務)", + "crew.msg.oauthFail": "OAuth 連線失敗", + "crew.msg.oauthUrlFail": "無法取得授權網址,請稍後再試或檢查平台 Threads 設定", + "crew.msg.deleted": "已移除 @{user}", + "crew.confirmDelete": "確定刪除帳號 @{user}?\n刪除後無法再選為 lead / cast。", + + "today.findTopic": "找話題", + "today.refreshTopics": "刷新話題", + "today.reload": "重新整理", + "today.loadFail": "無法載入今日資料", + "today.trendsFail": "無法刷新話題(可能額度不足或搜尋未設定)", + "today.trendsUpdated": "已更新 {n} 個話題", + "today.syncPosts": "同步已發文", + "today.syncPostsFail": "同步貼文失敗", + "today.needAccount": "請先連接 Threads 帳號", + "today.outcome.title": "本週成果", + "today.outcome.reach": "觸達", + "today.outcome.conversations": "對話", + "today.outcome.follows": "追蹤", + "today.outcome.followsHint": "可能相關,尚無強訊號", + "today.outcome.followsConfirmedHint": "/確定 {n}", + "today.outcome.conversions": "成交", + "today.outcome.emptyHint": "本週還沒有海巡外展成果,去海巡試試身手?", + "today.checkup.empty": "尚未產生本週健檢,將於下週一(依你的時區)自動產生。", + "today.checkup.prefix": "健檢:", + "today.pendingReplies": "待回覆", + "today.pendingRepliesN": "待回覆 · {n}", + "today.newThread": "寫一則", + "today.metricsAria": "今日數值", + "today.metric.pending": "待回", + "today.metric.pendingHint": "海巡佇列", + "today.metric.doneGoal": "已回/目標", + "today.metric.doneGoalHint": "今日海巡完成數/目標(海巡標記已發會累加)", + "today.metric.sentToday": "今日已發", + "today.metric.running": "進行中 {n}", + "today.metric.sentDone": "完成的發送", + "today.metric.failed": "發送異常", + "today.metric.needAction": "需處理", + "today.metric.ok": "正常", + "today.metric.mentions": "提及", + "today.metric.mentionsHint": "待回提及", + "today.pending.title": "海巡待回 · {n}", + "today.pending.empty": "目前沒有待回,去海巡掃一輪", + "today.goScout": "去海巡", + "today.pending.more": "還有 {n} 則 →", + "today.pending.handle": "海巡處理", + "today.pending.start": "開始處理", + "today.topics.title": "找話題", + "today.topics.empty": "還沒有話題,按「刷新話題」抓一輪", + "today.goStudio": "去靈感", + "today.heat": "熱度 {n}", + "today.topicAngle": "可當開場角度", + "today.moreInspire": "更多靈感", + "today.useTopic": "用話題發想", + "today.outbox.title": "今日發送", + "today.outbox.empty": "還沒有今日發送。可先", + "today.outbox.emptyMid": ",完成後會出現在", + "today.outbox.emptyEnd": "。", + "today.outbox.summary": "已完成 {sent} · 進行中 {running} · 異常 {failed}", + "today.badge.failed": "失敗", + "today.badge.scheduling": "排程", + "today.badge.sending": "發送中", + "today.badge.drafted": "已草稿", + "today.openOutbox": "開啟發送", + "today.accounts.title": "帳號成效", + "today.accounts.empty": "尚無成效資料,可先同步已發文", + "today.postsCount": "{n} 則貼文", + "today.views": "瀏覽", + "today.likes": "讚", + "today.repliesShort": "回", + "today.fullInsights": "完整成效 · 月比圖", + "today.viewPosts": "看已發文", + "today.manageAccounts": "管理帳號", + + + "outbox.title": "發送", + "outbox.tabsAria": "發送分頁", + "outbox.tab.active": "進行中", + "outbox.tab.history": "歷史", + "outbox.empty": "尚無發送", + "outbox.activeEmpty": "進行中是空的", + "outbox.historyEmpty": "尚無歷史", + "outbox.historyN": "歷史({n})", + "outbox.backActive": "回進行中({n})", + "outbox.progress": "進度 {progress}", + "outbox.detail": "詳情", + "outbox.deleting": "刪除中…", + "outbox.confirmDelete": "刪除發送項目「{title}」?\n無法復原。", + "outbox.deleted": "已刪除「{title}」", + "outbox.deleteFail": "刪除失敗", + "outbox.loadFail": "載入發送列表失敗", + "outbox.status.scheduling": "排程中", + "outbox.status.active": "發送中", + "outbox.status.completed": "已完成", + "outbox.status.partial_failed": "部分失敗", + "outbox.status.cancelled": "已取消", + "outbox.detail.missingId": "缺少 id", + "outbox.detail.notFound": "找不到發送", + "outbox.detail.loadFail": "載入發送內容失敗", + "outbox.detail.loading": "載入中…", + "outbox.detail.sendingHint": "正在發到 Threads(通常 10~30 秒),頁面會自動更新…", + "outbox.detail.doneHint": "已成功發到 Threads。", + "outbox.detail.markAllOk": "標記全部成功", + "outbox.detail.markRootFail": "標記主貼失敗", + "outbox.detail.processing": "處理中…", + "outbox.detail.delete": "刪除這筆", + "outbox.detail.back": "返回列表", + "outbox.detail.root": "主貼", + "outbox.detail.replyN": "回覆 {n}", + "outbox.detail.retry": "重試", + "outbox.detail.opFail": "操作失敗", + "outbox.step.published": "已發佈", + "outbox.step.failed": "失敗", + "outbox.step.publishing": "發佈中…", + "outbox.step.scheduled": "排程中", + "outbox.step.blocked": "已阻擋", + + "studio.title": "創作", + "studio.account": "帳號", + "studio.persona": "人設", + "studio.personaReady": "人設 ready", + "studio.personaNotReady": "人設未就緒", + "studio.tab.posts": "我的貼文", + "studio.tab.mentions": "提及 @", + "studio.tab.compose": "寫一則", + "studio.tab.plays": "互回方案", + "studio.tab.inspire": "靈感", + "studio.tab.insights": "成效", + + "mentions.hint": "誰 @ 你。待回 {n} 則。每則可改帳號/人設(預設用頂部)。", + "mentions.scoutLink": "海巡外展", + "mentions.empty": "尚無提及", + "mentions.emptyHint": "按「從 Threads 同步」拉取別人 @ 你的貼文/回覆/引用。若權限不足請到設定重新連帳。", + "mentions.needAccount": "請先在頂部選 Threads 帳號", + "mentions.sync": "從 Threads 同步", + "mentions.syncing": "同步中…", + "mentions.syncDone": "已同步 {n} 則提及", + "mentions.syncFail": "同步失敗:請確認已連帳,且 OAuth 含 threads_manage_mentions(可能需重新連帳)", + "mentions.openThread": "開原文", + "mentions.status.pending": "待回", + "mentions.status.replied": "已回", + "mentions.status.skipped": "略過", + "mentions.reply": "回覆", + "mentions.skip": "略過", + "mentions.draftLabel": "回覆草稿", + "mentions.repliedPrefix": "已回:{text}", + "mentions.needPersona": "請選 ready 人設再 AI 產文", + "mentions.fail": "失敗", + "mentions.marked": "已將這則提及標記為已回覆", + "mentions.markReplied": "標記為已回覆", + "mentions.markingReplied": "標記中…", + "mentions.withImages": " · 附圖 {n}", + + "compose.hint": "純發文:寫正文後送出 Outbox(非串場)。互回請用", + "compose.hintEnd": "。", + "compose.playsLink": "串場", + "compose.personaOff": "人設未 ready:仿寫/分析等 AI 工具停用。", + "compose.title": "標題(選填)", + "compose.titlePh": "方便在 Outbox 辨識", + "compose.body": "正文", + "compose.bodyPh": "寫下這則貼文…", + "compose.bodyCount": "{n} 字", + "compose.bodyLongWarning": "完整草稿已保留,但可能超過 Threads 單則可發布長度", + "compose.topicTag": "話題標籤(Threads tag)", + "compose.topicTagPh": "例如 寵物展(可不加 #)", + "compose.topicTagHint": "Threads 話題標籤,每則最多一個;1~50 字,勿含 . 或 &。也可在正文寫 #標籤。", + "compose.whoCanReplyHint": "發文時寫入 Threads。已發布的貼文無法再用 API 修改。", + "compose.tool.mimic": "仿寫", + "compose.tool.viral": "爆紅分析", + "compose.tool.research": "上網補資料", + "compose.tool.image": "產圖", + "compose.mimic.title": "仿寫別人貼文", + "compose.mimic.source": "參考全文", + "compose.mimic.sourcePh": "貼上想仿寫的貼文…", + "compose.mimic.direction": "新主題/新角度(可留空)", + "compose.mimic.directionPh": "例如:改寫成『功能越少,產品反而越好用』的觀點…", + "compose.mimic.directionHint": "這是新貼文真正要談的內容。留空時 AI 會從參考文延伸不同角度,不會照原文換句話說。", + "compose.mimic.structureNotes": "結構分析(會帶進仿寫)", + "compose.mimic.structureNotesPh": "可從「我的貼文 → 分析結構」帶入;或手動貼鉤子/結構/可複製點…", + "compose.mimic.structureNotesHint": "只借用敘事骨架、轉折與情緒曲線;內容依新方向重寫,語氣使用目前選擇的人設。", + "compose.mimic.broughtAnalysis": "已帶入參考貼文 + 結構分析,可直接仿寫或再改備註", + "compose.mimic.broughtSource": "已帶入參考貼文(尚未有結構分析;可先回我的貼文按「分析結構」)", + "compose.mimic.running": "仿寫中(背景任務,可離開本頁)…", + "compose.mimic.run": "依人設仿寫到正文", + "compose.mimic.queued": "已排程仿寫任務,完成後會自動填入正文", + "compose.mimic.done": "仿寫完成,可再改", + "compose.mimic.doneWithStructure": "仿寫完成(已套用結構分析骨架),可再改", + "compose.mimic.jobFail": "仿寫任務失敗:{err}", + "compose.viral.title": "爆紅分析", + "compose.viral.hint": "分析參考文或目前正文的鉤子/結構/可複製點。", + "compose.viral.source": "分析對象(可空=用正文)", + "compose.viral.running": "分析中…", + "compose.viral.run": "開始分析", + "compose.viral.result": "分析結果", + "compose.viral.done": "爆紅分析完成", + "compose.viral.needText": "請貼參考文或先寫正文", + "compose.research.title": "上網補專業資料", + "compose.research.q": "關鍵字", + "compose.research.qPh": "例如:無香洗劑 敏感肌", + "compose.research.running": "搜尋中…", + "compose.research.insert": "插入勾選內容到正文", + "compose.research.inserted": "已插入 {n} 條補充", + "compose.image.title": "產圖", + "compose.image.prompt": "畫面描述", + "compose.image.promptPh": "可空=從正文摘要", + "compose.image.running": "產圖中…", + "compose.image.run": "產生圖片", + "compose.image.done": "已產圖", + "compose.scheduleAt": "預計發送時間", + "compose.scheduleHint": "到點後由 Outbox 依序送出;不可早於現在。", + "compose.scheduleHintNow": "立即發送(到點=現在,送出時以當下為準)。", + "compose.schedulePast": "預計發送時間已過期,請改為現在或未來時間。", + "compose.scheduleNow": "設為現在", + "compose.publish": "送出到 Outbox", + "compose.publishing": "送出中…", + "compose.uploadingImages": "上傳圖片 {n}/{total}…", + "compose.uploadImageFail": "圖片上傳失敗,請重試或換較小的圖(≤5MB)", + "compose.waitImageUpload": "圖片還在上傳,請稍候再送出。", + "compose.waitImageUploadBtn": "圖片上傳中…", + "compose.imageUploadNeedRetry": "有圖片上傳失敗,請點縮圖上的重試後再送出。", + "compose.publishFail": "送出失敗", + "compose.fail": "失敗", + "compose.attachN": "附圖 {n}", + "compose.personaStatus": "人設:{status}", + "compose.ready": "ready", + "compose.notReady": "未就緒", + + "posts.sync": "重新同步 Threads", + "posts.syncing": "同步中…", + "posts.syncedAt": "同步 {time}", + "posts.notSynced": "未同步", + "posts.syncDone": "已從 Threads 同步 {n} 則貼文(含成效與留言)", + "posts.syncFail": "同步失敗,請確認已連帳且權限足夠(可能需重新 OAuth)", + "posts.loadingReplies": "載入留言中…", + "posts.loadRepliesFail": "載入留言失敗", + "posts.empty": "尚無貼文", + "posts.openThreads": "開 Threads", + "posts.whoCanReply": "誰可以回覆", + "posts.replyControl.everyone": "所有人", + "posts.replyControl.accounts_you_follow": "你追蹤的帳號", + "posts.replyControl.mentioned_only": "僅被提及的人", + "posts.replyControl.parent_post_author_only": "僅原po", + "posts.replyControl.followers_only": "僅追蹤者", + "posts.replyControlUpdated": "已更新為「{label}」。請到 Threads 確認。", + "posts.replyControlFail": "更新誰可以回覆失敗", + "posts.replyControlPublishOnly": "Threads 只能在發文時設定誰可以回覆。已發布貼文無法用 API 修改,請到創作頁發一則新貼文。", + "posts.hideReply": "隱藏回覆", + "posts.unhideReply": "取消隱藏", + "posts.hidingReply": "處理中…", + "posts.replyHidden": "已在 Threads 隱藏這則回覆。請到 Threads 確認。", + "posts.replyUnhidden": "已在 Threads 取消隱藏。請到 Threads 確認。", + "posts.hideFail": "隱藏/取消隱藏失敗", + "posts.hiddenBadge": "已隱藏", + "posts.insight": "分析洞察:{text}", + "posts.review": "覆盤:{text}", + "posts.formulaResult": "結構分析結果", + "posts.analyzedBadge": "已分析", + "posts.noText": "(此則無文字/純媒體)", + "posts.collapseReplies": "收合留言", + "posts.repliesBtn": "留言({total})· 未回 {pending}", + "posts.replyRoot": "回主貼", + "posts.analyzing": "分析中…", + "posts.reanalyze": "重新分析結構", + "posts.analyze": "分析結構", + "posts.mimicThis": "仿寫這則", + "posts.rootDraft": "回主貼草稿", + "posts.filter.pending": "未回覆({n})", + "posts.filter.replied": "已回覆({n})", + "posts.filter.all": "全部({n})", + "posts.noPending": "沒有未回覆留言", + "posts.noReplied": "還沒有已回覆留言", + "posts.noReplies": "尚無留言", + "posts.status.pending": "未回覆", + "posts.status.replied": "已回覆", + "posts.likesN": "讚 {n}", + "posts.childCount": "{n} 則子留言", + "posts.mine": "我方", + "posts.replyThis": "回這則", + "posts.replyAgain": "再回一則", + "posts.replyTo": "回 @{user}", + "posts.replyAgainTo": "再回 @{user}", + "posts.needPersona": "請先選 ready 人設再 AI 產文", + "posts.genFail": "生成失敗", + "posts.needText": "請先產生或輸入回覆", + "posts.needAccount": "請選擇要送出的 Threads 帳號", + "posts.sending": "送出到 Threads 中…", + "posts.sent": "已用 @{user} 發到 Threads", + "posts.sentImages": "(附圖 {n})", + "posts.accountFallback": "帳號", + "posts.sendFail": "發送失敗", + "posts.analyzeDone": "結構分析完成(手動觸發)", + "posts.analyzeFail": "分析失敗", + + "wizard.newTitle": "編互回劇本", + "wizard.editTitle": "編輯互回劇本", + "wizard.prev": "上一步", + "wizard.next": "下一步", + "wizard.err.topic": "請填主題", + "wizard.err.lead": "請選擇主帳號", + "wizard.err.leadUnusable": "主帳號不可用", + "wizard.submitFail": "提交失敗", + "wizard.unnamedPlay": "未命名串場", + "wizard.step.topic": "聊什麼", + "wizard.step.crew": "誰出場", + "wizard.step.script": "誰說什麼", + "wizard.step.preview": "預覽", + "wizard.step.schedule": "何時發", + "wizard.step.submit": "送出", + "wizard.stepperAria": "wizard 步驟", + "wizard.topic.title": "1. 這串要聊什麼", + "wizard.topic.name": "標題(可選)", + "wizard.topic.namePh": "例如:週末咖啡", + "wizard.topic.topic": "主題一句話", + "wizard.topic.topicPh": "這串文想聊什麼?", + "wizard.topic.aiView": "AI 視角", + "wizard.topic.personaNotReady": "人設未就緒", + "wizard.topic.quickFill": "快速填入", + "wizard.topic.sampleTitle": "週末咖啡話題", + "wizard.topic.sampleTopic": "週末想找間不踩雷的咖啡店,插座要多、能坐久。", + "wizard.crew.title": "2. 出場", + "wizard.crew.lead": "主帳", + "wizard.crew.noUsable": "尚無可用帳號", + "wizard.crew.unusable": "不可用", + "wizard.crew.cast": "配角", + "wizard.script.title": "3. 台詞", + "wizard.script.persona": "人設", + "wizard.script.personaNotReady": "人設未就緒", + "wizard.script.root": "主貼", + "wizard.script.replyN": "回覆 {n}", + "wizard.script.generating": "生成中…", + "wizard.script.ai": "AI 產文", + "wizard.script.account": "帳號:{name}", + "wizard.script.noLead": "(未選 lead)", + "wizard.script.speaker": "發言帳號", + "wizard.script.leadTag": "(lead)", + "wizard.script.text": "文案", + "wizard.script.rootPh": "主貼內容…", + "wizard.script.replyPh": "回覆內容…", + "wizard.script.addReply": "新增回覆步驟", + "wizard.preview.title": "4. 預覽這段對話", + "wizard.preview.unknown": "未知", + "wizard.preview.unknownAccount": "未知帳號", + "wizard.preview.root": "主貼", + "wizard.preview.leadTalk": "lead 接話", + "wizard.preview.empty": "(空白)", + "wizard.schedule.title": "5. 何時發出去", + "wizard.schedule.start": "第一則(主貼)時間", + "wizard.schedule.interval": "回覆間隔(分鐘)", + "wizard.schedule.intervalHint": "相對上一步;送出後後端會加隨機抖動,避免節奏太精準", + "wizard.submit.title": "6. 送出排程", + "wizard.submit.body": "確認後會把「{title}」這串互回(共 {n} 步)送進 Outbox,依序用各帳號發出。", + "wizard.submit.unnamed": "未命名", + "wizard.submit.root": "主貼", + "wizard.submit.replyN": "回覆 {n}", + "wizard.submit.submitting": "提交中…", + "wizard.submit.run": "提交到 Outbox", + + "reply.account": "用哪個帳號回", + "reply.persona": "用人設", + "reply.notReady": "此人設未 ready,無法 AI 產文(仍可手打後發送)。", + "reply.draft": "回覆草稿", + "reply.attach": "附圖", + "reply.generating": "生成中…", + "reply.ai": "AI 產文", + "reply.sending": "發送中…", + "reply.send": "發送", + + "image.attach": "附圖", + "image.attachFail": "附圖失敗", + "image.attachedAria": "已附圖片", + "image.alt": "附圖", + "image.named": "附圖 {n}", + "image.remove": "移除圖片", + "image.full": "已滿 {max} 張", + "image.more": "再附圖({n}/{max})", + "image.uploading": "上傳中", + "image.uploadingN": "正在上傳 {n} 張圖…", + "image.uploadFail": "上傳失敗", + "image.uploadBadUrl": "上傳回應無效", + "image.retry": "重試", + + "metrics.aria": "貼文成效", + "metrics.like": "讚", + "metrics.reply": "回覆", + "metrics.repost": "轉發", + "metrics.quote": "引用", + "metrics.view": "瀏覽", + "metrics.share": "分享", + "metrics.type.quote": "引用貼", + "metrics.type.reply": "回覆貼", + "metrics.type.image": "圖片", + "metrics.type.video": "影片", + "metrics.type.carousel": "輪播", + "metrics.type.repost": "轉發", + "metrics.type.text": "文字", + "metrics.type.post": "貼文", + + "jobs.title": "任務", + "jobs.desc": "背景任務分三區:執行中、定期排程、歷史。每頁可調筆數,不會一次拉完全部。", + "jobs.startDemo": "產生測試任務", + "jobs.demoHint": "需 worker 執行;狀態會自動輪詢更新。", + "jobs.demoLabel": "Demo 測試任務", + "jobs.demoCreated": "已建立測試任務 {id}…,等待 worker 領取", + "jobs.demoFail": "無法建立測試任務", + "jobs.template.tokenRenew": "Threads Token 定期延長(約每 30 天)", + "jobs.template.tokenRenewCadence": "約每 30 天自動執行一次 · 無需手動操作", + "jobs.template.tokenRenewBadge": "定期 · 每 30 天", + "jobs.template.personaAnalyzeAccount": "人設分析 · 公開貼文", + "jobs.template.personaAnalyzeText": "人設分析 · 文字來源", + "jobs.template.composeMimic": "仿寫貼文", + "jobs.template.playGenerateScript": "劇本一次產全文", + "jobs.template.radarSweep": "商機巡邏", + "jobs.template.unknown": "其他任務", + "jobs.stripMore": "還有 {n} 個進行中…", + "jobs.nextRun": "下次執行:{time}", + "jobs.status.pending": "待處理", + "jobs.status.queued": "已排程", + "jobs.status.running": "執行中", + "jobs.status.succeeded": "已完成", + "jobs.status.failed": "失敗", + "jobs.status.cancelled": "已取消", + "jobs.status.cancel_requested": "取消中", + "jobs.loadFail": "無法載入任務列表", + "jobs.empty": "尚無任務", + "jobs.empty.active": "目前沒有執行中或待領取的任務", + "jobs.empty.recurring": "尚無定期/遠期排程任務", + "jobs.empty.history": "尚無歷史紀錄(已完成/失敗/取消)", + "jobs.tabsAria": "任務分類", + "jobs.tab.active": "執行中", + "jobs.tab.recurring": "定期任務", + "jobs.tab.history": "歷史已執行", + "jobs.recurringHint": "尚未到期的排程(例如 Token 約 30 天延長)。到期後會出現在「執行中」。", + "jobs.total": "共 {n} 筆", + "jobs.showing": " · 顯示前 {n}", + "jobs.detail": "詳情", + "jobs.loadMore": "載入更多(還有 {n})", + "jobs.notFound": "找不到任務", + "jobs.detailLoadFail": "載入任務失敗", + "jobs.progress": "進度 {n}%", + "jobs.updated": "更新 {time}", + "jobs.backList": "返回列表", + "jobs.backCompose": "回寫一則", + "jobs.mimicApplyCompose": "套用到寫一則", + "jobs.mimicApplyHint": "仿寫已完成。按下方按鈕回到單篇發文,正文會自動帶入。", + "jobs.mimicNoResult": "找不到仿寫結果,請再跑一次仿寫", + "jobs.mimicApplyFail": "套用失敗", + "jobs.delete": "刪除", + "jobs.deleteConfirm": "確定刪除「{name}」?此操作無法復原。", + "jobs.deleted": "已刪除任務", + "jobs.deleteFail": "無法刪除任務", + "jobs.deleteRunningHint": "執行中的任務無法刪除,請稍候完成。", + "jobs.retentionHint": "終態任務約保留 2 天後自動清除", + + "plans.title": "變更方案", + "plans.current": "目前方案", + "plans.perMonth": "/月", + "plans.monthlyCredits": "每月 {n} 點", + "plans.usageLink": "用量", + "plans.inUse": "使用中", + "plans.recommended": "推薦", + "plans.creditsPerMonth": "每月 {n} 點", + "plans.manage": "管理訂閱", + "plans.loadFail": "無法載入訂閱方案", + + "plan.cta.current": "目前方案", + "plan.cta.upgrade": "升級", + "plan.cta.downgrade": "降級", + "plan.cta.switch": "切換", + + "plan.free.headline": "夠用試用,體驗完整流程", + "plan.free.bullet1": "每月 {n} 點(約 2~3 週輕度日常)", + "plan.free.bullet2": "完整功能:創作、海巡、發送、生圖", + "plan.free.bullet3": "用得出價值再升級 Starter", + "plan.free.bullet4": "平台忙碌時可改填自己的 Key", + "plan.free.right1": "可使用完整功能:帳號、創作、海巡、發送、任務與靈感。", + "plan.free.right2": "點數夠你真實試跑文案、搜尋與幾次生圖,不是空殼 demo。", + "plan.free.right3": "達上限後升級 Starter 繼續;或設定自備 Key(BYOK)不占平台點。", + "plan.free.quota1": "每月配給 {n} 點。", + "plan.free.note1": "Free 使用者無需付款;付費使用者請由帳務入口管理取消。", + "plan.free.note2": "取消付費訂閱後,方案依帳務入口顯示的日期切換。", + + "plan.starter.headline": "小團隊日常發文與海巡", + "plan.starter.bullet1": "每月 {n} 點(約 5× Free)", + "plan.starter.bullet2": "穩定發文、回覆、海巡", + "plan.starter.bullet3": "適合 1~3 人節奏 · 付費主力", + "plan.starter.bullet4": "付款成功立即生效", + "plan.starter.right1": "付款成功後本帳改為 Starter,當月依新額度計算。", + "plan.starter.right2": "點數支撐固定發文、回覆草稿與定期海巡。", + "plan.starter.right3": "功能與 Free 相同,差在能用多久;日常節奏建議由此開始。", + "plan.starter.quota1": "每月 {n} 點 · {price}。", + "plan.starter.note1": "需付款成功才變更方案。", + "plan.starter.note2": "自然月重置,未用完點數不累積至下月。", + + "plan.pro.headline": "多帳、重度 AI 與研究", + "plan.pro.bullet1": "每月 {n} 點(約 3× Starter)", + "plan.pro.bullet2": "高頻文案/研究/生圖 · 重度天花板", + "plan.pro.bullet3": "適合代理與多品牌", + "plan.pro.bullet4": "付款成功立即生效", + "plan.pro.right1": "付款成功後本帳改為 Pro,當月依 Pro 額度計算。", + "plan.pro.right2": "適合多帳、大量回覆與深研究,減少中途額度見底。", + "plan.pro.right3": "功能相同;買的是容量。再高可改 BYOK,平台限流時也不中斷。", + "plan.pro.quota1": "每月 {n} 點 · {price}。", + "plan.pro.note1": "付款失敗不會改方案。", + "plan.pro.note2": "付款完成後可於帳務紀錄查詢收據。", + + "plan.quota2": "分項點數:文案 {copy}(約 {copyCalls} 次)、研究 {research}(約 {researchCalls} 次)、搜尋 {search}(約 {searchCalls} 次)、生圖 {image}(約 {imageCalls} 張)。", + "plan.softCapsLine": "分項點數:文案 {copy} · 研究 {research} · 搜尋 {search} · 生圖 {image}", + "plan.approxCallsLine": "約 {copyCalls} 次文案 · {researchCalls} 次研究 · {searchCalls} 次搜尋 · {imageCalls} 張圖", + + "checkout.title": "確認方案", + "checkout.pickFirst": "請先選擇方案。", + "checkout.viewPlans": "看方案", + "checkout.fail": "無法完成", + "checkout.confirmFree": "確認切換至 Free", + "checkout.payAndAction": "{action}並付款 {price}", + "checkout.subscribe": "訂閱", + "checkout.perMonth": "/月", + "checkout.monthlyCredits": "每月 {n} 點", + "checkout.youGet": "你會得到", + "checkout.quota": "額度", + "checkout.notes": "注意", + "checkout.amountDue": "應付金額", + "checkout.billedMonthly": "{name} · 按月計費", + "checkout.already": "已是此方案", + "checkout.processing": "處理中…", + "checkout.currentPlan": "目前方案", + "checkout.pickOther": "改選其他方案", + "checkout.cancel": "取消", + "checkout.invalidUrl": "付款服務回傳了不安全的網址,未進行跳轉。", + "checkout.redirecting": "正在前往 Stripe 安全付款頁面…", + "checkout.redirectingPortal": "正在前往 Stripe 訂閱管理頁面…", + "checkout.redirectFailed": "無法開啟 Stripe 頁面。請檢查瀏覽器或網路設定後再試一次。", + "checkout.networkError": "無法連線至帳務服務。請檢查網路後再試一次。", + "checkout.unavailable": "帳務服務目前尚未啟用或暫時無法使用,請稍後再試。", + "checkout.sessionExpired": "登入狀態已失效,請重新登入後再試。", + "checkout.portalUnavailable": "目前沒有可管理的 Stripe 訂閱。請先選擇付費方案。", + "checkout.verifying": "正在確認付款與方案生效狀態…", + "checkout.pollFail": "無法查詢付款狀態,請重試。", + "checkout.missingId": "缺少結帳編號,無法確認付款結果。", + "checkout.terminalFail": "結帳未完成({status})。你可以重新選擇方案再試一次。", + "checkout.timeout": "付款可能仍在處理中,但方案尚未於 30 秒內生效。請重試查詢;請勿重複付款。", + "checkout.retry": "重試查詢", + "checkout.canceledTitle": "已取消結帳", + "checkout.canceledBody": "未變更方案,也未執行任何扣款操作。", + "checkout.manageInstead": "此方案異動請在帳務入口管理,避免建立重複訂閱。", + + "usage.widget.titleUsed": "{name} · 已用 {used}/{cap} 點", + "usage.widget.titleUnlimited": "{name} · 不擋額度", + "usage.widget.ariaUsed": "已用 {used} 點,共 {cap} 點", + "usage.widget.dialog": "方案與用量", + "usage.widget.currentPlan": "目前方案", + "usage.widget.unlimited": "不擋額度", + "usage.widget.perMonth": "/月", + "usage.widget.monthUsage": "本月用量", + "usage.widget.remaining": "還剩 {n} 點", + "usage.widget.leftShort": "還剩 {n}", + "usage.widget.usedOfCap": "{used}/{cap}", + "usage.widget.overShort": "已超額", + "usage.widget.upgradeShort": "升級", + "usage.widget.upgrade": "升級方案", + "usage.widget.includes": "這個方案包含", + "usage.widget.nudge": "本月額度快用完了,升級可立刻加大點數。", + "usage.widget.changePlan": "變更方案", + "usage.widget.usageDetail": "用量明細", + + "usage.meter.ai_copy": "AI 文案", + "usage.meter.ai_research": "AI 研究", + "usage.meter.web_search": "搜尋", + "usage.meter.ai_image": "AI 生圖", + "usage.meter.barAria": "{label} 已用 {credits} 點/上限 {cap} 點({count} 次)", + "usage.ledger.costAria": "消耗 {n} 點", + "usage.event.keyMode.platform": "平台點數", + "usage.event.keyMode.byok": "自備 Key", + "usage.event.cost.credits": "−{n}", + "usage.event.cost.byok": "自備", + "usage.event.cost.byokAria": "使用自備 Key,不扣平台點", + "usage.event.label.unknown": "使用紀錄", + "usage.event.label.genericAi": "AI 呼叫", + "usage.event.label.personaAnalyzeText": "人設分析 · 文字", + "usage.event.label.personaAnalyzeAccount": "人設分析 · 公開帳號", + "usage.event.label.composeMimic": "仿寫貼文", + "usage.event.label.composeViral": "爆紅分析", + "usage.event.label.personaPreview": "人設試產", + "usage.event.label.ownPostReply": "自己貼文 · 回覆草稿", + "usage.event.label.mentionReply": "提及 · 回覆草稿", + "usage.event.label.inspireChat": "靈感聊天", + "usage.event.label.researchSearch": "研究搜尋", + "usage.event.label.generateImage": "產生圖片", + "usage.event.label.search": "網頁搜尋", + "usage.event.label.aiComplete": "AI 補全", + "usage.event.source.personaAnalyzeText": "人設分析 · 文字", + "usage.event.source.personaAnalyzeAccount": "人設分析 · 公開帳號", + "usage.event.source.composeMimic": "仿寫貼文", + "usage.event.source.composeViral": "爆紅分析", + "usage.event.source.personaPreview": "人設試產", + "usage.event.source.ownPostAnalyze": "自己貼文 · 結構分析", + "usage.event.source.ownPostReply": "自己貼文 · 回覆草稿", + "usage.event.source.mentionReply": "提及 · 回覆草稿", + "usage.event.source.inspireChat": "靈感聊天", + "usage.event.source.researchSearch": "研究搜尋", + "usage.event.source.generateImage": "產生圖片", + "usage.event.source.proxySearch": "網頁搜尋", + "usage.event.source.proxyAi": "AI 補全", + + "usage.chart.period": "區間", + "usage.chart.allocated": "配給", + "usage.chart.consumed": "消耗", + "usage.chart.pctTitle": "消耗佔配給比例", + "usage.chart.aria": "配給與消耗", + "usage.chart.colAria": "{label} 配給 {purchased} 消耗 {consumed}", + + "settings.provider": "Provider", + "settings.model": "模型", + "settings.aiUnifiedHint": "文案、研究、延伸全部使用同一 provider 與模型。", + "settings.fetchModels": "取得模型", + "settings.fetchingModels": "讀取中…", + "settings.apiKey": "API Key", + "settings.configured": "已設定", + "settings.notConfigured": "未設定", + "settings.platformKeyOk": "可用平台 key(可改填自己的)", + "settings.modelsHint": "模型清單", + "settings.modelsCached": "模型清單來自快取(約 5 分鐘)", + "settings.modelsLoaded": "已取得 {provider} 模型清單", + "settings.aiSaved": "AI 設定已儲存", + "settings.searchSaved": "搜尋設定已儲存", + "settings.clearAiKey": "清除自備 AI Key", + "settings.aiKeyCleared": "自備 AI Key 已清除", + "settings.clearExaKey": "清除 Exa Key", + "settings.exaKeyCleared": "Exa Key 已清除", + "settings.searchProvider": "搜尋 Provider", + "settings.expand": "延伸策略", + "settings.exaKey": "Exa API Key", + "settings.devMode": "測試海巡(本機工作階段)", + "settings.devModeHint": "開啟後,測試海巡可使用已同步的 Chrome 登入態。正式發文/留言仍走官方 API。", + "settings.ext.title": "Chrome 擴充套件", + "settings.ext.desc": "安裝此擴充(v1.2.0+),才能從 Chrome 同步 Threads 登入態到測試海巡。", + "settings.ext.step1": "下載並解壓縮 ZIP,得到 haixun-threads-sync 資料夾", + "settings.ext.step2": "Chrome 開啟 chrome://extensions,開啟「開發人員模式」", + "settings.ext.step3": "點「載入未封裝項目」,選擇解壓後的資料夾(已安裝則按「重新載入」)", + "settings.ext.step4": "在擴充選項填入此站網址(與網址列一致),再重新整理巡樓頁", + "settings.ext.download": "下載擴充套件(ZIP)", + "settings.ext.sessionTitle": "Chrome Session(測試海巡)", + "settings.ext.sessionHint": "從已登入的 Threads 分頁同步登入態,供測試海巡使用。正式發文仍走官方 API。", + "settings.ext.pageOrigin": "目前分頁:{origin}", + "settings.ext.detected": "擴充已偵測", + "settings.ext.notReady": "尚未偵測擴充", + "settings.ext.synced": "Session 已同步", + "settings.ext.notSynced": "Session 未同步", + "settings.ext.syncBtn": "從 Chrome 同步 Session", + "settings.ext.recheck": "重新偵測", + "settings.ext.syncOk": "Chrome 工作階段已同步到測試海巡", + "settings.ext.syncFail": "Chrome session 同步失敗", + "settings.ext.needLogin": "尚未登入,請先登入巡樓後再同步。", + "settings.ext.notDetected": "找不到巡樓 Chrome 擴充(頁面 {origin})。請用 v1.2.1+:chrome://extensions 重新載入 → 擴充選項填入同一網址並儲存 → 回此頁 F5。", + "settings.ext.reloadHint": "安裝或更新擴充後請重新載入擴充,再按 F5 刷新此頁。", + "settings.ext.detectSteps": "裝好了卻偵測不到?請依序:① chrome://extensions 確認「巡樓 Threads Session 同步」已啟用並按「重新載入」(需 v1.2.1)② 擴充「詳細資料/選項」把 Server URL 設成 {origin} 並儲存(彈窗按允許)③ 回此分頁硬重新整理(F5)。本機請勿混用 localhost 與 127.0.0.1。", + + "forgot.fail": "送出失敗", + "forgot.mockHint": "正式環境會寄到信箱;此處直接給連結:", + "forgot.checkInbox": "請檢查信箱(含垃圾郵件)。", + + "reset.mismatch": "兩次密碼不一致", + "reset.fail": "重設失敗", + "reset.cardTitle": "重設密碼", + "reset.redirecting": "即將前往登入頁…", + "reset.loginNow": "立即登入", + "reset.passwordPh": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + "reset.forgotLink": "重新申請重設", + + "verify.sendFail": "寄送失敗", + "verify.fail": "驗證失敗", + "verify.success": "信箱已驗證,可以使用巡樓了。", + "verify.codePh": "6 位數字", + "verify.currentAccount": "目前帳號:{email}", + + "login.brandTitle": "巡樓 · Lapras", + + "home.navLabel": "公開導覽", + "public.localeLabel": "介面語言", + "home.heroTitle": "在 Threads 找到客戶,一路跟到成交", + "home.heroLead": "每天自動整理需求名單,回覆與跟進都在同一張工作台。", + "home.outcomesTitle": "能做到什麼", + "home.outcome.find.title": "找到正在找你的人", + "home.outcome.find.body": "關鍵字訂閱每天自動掃;需要時也可手動掃一輪。", + "home.outcome.reply.title": "回得出、說得準", + "home.outcome.reply.body": "依你的服務與口吻產草稿,禁語不會亂講。", + "home.outcome.close.title": "跟得到成交", + "home.outcome.close.body": "同一人收斂成名單,階段與待追蹤不靠記憶。", + "home.productTitle": "主打功能", + "home.productLead": "從找需求到發送,都在同一張工作台。", + "home.preview.radar.title": "今日商機", + "home.preview.radar.caption": "訂閱關鍵字,每天自動整理需求名單", + "home.preview.scout.title": "海巡掃場", + "home.preview.scout.caption": "手動掃一輪,立刻鎖定可回覆貼文", + "home.preview.studio.title": "回覆與創作", + "home.preview.studio.caption": "依服務與口吻產草稿,禁語不亂講", + "home.preview.crm.title": "名單與階段", + "home.preview.crm.caption": "同一人收斂成一張卡,跟進不靠記憶", + "home.preview.outbox.title": "發送佇列", + "home.preview.outbox.caption": "排程、待送、已送一覽", + "home.preview.mock.radar.badge": "今日 · 高匹配", + "home.preview.mock.radar.meta": "3 則新需求", + "home.preview.mock.radar.row1": "台北|徵求搬家報價", + "home.preview.mock.radar.row2": "有人在問居家清潔方案", + "home.preview.mock.radar.row3": "接案設計 · 預算已備", + "home.preview.mock.scout.badge": "關鍵字掃描", + "home.preview.mock.scout.hit": "求推:有推薦的會計師嗎", + "home.preview.mock.scout.snippet": "想找能處理小型公司報稅的…", + "home.preview.mock.scout.hit2": "有人用過某某 SaaS 嗎", + "home.preview.mock.studio.tab1": "靈感", + "home.preview.mock.studio.tab2": "回覆", + "home.preview.mock.studio.tab3": "排程", + "home.preview.mock.studio.draft1": "嗨,我看到你在找搬家協助——", + "home.preview.mock.studio.draft2": "我們主要服務大台北,可先估趟次與箱數。", + "home.preview.mock.studio.draft3": "(依你的服務檔與禁語產生)", + "home.preview.mock.crm.col1": "新線索", + "home.preview.mock.crm.col2": "洽談中", + "home.preview.mock.crm.col3": "成交", + "home.preview.mock.crm.foot": "待追蹤與到期提醒", + "home.preview.mock.outbox.r1": "回覆 · 今晚 20:00", + "home.preview.mock.outbox.r2": "主貼 · 待確認", + "home.preview.mock.outbox.r3": "已送 · 成效同步中", + "home.pricingTitle": "方案", + "home.pricingLead": "功能相同,差在每月平台點數。", + "home.pricingHoverHint": "把游標移到「每月點數」可看各方案能做什麼。", + "home.ctaLogin": "登入", + "home.privacyLink": "隱私權政策", + "home.termsLink": "服務條款", + "home.dataDeletionLink": "資料刪除說明", + "legal.footerNav": "法律與政策連結", + + "privacy.navLabel": "隱私權政策導覽", + "privacy.title": "隱私權政策", + "privacy.updated": "最後更新:2026-07-31", + "privacy.intro": + "本隱私權政策說明巡樓(Lapras,以下稱「本服務」或「我們」)如何蒐集、使用、儲存、分享與刪除你的個人資料與 Meta/Threads 平台資料。本政策適用於巡樓網頁主控台、公開工具頁,以及你透過 OAuth 連結之 Threads 帳號相關處理。使用本服務即表示你知悉本政策。", + "privacy.section.overview.title": "1. 控管者與適用範圍", + "privacy.section.overview.body": + "本服務由巡樓(Lapras)營運方作為個人資料控管者。\n本政策涵蓋:會員帳號、工作區內容、用量與付款識別、以及你授權本服務自 Meta/Threads API 取得之平台資料。\n若你透過邀請加入他人工作區,工作區擁有者可能另有內部規範,但不取代本政策對平台層與會員層資料之說明。\n本政策為我們自有政策,非 Meta、Threads 或 Instagram 之政策;Meta 如何處理其產品內資料,請另見 Meta 隱私中心。", + "privacy.section.collect.title": "2. 我們蒐集哪些資料", + "privacy.section.collect.body": + "(A)你直接提供的資料\n• 帳號:Email、顯示名稱、密碼雜湊(不保存明文密碼)、角色與信箱驗證狀態。\n• 你輸入的內容:人設、品牌、海巡意圖/關鍵字、創作草稿、回覆文案、Outbox 內容、工作區設定與上傳之媒體(若有)。\n• 支援與個資請求:你寄給我們的 Email 與請求內容。\n\n(B)自動蒐集\n• 登入 session、IP、基本瀏覽器/裝置資訊(安全、除錯、防濫用)。\n• 介面偏好(語言、主題)。\n• 功能用量與任務日誌(方案額度、錯誤診斷)。\n\n(C)來自 Meta/Threads 的平台資料(僅在你授權連結後)\n• Threads 使用者識別(例如 user id、username)、公開或授權範圍內的個人檔摘要。\n• 你授權範圍內的貼文、回覆、媒體中繼資料、以及為完成發送/讀取/成效所需之 API 回應。\n• 存取權杖(access token/refresh 相關憑證):僅用於代你呼叫 Threads API,與會員登入 JWT 分開存放。\n\n(D)付款(若啟用)\n• 方案等級、點數用量、金流交易識別(細節依金流供應商;我們不儲存完整卡號)。\n\n我們不會以一般使用為條件要求無關的政府身分證件。", + "privacy.section.use.title": "3. 處理目的與方式", + "privacy.section.use.body": + "我們處理上述資料的目的包括:\n• 提供帳號、工作區、權限與登入安全。\n• 執行核心功能:海巡掃描/外展、創作與 AI 輔助、Outbox 排程與發送、帳號與貼文同步、成效與用量計量。\n• 以你授權的 Threads token 代表你呼叫 Meta/Threads API(讀取授權內容、發布你確認之內容等,以實際 scope 為準)。\n• 寄送驗證碼、密碼重設與服務通知。\n• 防止濫用、保障服務穩定、除錯與產品改善(儘量去識別化)。\n• 遵守法律義務或回應合法請求。\n\n我們不會將 Meta/Threads 平台資料出售給資料掮客,也不會將該等資料用於與提供本服務無關的獨立行銷檔案建置。", + "privacy.section.threads.title": "4. Threads/Meta 平台資料", + "privacy.section.threads.body": + "連結方式:你透過 Meta OAuth 授權本服務存取 Threads。我們僅請求並使用完成產品功能所需之權限,例如讀取基本檔案、內容發布等;實際清單以授權畫面為準。\n\n用途限制:自 Meta 取得的平台資料,僅用於提供、維護與改善你所使用的巡樓功能(例如綁定帳號、同步貼文、排程/發送回覆或貼文、顯示狀態與除錯),不會轉售,亦不會用於與你授權無關的廣告定向。\n\n儲存與隔離:Threads 授權憑證與會員登入憑證分開管理;工作區與帳號層級有權限控管。\n\n你可隨時:\n• 在本服務解除 Threads 綁定;及/或\n• 在 Meta/Threads/Instagram 設定中撤銷本 App 授權。\n解除後我們停止新的 API 存取,並依第 8 節處理刪除或匿名化。\n\nMeta/Threads 自身如何處理資料,受 Meta 隱私政策與 Threads 補充條款拘束;本服務無法控制 Meta 端行為。", + "privacy.section.share.title": "5. 資料分享與處理者", + "privacy.section.share.body": + "我們可能在下列情況分享或由處理者代為處理資料:\n• 基礎設施:雲端主機、資料庫、物件儲存、電子郵件發送、監控與日誌服務(僅為營運本服務)。\n• AI/搜尋供應商:當你使用靈感、分析、建議文案等功能時,必要提示與內容片段可能傳送至供應商(見第 6 節)。\n• 金流供應商:處理付款時。\n• 法律要求:法院、主管機關之合法要求。\n• 業務承繼:合併、收購等情形下,依適用法律與通知處理。\n\n我們要求處理者僅依指示處理,並採取合理安全措施。我們不會出售個人資料。", + "privacy.section.ai.title": "6. AI 與自動化處理", + "privacy.section.ai.body": + "部分功能會將你提供的提示、貼文樣本、結構化備註或授權同步之公開內容摘要送至 AI/搜尋供應商,以產生建議文案、風格分析或檢索結果。\n你可使用平台預設供應商,或自行設定 BYOK(Bring Your Own Key);BYOK 時請求導向你指定的供應商,請同時閱讀其條款。\n我們不會在未經你另行同意下,把你的內容當作對外行銷素材或公開訓練語料宣傳。", + "privacy.section.retention.title": "7. 保存期間", + "privacy.section.retention.body": + "• 帳號與工作區資料:於帳號有效期間保存,以維持服務。\n• Threads token:於綁定有效期間保存;解除綁定或刪除請求後失效/移除。\n• 任務日誌、用量與安全紀錄:依營運、爭議處理與法令需要保存合理期間後刪除或匿名化。\n• 備份:可能在輪替週期內短暫殘留,期滿清除。\n詳細刪除步驟見第 8 節與「資料刪除說明」頁。", + "privacy.section.deletion.title": "8. 如何請求刪除資料", + "privacy.section.deletion.body": + "你可依下列方式請求刪除我們所持有、與你相關的個人資料與平台資料:\n\n方式一(建議):登入巡樓 → 會員/設定相關頁面解除 Threads 綁定,並依介面指示提出帳號或資料刪除(若已提供自助刪除)。\n\n方式二:寄信至本服務營運方,使用你註冊之 Email 寄出,主旨註明「資料刪除請求」或 Data Deletion Request,並提供:\n• 註冊 Email\n• 若知悉:Threads username 或本服務內顯示之帳號識別\n• 欲刪除範圍(全部帳號/僅 Threads 連線資料/特定工作區)\n\n方式三:在 Meta 移除本 App 並請求刪除時,依 Meta 流程處理;我們也會依平台回呼(若有)處理對應資料,否則請仍依方式二聯絡我們。\n\n更完整的步驟見本站「資料刪除說明」頁(/data-deletion)。\n我們於核對身分後,將在合理期間(通常 30 日內,法令另有規定從其規定)刪除或匿名化,法令要求保留者除外。", + "privacy.section.rights.title": "9. 你的權利", + "privacy.section.rights.body": + "在適用法律允許範圍內,你可要求:查詢、更正、下載(可攜)、刪除、限制或反對特定處理。你可隨時登出、修改個人資料、解除 Threads 綁定或停止使用。行使權利請依第 12 節聯絡我們;我們可能需驗證身分。", + "privacy.section.security.title": "10. 安全措施", + "privacy.section.security.body": + "我們採取合理技術與組織措施,包括 HTTPS 傳輸、密碼雜湊、權限隔離、會員憑證與 Threads token 分離存放、以及存取控管。無系統可保證絕對安全;若發生可能影響你權益之資安事件,我們將依法或依政策通知。", + "privacy.section.children.title": "11. 兒童與未成年人", + "privacy.section.children.body": + "本服務面向內容經營與商業/創作者使用,不以兒童為對象。我們不故意蒐集未滿適用年齡(例如 13 歲,或你所在地更高門檻)者之個人資料。若你認為兒童資料被誤收,請依第 12 節聯絡,我們將儘速刪除。", + "privacy.section.contact.title": "12. 聯絡我們", + "privacy.section.contact.body": + "隱私權、個資查詢、更正或刪除請求,請使用你註冊之 Email 與本服務營運方聯繫,主旨註明「Privacy/Data Request」,並說明帳號 Email 與請求內容,以便核對身分。\n你亦可先登入後於會員/設定頁依指示操作。", + "privacy.section.updates.title": "13. 政策更新", + "privacy.section.updates.body": + "我們可能因功能、Meta 平台規則或法令更新本政策。更新後會修改「最後更新」日期;重大變更時可能以站內通知或 Email 告知。在法律允許範圍內,更新後繼續使用表示你知悉修訂內容。", + "privacy.backHome": "回到介紹首頁", + + "terms.navLabel": "服務條款導覽", + "terms.title": "服務條款", + "terms.updated": "最後更新:2026-07-31", + "terms.intro": + "歡迎使用巡樓(Lapras)。本服務條款(Terms of Service)規範你與本服務營運方之間就使用網頁主控台、公開工具與相關功能之約定。使用本服務即表示你同意本條款;若不同意,請勿使用。", + "terms.section.acceptance.title": "1. 接受條款", + "terms.section.acceptance.body": + "你必須具備締結合約之法定能力,並遵守所在地法律。若你代表公司或組織使用,你保證有權使該組織受本條款拘束。", + "terms.section.service.title": "2. 服務說明", + "terms.section.service.body": + "巡樓提供 Threads 內容經營相關工具,包括但不限於帳號綁定、海巡與外展、創作與發送、人設/品牌、用量與方案等。功能可能隨版本調整;我們得基於維護、安全或法規暫停或變更部分功能。", + "terms.section.accounts.title": "3. 帳號與安全", + "terms.section.accounts.body": + "你應提供正確資料、妥善保管登入憑證,並對帳號下之行為負責。發現未授權使用應立即通知我們。我們得因安全、濫用或違規而限制或停權帳號。", + "terms.section.threads.title": "4. Threads/Meta 連線", + "terms.section.threads.body": + "部分功能需你透過 Meta OAuth 授權連結 Threads。你保證有權連結該帳號,並遵守 Meta、Threads、Instagram 之平台條款與社群規範。\n你應自行確保發送內容合法、不侵權、不違反平台政策。因你的內容或帳號行為導致平台處罰、API 拒絕或第三方主張,由你自行負責;本服務僅提供工具,不保證平台審核結果或觸及成效。\n你可以隨時解除綁定或撤銷 Meta 授權;解除後部分功能將無法使用。", + "terms.section.content.title": "5. 你的內容", + "terms.section.content.body": + "你保留對自己上傳或產生內容之權利。你授予我們為提供服務所需之非專屬、全球、免權利金授權(儲存、處理、傳輸、顯示給你與你授權之工作區成員、以及呼叫必要之第三方 API)。你保證內容不違法、不侵權。", + "terms.section.acceptable.title": "6. 禁止行為", + "terms.section.acceptable.body": + "禁止:濫用 API 或自動化干擾他人;散布垃圾、詐騙、仇恨或違法內容;規避用量或安全機制;逆向工程或未經授權存取系統;將服務用於違反 Meta 平台政策之行為;侵害他人隱私或智慧財產。", + "terms.section.billing.title": "7. 方案與用量", + "terms.section.billing.body": + "付費方案、點數與限制以產品內標示為準。未付款、超額或濫用可能導致功能降級或暫停。退款政策依購買時說明與適用法律。", + "terms.section.disclaimer.title": "8. 免責與責任限制", + "terms.section.disclaimer.body": + "服務依「現況」提供。在法律允許最大範圍內,我們不保證無中斷、無錯誤,或第三方平台(含 Threads/Meta/AI 供應商)持續可用。對間接、附隨、逸失利益等損害,除法律強制規定外不負責任;我們對你的總責任以你於事故前十二個月內就本服務已付費用為上限(若為免費使用則為零),法律禁止限制者除外。", + "terms.section.termination.title": "9. 終止", + "terms.section.termination.body": + "你可隨時停止使用並依隱私權政策請求刪除資料。我們得因違約、濫用、法律要求或停止營運而終止或暫停服務。終止後依隱私權政策處理資料。", + "terms.section.contact.title": "10. 聯絡與準據", + "terms.section.contact.body": + "條款相關問題請透過註冊 Email 聯絡營運方。準據法與管轄,除強制規定外,以本服務主要營運地之法律為準。隱私處理見 /privacy;資料刪除見 /data-deletion。", + + "deletion.navLabel": "資料刪除說明導覽", + "deletion.title": "使用者資料刪除說明", + "deletion.updated": "最後更新:2026-07-31", + "deletion.intro": + "本頁說明如何請求巡樓(Lapras)刪除與你相關的個人資料與 Threads 平台資料。", + "deletion.section.summary.title": "1. 摘要", + "deletion.section.summary.body": + "當你不再使用本服務,或自 Meta 移除本 App 並希望刪除我們所持資料時,請依下列步驟提出請求。我們會在核對身分後刪除或匿名化可刪除之資料(法令或正當利益需保留者除外)。", + "deletion.section.steps.title": "2. 如何提出刪除請求", + "deletion.section.steps.body": + "步驟 1:若仍可登入,先至巡樓解除所有 Threads 帳號綁定(設定/帳號相關頁)。\n步驟 2:使用你註冊本服務時的 Email 寄信給營運方。\n步驟 3:主旨請寫:「資料刪除請求」或 Data Deletion Request。\n步驟 4:信中請提供:\n• 註冊 Email(必填)\n• 顯示名稱或會員識別(若知悉)\n• Threads username 或本服務內帳號 id(若曾連結)\n• 刪除範圍:整個帳號/僅 Threads 連線與同步資料/特定工作區\n步驟 5:我們回覆確認後開始處理;完成後以 Email 告知結果(若該信箱仍可用)。", + "deletion.section.threads.title": "3. 與 Threads/Meta 授權的關係", + "deletion.section.threads.body": + "刪除本服務資料不會自動刪除你在 Threads/Instagram/Meta 上的貼文或帳號。\n請同時在 Meta/Threads 設定中撤銷對本 App 的授權,以停止 Meta 端的授權狀態。\n若平台端另有刪除回呼,我們會一併處理對應資料;否則請依本頁 Email 流程提出請求。", + "deletion.section.scope.title": "4. 會刪除什麼", + "deletion.section.scope.body": + "在合理可行範圍內,我們將刪除或匿名化:\n• 會員個人資料與登入憑證\n• 工作區內由你建立之內容(人設、草稿、海巡與 Outbox 等,依請求範圍)\n• Threads 連線識別、token 與同步快取\n• 非必要之任務與分析紀錄\n\n可能暫時或依法保留:\n• 法令要求之交易/帳務紀錄\n• 安全與防濫用日誌(有限期間)\n• 備份輪替中尚未到期之副本", + "deletion.section.timeline.title": "5. 處理時程", + "deletion.section.timeline.body": + "我們通常在收到可核對之請求後 30 日內完成刪除或匿名化。若需更長時間(例如複雜工作區或法遵審查),我們會告知預估時程。備份清除可能隨後完成。", + "deletion.section.contact.title": "6. 聯絡", + "deletion.section.contact.body": + "請使用註冊 Email 聯絡本服務營運方,主旨「Data Deletion Request」。完整隱私說明見 /privacy;服務條款見 /terms。", + + "common.listSep": "、", + "common.dash": "—", + + "scout.title": "話題靈感", + "scout.topic.intro": "輸入作品、人物、事件或工作方向;系統會判斷你想找的熱門討論、推薦或公開工作訊號。要持續管理客戶需求,請用「商機」。", + "scout.topic.termHint": "已依整體意圖預選最多 3 組;你仍可取消、改詞或加上自己的說法。", + "scout.topic.termsNeedShort": "有 {n} 組關鍵字不合 Threads 短詞規則,請改短後再搜。", + "scout.topic.noTerms": "沒產出可用關鍵字,請換個更具體的主題再試(例:台北 市集、保母 求推薦)。", + "scout.topic.termsReadyPrimary": "已理解輸入並預選最多 3 組高信心查詢,另有 {n} 組可調整。", + "scout.topic.workshopHintSelect": "這些詞會分別搜尋後再去重;已預選最多 3 組高信心查詢,你可以在送出前調整。", + "scout.topic.primaryTerm": "主查詢(建議)", + "scout.topic.variantTerm": "變體 {n}", + "scout.topic.useTerm": "使用此關鍵字搜尋", + "scout.today": "今日出擊", + "scout.purposeValue": "痛點回覆", + "scout.purposeDemand": "找需求痛點", + "scout.purposeProvider": "解法媒合", + "scout.purposeActivity": "活躍短回", + "scout.goal": "今日目標(則)", + "scout.progress": "進度 {done}/{goal}", + "scout.intent": "我想找/回應", + "scout.keyword": "關鍵字", + "scout.intentPh": "例:換季頭皮刺癢、真的無香、週末有插座", + "scout.keywordPh": "例:外包 工程師 後端、鬼滅之刃", + "scout.productOptional": "產品(選填)", + "scout.productRequired": "要解決的產品(必填)", + "scout.selectProduct": "選擇產品", + "scout.noProduct": "不帶產品", + "scout.brandFallback": "品牌", + "scout.placement": "置入:{label}", + "scout.providerProduct": "產品:{label}", + "scout.painPart": " · 痛點「{pain}」", + "scout.noProductsBefore": "尚無產品,可到", + "scout.noProductsAfter": "新增。", + "scout.start": "開始", + "scout.startMore": "再撈一批", + "scout.fetching": "撈取中…", + "scout.planKeywords": "產出關鍵字", + "scout.planning": "整理關鍵字…", + "scout.workshop": "搜尋關鍵字(可改)", + "scout.workshopHint": "確認後才會搜尋。每條是一組獨立 query;刪掉不準的、加上你知道有效的說法。", + "scout.workshopEmpty": "至少保留一條關鍵字才能搜尋", + "scout.addTerm": "新增", + "scout.addTermPh": "再加一條搜尋關鍵字", + "scout.removeTerm": "移除", + "scout.confirmScan": "用這些詞開始搜", + "scout.startImmediate": "立即開始海巡", + "scout.immediateHint": "立即開始會先自動整理建議詞,再用最多 3 組精準搜尋詞送出。", + "scout.openDailySchedule": "查看每日排程", + "scout.scheduleHint": "每日自動巡邏請到商機的巡邏設定。", + "scout.replan": "重新產出", + "scout.clearWorkshop": "取消", + "scout.termsReady": "已產出 {n} 條關鍵字,請確認後再搜。", + "scout.runs": "海巡批次", + "scout.runCount": "批次({n})", + "scout.runCreatedAt": "建立 {time}", + "scout.runSelectAria": "切換海巡批次", + "scout.runPending": "待回 {n} · ", + "scout.runDone": "已清完 · ", + "scout.runTotal": "({n} 則)", + "scout.runStatus.queued": "排隊中", + "scout.runStatus.running": "掃描中", + "scout.runStatus.succeeded": "已完成", + "scout.runStatus.failed": "失敗", + "scout.runStatus.cancelled": "已取消", + "scout.shortfall": "尚缺 {n} 則", + "scout.shortfallReason.source_exhausted": "來源已用盡", + "scout.shortfallReason.duplicate_exhausted": "重複太多", + "scout.shortfallReason.relevance_exhausted": "關聯性不足", + "scout.shortfallReason.source_unavailable": "來源暫不可用", + "scout.shortfallReason.limit_reached": "達到上限", + "scout.shortfallReason.unknown": "來源不足", + "scout.refreshRuns": "重新整理批次", + "scout.refreshingRuns": "整理中…", + "scout.runsRefreshed": "批次已重新整理", + "scout.newRunReady": "有新的海巡批次完成;目前閱讀位置未變,按重新整理批次查看。", + "scout.deleteRun": "刪除這一批", + "scout.deleting": "刪除中…", + "scout.now": "現在這一則", + "scout.emptyBatch": "這一批沒有待回。想每天自動收需求名單?用側欄「商機」訂閱關鍵字。", + "scout.draft": "回覆草稿", + "scout.draftPhActivity": "短回…", + "scout.draftPhValue": "共感 → 建議…", + "scout.sendAccount": "用哪個帳號送", + "scout.noAccount": "無可用帳號", + "scout.personaForRegen": "人設(再產用)", + "scout.notReady": "(未就緒)", + "scout.skip": "略過", + "scout.regen": "再產", + "scout.send": "發送", + "scout.openThreadsReply": "開啟 Threads 留言", + "scout.markManualDone": "已留言,標記完成", + "scout.manualReplyHint": "先確認草稿,開啟 Threads 後直接在原文底下留言;草稿會嘗試自動複製。完成後回到這裡標記完成。", + "scout.openedAndCopied": "已開啟 Threads,草稿也已複製;貼上留言後再回來標記完成。", + "scout.openedManual": "已開啟 Threads;留言完成後請回來標記完成。", + "scout.noPermalink": "這筆命中沒有可開啟的 Threads 原文連結。", + "scout.manualDone": "已標記人工留言完成 · 今日 {done}/{goal}", + "scout.manualDoneFail": "標記人工留言完成失敗", + "scout.resend": "重新送出", + "scout.sending": "發送中…", + "scout.needAccountBefore": "請先到", + "scout.needAccountAfter": "連線可用帳號。", + "scout.loadingKnowledge": "正在整理周邊知識…", + "scout.product": "產品", + "scout.queue": "命中紀錄 · {n}", + "scout.valueQueue": "痛點/產品接話 · {n}", + "scout.providerQueue": "解法提供者 · {n}", + "scout.demandQueue": "需求痛點 · {n}", + "scout.activityQueue": "活躍短回 · {n}", + "scout.noMatchesInQueue": "這個佇列暫無命中", + "scout.collapseQueue": "收合佇列", + "scout.expandQueue": "展開佇列", + "scout.noOtherPending": "沒有其他待回", + "scout.unnamedRun": "未命名批次", + "scout.thisRun": "此批次", + "scout.confirmDeleteRun": "刪除海巡批次「{label}」?\\n會一併刪除這批命中與周邊知識,無法復原。", + "scout.deletedRun": "已刪除批次「{label}」", + "scout.deleteRunFail": "刪除批次失敗", + "scout.err.runBusy": "這批次正在掃描或已完成,暫時不能刪除。", + "scout.err.notFound": "這批次或貼文已不存在,請重新整理批次。", + "scout.err.sourceUnavailable": "海巡來源目前無法使用,請稍後再試。", + "scout.needKeyword": "先填關鍵字", + "scout.needIntent": "先寫這次要找什麼", + "scout.productMissing": "所選產品不在列表中,請重新選擇", + "scout.providerSetupRequired": "解法媒合需要產品的痛點與至少一個標籤或解法能力詞;請到品牌頁補齊後再試。", + "scout.defaultLabel": "海巡", + "scout.newRunActivity": "新批次「{label}」· {n} 則待回", + "scout.newRunValue": "新批次「{label}」· {n} 則 · 請處理「現在這一則」", + "scout.knowledgeReady": "「{label}」周邊知識已備好 · {n} 則可學", + "scout.patrolFail": "這輪海巡失敗", + "scout.loadFail": "海巡資料載入失敗,請重新整理後再試。", + "scout.scanQueued": "已建立海巡任務「{label}」,完成後會提示新的批次。", + "scout.workerWaiting": "海巡任務仍在等待 worker。請確認 apps/backend worker 正在執行。", + "scout.scanReady": "海巡完成,找到 {n} 則待回。", + "scout.queued": "已交由 @{who} 的 Outbox 發送佇列處理 · 今日 {done}/{goal}", + "scout.scanJob": "海巡任務", + "scout.scanInProgress": "正在掃描", + "scout.crawlerSessionRequired": "測試海巡需要有效的 Chrome 工作階段。請在設定頁同步已登入 Threads 分頁的登入態。", + "scout.openSettings": "開啟設定", + "scout.source": "來源:Threads Keyword Search", + "scout.resultKeyword": "關鍵字:{tag}", + "scout.classification": "分類:{classification}", + "scout.postedAt": "發文:{time}", + "scout.postedUnknown": "貼文時間未知", + "scout.scannedAt": "掃入於 {time}", + "scout.createdAt": "建立於 {time}", + "scout.openPermalink": "在 Threads 開啟原文", + "scout.draftFail": "產草稿失敗", + "scout.skipped": "已略過", + "scout.status.new": "待處理", + "scout.status.drafted": "已起草", + "scout.status.queued": "發送佇列中", + "scout.status.published": "已發送", + "scout.status.skipped": "已略過", + "scout.noDraft": "沒有可發送的草稿", + "scout.accountFallback": "帳號", + "scout.sent": "已發送(@{who})· 今日 {done}/{goal}", + "scout.sendFail": "發送失敗", + "scout.confirmDeletePost": "刪除這則命中?", + "scout.deletedPost": "已刪除這則命中", + "scout.stanceActivity": "短回 · 養活躍", + "scout.stanceDemand": "需求痛點 · 可回應", + "scout.stanceProvider": "解法媒合 · 不推產品", + "scout.demandHint": "這是正在求助或比較解法的需求貼文。先閱讀原文,再以有幫助的方式回應。", + "scout.providerHint": "這是解法提供者候選名單。請先看原文與能力證據,再自行決定是否聯絡。", + "scout.stanceProduct": "共感 · 可輕帶產品", + "scout.stanceRelation": "接話 · 建關係", + + "brands.title": "品牌", + "brands.railAria": "品牌列表", + "brands.railLabel": "你的牌子", + "brands.add": "新增", + "brands.brandName": "品牌名稱", + "brands.brandNamePh": "例如:自家品牌", + "brands.creating": "建立中…", + "brands.createBrand": "建立品牌", + "brands.searchAria": "搜尋品牌", + "brands.searchPh": "搜尋品牌…", + "brands.empty": "尚無品牌", + "brands.noMatch": "無符合", + "brands.selectAria": "選擇品牌", + "brands.pickOne": "選一個品牌", + "brands.inUseHint": "使用中 · 海巡與創作會套用此牌", + "brands.inUse": "使用中", + "brands.tabBrands": "品牌庫", + "brands.tabInfo": "牌子資料", + "brands.tabProducts": "產品", + "brands.tabProductsN": "產品({n})", + "brands.displayName": "名稱", + "brands.brief": "摘要", + "brands.briefPh": "一句話說明這個牌子", + "brands.audience": "受眾", + "brands.audiencePh": "誰會在意、為什麼", + "brands.goals": "目標", + "brands.goalsPh": "想在 Threads 達成什麼", + "brands.saving": "儲存中…", + "brands.deleteBrand": "刪除品牌", + "brands.searchProductAria": "搜尋產品", + "brands.searchProductPh": "搜尋產品…", + "brands.addProduct": "新增產品", + "brands.noProducts": "尚無產品", + "brands.hasLink": "有連結", + "brands.painLabel": "痛點 ", + "brands.editProduct": "編輯產品", + "brands.newProduct": "新增產品", + "brands.importFromUrl": "從商品連結帶入", + "brands.fetching": "抓取中…", + "brands.fetch": "抓取", + "brands.pains": "痛點", + "brands.painsPh": "一列一個", + "brands.tags": "標籤", + "brands.tagsPh": "逗號分隔", + "brands.intro": "介紹", + "brands.providerCapabilities": "可解決痛點的能力/服務", + "brands.providerCapabilitiesPh": "例如:皮膚科、過敏原檢測、敏感肌諮詢", + "brands.providerExcludes": "同類型排除詞", + "brands.providerExcludesPh": "例如:洗髮精、護髮產品", + "brands.link": "連結", + "brands.update": "更新", + "brands.createItem": "新增", + "brands.needName": "請輸入名稱", + "brands.created": "已建立「{name}」", + "brands.createFail": "建立失敗", + "brands.saved": "已儲存", + "brands.saveFail": "儲存失敗", + "brands.confirmDelete": "確定刪除「{name}」?", + "brands.deleted": "已刪除", + "brands.deleteFail": "刪除失敗", + "brands.fetchFail": "抓取失敗", + "brands.needLabelContext": "名稱與介紹為必填", + "brands.productUpdated": "已更新", + "brands.productAdded": "已新增", + "brands.confirmDeleteProduct": "刪除此產品?", + + "insights.title": "帳號成效", + "insights.account": "帳號", + "insights.noAccount": "尚無帳號", + "insights.syncing": "同步中…", + "insights.syncPosts": "同步貼文", + "insights.myPosts": "我的貼文", + "insights.pickAccount": "選擇帳號", + "insights.goAccounts": "帳號", + "insights.kpiMonth": "本月指標", + "insights.monthViews": "本月瀏覽", + "insights.monthLikes": "本月讚", + "insights.monthReplies": "本月回覆", + "insights.engRate": "互動率", + "insights.vsPrev": "vs 上月", + "insights.avgNear": "近帖均 {rate}", + "insights.trendTitle": "趨勢與分析 · @{user}", + "insights.metricViews": "瀏覽", + "insights.metricLikes": "讚", + "insights.metricReplies": "回覆", + "insights.metricPosts": "貼文", + "insights.metricPostsFull": "貼文數", + "insights.chartMetrics": "圖表指標", + "insights.barsAria": "近月{metric},點柱查看該月分析", + "insights.barsLabel": "{metric} · 近 {n} 個月", + "insights.clickBar": " · 點柱看分析", + "insights.pickMonthAria": "選擇月份", + "insights.barTitle": "{label}:{value}{est} · 點看分析", + "insights.est": "(估)", + "insights.monthSuffix": "{m}月", + "insights.sparkAria": "趨勢折線,點節點可選月", + "insights.analysisOf": "{label} 分析", + "insights.producedAt": "產出於 {time}", + "insights.hasEstimate": " · 含估測數據", + "insights.viewsVsPrev": " · 瀏覽 vs 前月 {delta}", + "insights.statPosts": "貼文", + "insights.statViews": "瀏覽", + "insights.statLikes": "讚", + "insights.statReplies": "回", + "insights.conclusions": "結論", + "insights.recommendations": "建議", + "insights.highlights": "當月亮點", + "insights.findTopics": "找話題", + "insights.goScout": "去探查", + "insights.selectMonth": "選擇月份", + "insights.topPosts": "表現較佳貼文", + "insights.noPosts": "尚無貼文", + "insights.postStats": "瀏覽 {views} · 讚 {likes} · 回 {replies}", + "insights.openThreads": "開啟 Threads", + "insights.zeroPct": "0%", + "insights.panelHint": "依「我的貼文」同步數據聚合本月成效與近月趨勢", + "insights.lastSynced": "上次同步 {time}", + "insights.neverSynced": "尚未同步", + "insights.emptyTitle": "尚無貼文數據", + "insights.emptyDesc": "先按「同步貼文」從 Threads 拉入你的貼文與成效,再看月比圖與分析。", + "insights.syncDone": "已同步 {n} 則貼文,成效已更新", + "insights.syncFail": "同步失敗", + "insights.loadFail": "載入貼文失敗", + "insights.zeroViewsHint": "已有貼文但瀏覽多為 0:可能 Insights 權限不足或尚未產出,可再按一次同步。", + "insights.postsInMonth": "{n} 則", + "insights.kpiForMonth": "{label} 指標", + "insights.topPostsOfMonth": "{label} · 表現較佳貼文", + "insights.noPostsInMonth": "{label} 尚無貼文", + "insights.pastMonthEmptyHint": "該月沒有已同步的貼文(過去月份只會自動補抓一次)。可手動同步或改選其他月。", + "insights.pastBackfillDone": "已補抓歷史貼文 {n} 則(過去月份只抓一次)", + "insights.autoRefreshDone": "本月成效已更新({n} 則)", + "insights.noDataNoAnalysis": "{label} 沒有貼文數據,不產生結論。", + "insights.noAnalysisYet": "{label} 尚無可寫的結論(需有已同步貼文)。", + "insights.thisMonth": "本月", + "insights.narrative.summary": "{when}彙總:貼文 {posts}、瀏覽 {views}、讚 {likes}、回覆 {replies}(來自已同步貼文)。", + "insights.narrative.viewsDelta": "瀏覽較前月 {delta}({prev} → {curr})。", + "insights.narrative.viewsFlat": "瀏覽與前月大致持平({delta})。", + "insights.narrative.repliesUp": "回覆數 {delta},對話熱度上升。", + "insights.narrative.repliesDelta": "回覆數 {delta}。", + "insights.narrative.engRate": "互動率約 {pct}%(讚+回+轉+引用+分享/瀏覽)。", + "insights.narrative.engLow": "互動率偏低:可多試帶明確條件的提問收尾。", + "insights.narrative.engGood": "互動率不錯:可複製高表現貼的結構再測 1~2 則。", + "insights.narrative.zeroViews": "此月有讚/回覆,但瀏覽為 0(Insights 可能尚未回傳或權限不足)。", + "insights.narrative.highlight": "表現較佳之一:{snippet}", + "insights.narrative.smallSample": "該月貼文偏少,樣本小,月比僅供參考。", + + "plays.tabOwn": "我的貼文", + "plays.tabLink": "Threads 連結", + "plays.noPosts": "尚無貼文", + "plays.targetPost": "目標貼文", + "plays.likesSuffix": " (讚{n})", + "plays.linkCard": "貼 Threads 連結", + "plays.postLink": "貼文連結", + "plays.resolving": "解析中…", + "plays.resolve": "解析連結", + "plays.resolveHint": "解析後可排自家帳號在該則下面回覆。", + "plays.targetOwn": "目標貼文(自己的)", + "plays.openThreads": "開 Threads", + "plays.external": "外站貼", + "plays.addScheme": "新增方案", + "plays.schemeCount": "此目標目前 {n} 個方案", + "plays.noSchemes": "尚無方案", + "plays.replyCount": "{n} 則留言", + "plays.editTitle": "編輯:{title}", + "plays.schemeName": "方案名稱", + "plays.schemeNamePh": "例如:方案 A · 溫和接話", + "plays.speakersOwn": "可出場帳號(貼主帳固定可回)", + "plays.speakers": "可出場帳號", + "plays.postOwner": "(貼文主帳)", + "plays.noAccounts": "沒有可用帳號,請先到設定連線 Threads。", + "plays.interval": "間隔(分)", + "plays.applyInterval": "套用間隔", + "plays.aiEmpty": "空白則 AI", + "plays.aiBusy": "產文中…", + "plays.aiFail": "AI 產文失敗", + "plays.aiStepDone": "已產好此步,可再改", + "plays.aiNoneFilled": "沒有可產的空白步驟(或人設未就緒)", + "plays.needPersonaForStep": "此步請先選就緒人設,才能 AI 產文", + "plays.aiEmptyResult": "AI 回傳空白,請重試或換較快的模型", + "plays.saveBeforeAi": "請先「儲存方案」,再一次產全文(背景任務需要 play id)", + "plays.scriptJobQueued": "已排程一次產全文(背景任務,可離開;完成後步驟會自動填上)", + "plays.scriptJobDone": "劇本產文完成,已填入各步驟(可再改)", + "plays.scriptJobDoneReload": "劇本產文完成,請重新開啟方案查看", + "plays.replies": "留言({n})", + "plays.stepN": "第 {n} 則", + "plays.who": "誰留", + "plays.personaOpt": "人設(選填)", + "plays.brandOpt": "品牌(選填)", + "plays.reply": "留言", + "plays.attach": "附圖", + "plays.addOne": "加一則", + "plays.saving": "儲存中…", + "plays.saveScheme": "儲存方案", + "plays.submitting": "送出中…", + "plays.submitOutbox": "送出到 Outbox", + "plays.closeEdit": "關閉編輯", + "plays.noTarget": "還沒有目標貼文", + "plays.resolved": "已解析連結", + "plays.resolveFail": "解析失敗", + "plays.filled": "已產 {n} 則", + "plays.needTarget": "請先選定目標貼文", + "plays.saved": "方案已儲存", + "plays.saveFail": "儲存失敗", + "plays.submitted": "已送進 Outbox", + "plays.submitFail": "送出失敗", + "plays.confirmDelete": "刪除此方案?", + "plays.accountFallback": "帳號", + + "inspire.loading": "載入中…", + "inspire.trendsAria": "話題靈感", + "inspire.trendsLabel": "話題靈感", + "inspire.trendsHint": "網搜彙整,非官方熱搜 · 找靈感會扣搜尋點數", + "inspire.trendsSeed": "示意", + "inspire.topicSeed": "想寫跟「{topic}」有關的 Threads,幫我發想開場與角度。", + "inspire.trendsEmpty": "點「找靈感」才會搜尋(扣點)", + "inspire.refreshConfirm": "找靈感會消耗 1 次「搜尋」點數,確定?", + "inspire.refreshOk": "已更新 {n} 則話題靈感", + "inspire.refreshFail": "找靈感失敗(點數不足或搜尋失敗)", + "inspire.refresh": "找靈感", + "inspire.clearChat": "新對話", + "inspire.clearedNewSession": "已開新對話", + "inspire.sessionsAria": "靈感對話列表", + "inspire.session": "對話", + "inspire.sessionNew": "新對話", + "inspire.newSession": "+ 新對話", + "inspire.newSessionOk": "已開新對話(舊的還在列表)", + "inspire.deleteSession": "刪除目前對話", + "inspire.deleteSessionShort": "刪除", + "inspire.confirmDeleteSession": "刪除目前對話?此則聊天會永久移除。", + "inspire.deletedSession": "已刪除,已切到其他對話", + "inspire.pinAsElement": "套用為元素", + "inspire.you": "你", + "inspire.ai": "AI", + "inspire.system": "系統", + "inspire.useDraft": "用這則寫", + "inspire.openPlay": "開串場", + "inspire.thinking": "思考中…", + "inspire.stop": "停止產生", + "inspire.stopped": "已停止產生", + "inspire.pinnedAria": "本輪參考(給 AI 看)", + "inspire.pinned": "本輪參考", + "inspire.pinnedCount": "· {n}", + "inspire.pinsLocalShort": "本次工作階段", + "inspire.pinsSessionLocal": "參考項目只套用於本次工作階段;送出訊息時會一併帶入。", + "inspire.pickRight": "右側點選=掛給 AI;點名稱可插入輸入", + "inspire.unpinTitle": "取消參考", + "inspire.insertPinTitle": "插入主輸入", + "inspire.insertBrand": "聊聊「{name}」", + "inspire.insertedPin": "已插入「{name}」到輸入框", + "inspire.flowStep1": "備料/參考", + "inspire.flowStep2": "聊天發想", + "inspire.flowStep3": "用人設定稿", + "inspire.emptyTitle": "先聊清楚,再用人設定稿", + "inspire.emptyDesc": "選一條路開始。不需要先懂全部按鈕。", + "inspire.entryTopic": "從一句話開始發想", + "inspire.entryPaste": "已有草稿,直接用人設改寫", + "inspire.startTopicHint": "在下方輸入主題或想法,Enter 送出", + "inspire.pasteDraftHint": "把草稿貼進「待改寫內容」,再按用人設改寫", + "inspire.needMaterialOrPaste": "沒有聊天素材時,請直接貼上要改寫的文字", + "inspire.flowOneLiner": "先聊清楚;需要資料時開啟查資料,最後一鍵整理成貼文。", + "inspire.showTopics": "找題材", + "inspire.hideTopics": "收起題材", + "inspire.showLibrary": "參考庫", + "inspire.hideLibrary": "收起庫", + "inspire.showAdvanced": "進階", + "inspire.hideAdvanced": "收起進階", + "inspire.readyToWrite": "已聊 {n} 輪,可以定稿了", + "inspire.inputAria": "跟 AI 說", + "inspire.inputPh": "想發想什麼?Enter 送出 · Shift+Enter 換行", + "inspire.send": "送出", + "inspire.generate": "整理成貼文", + "inspire.generateHint": "根據整段對話、人設聲紋與高互動寫法整理成可發布正文", + "inspire.webSearch": "查資料", + "inspire.webSearchOn": "查資料:開", + "inspire.webSearchHint": "開啟後,下一則訊息會先用 Exa 查資料再交給 AI 討論", + "inspire.needConversation": "先聊一句你的想法,再整理成貼文。", + "inspire.needReadyPersona": "請先選擇已完成分析的人設。", + "inspire.generating": "改寫中…", + "inspire.generateOk": "已依人設改寫成草稿", + "inspire.materialTitle": "鎖定要寫的內容", + "inspire.materialHint": "產文只會改寫這段(可編輯),不會另起新主題。聊天負責發想,這裡負責定稿。", + "inspire.materialLabel": "待改寫內容", + "inspire.materialPh": "從對話整理出的重點、角度、想講的事…", + "inspire.rewriteNotes": "改寫指示(可選)", + "inspire.rewriteNotesPh": "例如:短一點、更口語、加問句", + "inspire.rewriteDefault": "用人設寫成 Threads 正文", + "inspire.confirmRewrite": "用人設改寫", + "inspire.needMaterial": "先聊出一些內容,或在素材框貼上要改寫的文字", + "inspire.library": "元素庫", + "inspire.addNew": "+ 新增", + "inspire.kind": "類型", + "inspire.kindRole": "角色指令", + "inspire.kindSnippet": "片段", + "inspire.kindTrendNote": "熱點備註", + "inspire.kindBrand": "品牌", + "inspire.kindTrend": "熱點", + "inspire.name": "名稱", + "inspire.namePh": "例如:專業 Threads 寫手", + "inspire.body": "內容(會進 prompt)", + "inspire.bodyPh": "你是一位…", + "inspire.saveElement": "存進元素庫", + "inspire.citeBrand": "引用品牌", + "inspire.applied": "已套用", + "inspire.clickApply": "點擊套用", + "inspire.noBrands": "尚無品牌", + "inspire.appliedToggle": "已套用 · 再點取消", + "inspire.deleteAria": "刪除", + "inspire.needInput": "先輸入你想聊的方向", + "inspire.fail": "失敗", + "inspire.wantWrite": "想寫關於 {label}:{summary}", + "inspire.trendBody": "主題:{label}。{summary}", + "inspire.pinnedTrend": "已套用熱點 {label}", + "inspire.needTitleBody": "請填名稱與內容", + "inspire.added": "已加入元素庫", + "inspire.addFail": "新增失敗", + "inspire.confirmRemove": "從元素庫移除此項?", + "inspire.confirmClear": "開新對話?(舊對話會保留在列表)", + "inspire.genMessage": "用人設寫成 Threads 正文", + "inspire.previewTitle": "本輪會送出的完整內容", + "inspire.previewPrompt": "完整 prompt(人設/品牌產品/元素/對話,與送 AI 相同)", + "inspire.previewSections": "已帶入段落", + "inspire.previewPinnedCount": "套用元素 {n} 個", + "inspire.previewNoPins": "目前沒有套用元素(右側可點選)", + "inspire.runes": "字", + "inspire.copyAll": "複製全文", + "inspire.copied": "已複製完整 prompt", + "inspire.copyFail": "複製失敗", + "inspire.rawPrompt": "送給 AI 的原文(一字不差)", + "inspire.verifyHow": "怎麼確認一致:先按 ? 看 fingerprint → 不改內容直接送出 → 狀態列顯示「與預覽一致」。", + "inspire.verifyHowShort": "先輸入文字 → 按 ? → 不改內容按送出 → 應一致。對話不會整包重送:只帶前情摘要 + 最近幾則。", + "inspire.previewModeNote": "此為 mode={mode} 的實際組裝(與同 mode 送出相同)。", + "inspire.lastSentFp": "剛送出指紋", + "inspire.matchOk": "與上次送出一致 ✓", + "inspire.matchBad": "與預覽不一致(輸入/pin/人設/mode 不同)", + "inspire.matchBadShort": "≠ 上次送出 {sent}", + "inspire.fpMatch": "送出指紋 {fp} 與預覽一致", + "inspire.fpMismatch": "指紋不同:預覽 {preview} ≠ 送出 {sent}(是否改過字/mode?)", + "inspire.fpSent": "送出指紋 {fp}", + "inspire.viewSent": "看剛送出的全文", + "inspire.viewSentShort": "剛送", + "inspire.sentPrompt": "剛送出的完整 prompt", + "inspire.sentPromptNote": "後端實際丟給 AI 的 prompt(stream done 回傳)。", + + "persona.add": "新增人設", + "persona.empty": "尚無人設", + "persona.emptyDesc": "新增後做分析即可用於產文。", + "persona.statusReady": "ready", + "persona.statusAnalyzing": "分析中", + "persona.statusPending": "待分析", + "persona.default": "預設", + "persona.backList": "← 人設列表", + "persona.tabOverview": "概要", + "persona.tabAnalyze": "分析", + "persona.tabFingerprint": "指紋", + "persona.tabPreview": "試產", + "persona.name": "名稱", + "persona.brief": "定位 brief", + "persona.briefPh": "是誰、對誰說、核心訊息…", + "persona.avoid": "護欄 · 禁止詞(逗號分隔)", + "persona.guardChars": "{n} 字", + "persona.banAi": " · 禁 AI 腔", + "persona.notReadySuffix": " · 未就緒", + "persona.setDefault": "設為預設", + "persona.modeAccount": "公開帳號", + "persona.modeText": "貼文字", + "persona.username": "Threads username", + "persona.fromBound": "從已綁帳號帶入", + "persona.select": "選擇…", + "persona.crawlAnalyze": "分析公開貼文", + "persona.crawlBusy": "分析中…", + "persona.refText": "參考文字(--- 分隔多篇)", + "persona.refTextPh": "第一段…\n\n---\n\n第二段…", + "persona.sourceLabel": "來源說明(選填)", + "persona.sourcePh": "自己的舊文", + "persona.analyzeText": "從文字分析", + "persona.analyzeBusy": "分析中…", + "persona.sampleMeta": "樣本 {n}", + "persona.sourceManual": "貼文", + "persona.analyzeHint": "完成分析後會顯示 8D 摘要。", + "persona.fingerprintHint": "產文主體。可改口頭禪、節奏、禁忌;儲存後 Studio/回覆會吃這份。", + "persona.fingerprint": "語言指紋", + "persona.fingerprintPh": "分析後自動填入…", + "persona.saveFingerprint": "儲存指紋", + "persona.tryGen": "試產主貼 + 回覆", + "persona.previewHint": "依目前指紋寫一則可能的主貼與回文;會嘗試用即時新聞當話題靈感(轉成這個人的口吻,不是新聞稿)。", + "persona.previewRunning": "產文中…", + "persona.previewDone": "試產完成 · 話題:{topic}({source})", + "persona.previewFail": "試產失敗,請再試一次", + "persona.previewTopicLabel": "話題靈感:{topic} · {source}", + "persona.topicNews": "即時新聞", + "persona.topicManual": "手動", + "persona.topicFallback": "生活靈感", + "persona.notReadyMsg": "人設未就緒", + "persona.rootPost": "主貼", + "persona.reply": "回覆", + "persona.hidePrompt": "隱藏 prompt block", + "persona.showPrompt": "顯示注入的 prompt", + "persona.promptBlock": "prompt block(post)", + "persona.pickOne": "選一個人設", + "persona.pickDesc": "或按新增開始分析。", + "persona.created": "已建立,請到「分析」完成帳號分析或貼文字", + "persona.saved": "已儲存", + "persona.textDone": "文字分析完成 · {n} 段 → ready", + "persona.analyzeFail": "分析失敗", + "persona.reading": "正在讀取公開貼文…", + "persona.accountDone": "@{user} · {n} 則 → ready", + "persona.jobQueued": "已排入背景任務 · 可離開此頁,完成後自動存檔", + "persona.jobQueuedCrawl": "已排入分析任務 · 可離開此頁,完成後自動寫入人設", + "persona.jobRunning": "背景分析中… 完成後會自動更新(也可到「任務」查看進度)", + "persona.jobDone": "背景分析完成 · 已寫入指紋/範本", + "persona.jobFailed": "背景分析失敗,請到任務頁查看錯誤或重試", + "persona.loadFail": "載入人設失敗", + "persona.openJob": "開啟任務詳情", + "persona.setDefaultMsg": "「{name}」已設為預設", + "persona.confirmDelete": "確定刪除人設「{name}」?", + "persona.deleted": "人設已刪除", + "persona.needReady": "請先完成分析(ready)", + "persona.dim.d1Tone": "D1 語氣人格", + "persona.dim.d2Structure": "D2 結構模板", + "persona.dim.d3Interaction": "D3 互動方式", + "persona.dim.d4Topics": "D4 主題分布", + "persona.dim.d5Rhythm": "D5 發文節奏", + "persona.dim.d6Visual": "D6 視覺語法", + "persona.dim.d7Conversion": "D7 轉換方式", + "persona.dim.d8Risk": "D8 風險紅線", + + "admin.users.loadFail": "載入失敗", + "admin.users.created": "已新增島民「{name}」· 請複製下方密碼", + "admin.users.createFail": "新增失敗", + "admin.users.unlimitedOn": "「{name}」已設不擋額度(用量仍計算)", + "admin.users.unlimitedOff": "「{name}」已改回依方案擋額度", + "admin.users.updateFail": "更新失敗", + "admin.users.planSet": "「{name}」方案 → {plan}", + "admin.users.confirmSuspend": "確定停權「{name}」?\\n停權後無法登入。", + "admin.users.confirmUnsuspend": "確定復權「{name}」?\\n復權後可重新登入。", + "admin.users.didSuspend": "已停權「{name}」", + "admin.users.didUnsuspend": "已復權「{name}」", + "admin.users.suspendFail": "停權失敗", + "admin.users.unsuspendFail": "復權失敗", + "admin.users.markedVerified": "已將 {name} 標為信箱已驗證", + "admin.users.markedUnverified": "已將 {name} 標為未驗證", + "admin.users.rolesUpdated": "已更新 {name} 的權限:{roles}", + "admin.users.rolesFail": "權限更新失敗", + "admin.users.confirmReset": "確定幫「{name}」重設密碼?\\n臨時密碼會固定顯示直到你按關閉(可重整)。", + "admin.users.resetDone": "已重設 {name} 的密碼(下方可持續顯示,請複製後再關閉)", + "admin.users.resetFail": "重設失敗", + "admin.users.copied": "已複製到剪貼簿", + "admin.users.copyFail": "複製失敗,請手動選取密碼", + "admin.users.confirmDismissTemp": "關閉後此頁將不再顯示這組臨時密碼(若尚未複製請先複製)。確定關閉?", + "admin.users.tempPwNew": "新島民臨時密碼", + "admin.users.tempPw": "臨時密碼", + "admin.users.tempPwPersist": "(持續顯示 · 可重整)", + "admin.users.copyPw": "複製密碼", + "admin.users.close": "關閉", + "admin.users.createTitle": "新增島民", + "admin.users.memberName": "島民名稱", + "admin.users.displayNamePh": "顯示名稱", + "admin.users.email": "Email", + "admin.users.initPassword": "初始密碼(選填)", + "admin.users.initPasswordPh": "空白則自動產生;若填寫須符合密碼政策", + "admin.users.markVerifiedCheck": "信箱標為已驗證(可直接使用)", + "admin.users.alsoAdmin": "同時設為管理員", + "admin.users.creating": "建立中…", + "admin.users.createSubmit": "建立島民", + "admin.users.clear": "清除", + "admin.users.searchActive": "搜尋「{query}」· 可匹配名稱、Email、uid", + "admin.users.noMatch": "無符合", + "admin.users.none": "尚無島民", + "admin.users.you": "這是你", + "admin.users.status": "狀態", + "admin.users.role": "角色", + "admin.users.emailVerify": "信箱驗證", + "admin.users.bio": "簡介", + "admin.users.timezone": "時區", + "admin.users.notifyEmail": "Email 通知", + "admin.users.on": "開", + "admin.users.off": "關", + "admin.users.createdAt": "建立", + "admin.users.updatedAt": "更新", + "admin.users.accountStatus": "帳號狀態", + "admin.users.updating": "更新中…", + "admin.users.usageTitle": "用量與方案", + "admin.users.usageLiveSkip": "方案/額度屬 Usage 域,live 後端尚未接上(M3);此區僅 mock 可改。", + "admin.users.plan": "方案", + "admin.users.planOption": "{name}({credits} 點/月)", + "admin.users.unlimited": "不擋額度", + "admin.users.byPlan": "依方案", + "admin.users.setUnlimited": "設為不擋額度", + "admin.users.setLimited": "改回擋額度", + "admin.users.unlimitedHint": "不擋額度:達方案上限仍可繼續用;AI/Search 次數與點數照樣計算。", + "admin.users.loadingUsage": "載入用量設定…", + "admin.users.assignRoles": "指派權限", + "admin.users.memberBase": "{role}(基底,不可關閉)", + "admin.users.adminDesc": "{role} — 可管理島民與系統", + "admin.users.saving": "儲存中…", + "admin.users.saveRoles": "儲存權限", + "admin.users.markUnverifiedBtn": "標為未驗證", + "admin.users.markVerifiedBtn": "標為已驗證", + "admin.users.resetting": "重設中…", + "admin.users.resetTemp": "重設密碼(產生臨時)", + "admin.users.customPw": "或指定新密碼(選填)", + "admin.users.customPwPh": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", + "admin.users.resetWithCustom": "用指定密碼重設", + + "usage.tabMine": "我的用量", + "usage.tabTenant": "全體用量", + "usage.currentPlan": "目前方案", + "usage.planMeta": "/月 · 每月 {n} 點額度", + "usage.changePlan": "變更方案", + "usage.upgradePlan": "升級方案", + "usage.outcome.title": "本月成果", + "usage.outcome.summary": "觸達 {reach} · 對話 {conversations} · 成交 {conversions}", + "usage.outcome.amount": "(約 ${amount})", + "usage.outcome.emptyHint": "本月還沒有可歸因的成果,去海巡或發文試試。", + "usage.warn.unlimitedOver": "本月已用 {used} 點(方案 {cap},不擋額度,仍可繼續)。", + "usage.warn.exhausted": "本月點數已用完。可升級方案或等待下月重置。", + "usage.warn.high": "本月已用 {pct}% 點數。", + "usage.warn.meterNear": "{label} 接近單項上限({credits}/{cap} 點)。", + "usage.usedThisMonth": "本月已用", + "usage.remainLabel": "剩餘", + "usage.ledgerToggle": "使用紀錄", + "usage.collapse": "收合", + "usage.eventsCount": "{n} 筆", + "usage.granularity": "粒度", + "usage.day": "日", + "usage.monthUnit": "月", + "usage.year": "年", + "usage.from": "起", + "usage.to": "迄", + "usage.callCounts": "呼叫次數", + "usage.noMembers": "尚無會員", + "usage.planAria": "{name} 方案", + "usage.unlimitedTitle": "不擋額度", + "usage.setLimited": "改回擋額度", + "usage.setUnlimited": "設為不擋額度", + "usage.limitShort": "擋", + "usage.subscribed": "已訂閱 {name}", + "usage.unlimitedSet": "已設不擋額度", + "usage.limitedSet": "已改回擋額度", + "usage.planUpdated": "已更新方案 {name}", + "usage.fail": "失敗", + + "currency.TWD": "新台幣 (TWD)", + "currency.USD": "美元 (USD)", + "currency.JPY": "日圓 (JPY)", + "currency.EUR": "歐元 (EUR)", + "currency.HKD": "港幣 (HKD)", + + "locale.zh-TW": "繁體中文", + "locale.en": "English", + + "pager.nav": "分頁", + "pager.pageSize": "每頁筆數", + "pager.perPage": "{n}/頁", + "pager.prev": "上一頁", + "pager.next": "下一頁", + + "plays.defaultTitle": "新方案", + "plays.topicOnPost": "掛在:{snippet}", + "plays.topicOnExternal": "掛在:{label} · {snippet}", + "plays.externalFallback": "外站貼", + + "persona.newName": "新人設", + "persona.previewTopic": "週末想找能坐久的咖啡店", + "persona.previewReplySample": "大安那間還行但人很多", + + "inspire.playTitle": "靈感串場", + + "play.err.needLead": "請選擇主帳號", + "play.err.needRoot": "請至少有一則主貼", + "play.err.firstMustRoot": "第一則必須是主貼", + "play.err.rootMustLead": "主貼必須使用主帳", + "play.err.rootEmpty": "主貼文案不可空白", + "play.err.replyAccount": "回覆只能用主帳或已選配角", + "play.err.replyEmpty": "回覆文案不可空白", + "play.err.needTarget": "請選擇自己的貼文,或貼上 Threads 連結", + "play.err.needReplies": "請至少排 1 則留言", + "play.err.needReplyAccounts": "請至少選一個可回覆帳號", + "play.err.stepAccount": "每則留言都要指定可用帳號", + "play.err.stepEmpty": "留言內容不可空白", + "play.err.notFound": "找不到互回方案", + + "time.justNow": "剛剛", + "time.minAgo": "{n} 分前", + "time.hourAgo": "{n} 小時前", + "time.dayAgo": "{n} 天前", + "time.min": "{n} 分鐘", + "time.hour": "{n} 小時", + "time.day": "{n} 天", + "time.expired": "已過期 {span}", + "time.remaining": "剩餘 {span}", + "time.sessionUnknown": "未記錄到期時間", + "time.sessionExpired": "Token 已過期 · {absolute}", + "time.sessionSoon": "即將到期 · {relative}({absolute})", + "time.sessionOk": "有效 · {relative}({absolute})", + + // demand-radar:服務檔案(每會員一份,判定與回覆都讀它) + "policy.title": "商機與回覆政策", + "policy.subtitle": "設定服務範圍、禁語、案例、FAQ 與口吻;商機判定和生成回覆會共用這份政策。", + "radar.profile.title": "服務檔案", + "radar.profile.subtitle": "雷達用這份資料判斷商機是否值得回,回覆也照這裡的價格與口吻寫。", + "radar.profile.firstTimeHint": "先填服務檔案,才能開啟雷達訂閱。內容越具體,判定與回覆越準。", + "radar.profile.updatedAt": "上次更新:{at}", + "radar.profile.saved": "服務檔案已儲存", + "radar.profile.services": "服務與價格", + "radar.profile.servicesHint": "至少填一項。價格留空代表面議,填了就會出現在回覆裡。", + "radar.profile.serviceName": "服務名稱", + "radar.profile.serviceNamePh": "例:室內設計丈量規劃", + "radar.profile.priceMin": "價格下限", + "radar.profile.priceMax": "價格上限", + "radar.profile.addService": "+ 新增服務", + "radar.profile.areas": "服務區域", + "radar.profile.areasHint": "選你實際接得到的縣市;地區不合的商機會被降分。可遠端就勾下面那項。", + "radar.profile.remoteOk": "可遠端服務(不限地區)", + "radar.profile.forbidden": "禁語", + "radar.profile.forbiddenHint": "一行一條。這些字不會出現在任何生成的回覆裡。", + "radar.profile.forbiddenPh": "保證\n最便宜\n第一名", + "radar.profile.cases": "案例", + "radar.profile.casesHint": "選填。回覆需要舉證時會引用,沒有就不引用。", + "radar.profile.caseTitle": "案例標題", + "radar.profile.caseSummary": "一句話說明", + "radar.profile.addCase": "+ 新增案例", + "radar.profile.faq": "常見問答", + "radar.profile.faqHint": "選填。對方問到類似問題時,回覆會照這裡的答案講。", + "radar.profile.faqQuestion": "問題", + "radar.profile.faqAnswer": "回答", + "radar.profile.addFaq": "+ 新增問答", + "radar.profile.availability": "可接案時間", + "radar.profile.availabilityHint": "例:兩週內可開工、只接週末", + "radar.profile.toneNote": "口吻備註", + "radar.profile.toneNoteHint": "例:講話直接不客套、不用驚嘆號", + + // demand-radar:商機訂閱(每日自動;對照海巡=手動掃場) + "radar.watches.title": "商機訂閱", + "radar.watches.subtitle": "訂好關鍵字後每天自動巡,整理成「今日商機」。和海巡「按一次掃一輪」不同。", + "radar.watches.needProfile": "先填服務檔案,才能開商機訂閱", + "radar.watches.needProfileHint": "系統靠服務檔案判斷需求適不適合你;沒有它會誤判。", + "radar.watches.goProfile": "去填服務檔案", + "radar.watches.quota": "啟用中 {used} / {max}", + "radar.watches.quotaFull": "已達方案上限,要新增請先暫停或封存一個", + "radar.watches.add": "+ 新增訂閱", + "radar.watches.newTitle": "新增商機訂閱", + "radar.watches.editTitle": "編輯商機訂閱", + "radar.watches.requiredHint": "為必填欄位", + "radar.watches.terms": "關鍵字", + "radar.watches.termsHint": "一行一個短詞。每組最多 2 詞、中文每詞 2–4 字,才能在 Threads 搜到;長句儲存時會自動收成短詞。", + "radar.watches.threadsWarn": "有關鍵字不合 Threads 短詞規則:每組最多 2 詞、中文每詞 2–4 字、整組 ≤12 字、勿用標點/#/emoji。儲存時會自動收成可搜短詞。", + "radar.watches.threadsRequired": "這些關鍵字收不成 Threads 可搜的短詞。請改成每組最多 2 詞、中文每詞 2–4 字。", + "radar.watches.termsPh": "室內設計\n找設計師", + "radar.watches.excludeTerms": "排除詞", + "radar.watches.excludeHint": "命中這些字就整筆跳過,例如同業自我推銷、抽獎文。", + "radar.watches.excludePh": "徵才\n抽獎", + "radar.watches.regionsHint": "地區留空=沿用服務檔案的服務區域;只有這個訂閱要縮小範圍時才選。", + "radar.watches.regionsFromProfile": "地區沿用服務檔案", + "radar.watches.enableNow": "建立後立刻啟用(會佔用啟用配額)", + "radar.watches.filterStatus": "狀態", + "radar.watches.statusAll": "全部", + "radar.watches.status.active": "啟用中", + "radar.watches.status.paused": "已暫停", + "radar.watches.status.archived": "已封存", + "radar.watches.pause": "暫停", + "radar.watches.resume": "恢復", + "radar.watches.archive": "封存", + "radar.watches.confirmArchive": "封存後不會再巡,也不能恢復。要繼續嗎?", + "radar.watches.deleteArchived": "刪除封存設定", + "radar.watches.deletingArchived": "刪除中…", + "radar.watches.confirmDeleteArchived": "確定永久刪除這個封存巡邏設定嗎?既有商機、巡邏紀錄與統計會保留,但這個設定之後不會再出現在列表。", + "radar.watches.deletedArchived": "封存巡邏設定已刪除", + "radar.watches.created": "訂閱已建立", + "radar.watches.createdFirstSweep": "訂閱已建立,首巡已排入,約幾分鐘後回今日商機頁看結果(之後每天自動巡,不用再等)", + "radar.watches.updated": "訂閱已更新", + "radar.watches.paused": "訂閱已暫停", + "radar.watches.resumed": "訂閱已恢復", + "radar.watches.archived": "訂閱已封存", + "radar.watches.lastSwept": "上次巡:{at}", + "radar.watches.neverSwept": "還沒巡過", + "radar.watches.empty": "還沒有商機訂閱", + "radar.watches.emptyHint": "加客人會用的短詞(例如「室內設計」「找設計師」),系統會每天自動幫你巡。若要現在手動掃痛點/話題,用側欄「海巡」。", + "radar.watches.emptyFiltered": "這個狀態下沒有訂閱", + "radar.watches.scheduleTitle": "每日定時巡邏:每天台北 06:00(UTC 22:00)", + "radar.watches.scheduleHint": "開著的訂閱每天自動巡一輪。要現在看結果,按「立即巡邏」。關掉立即巡邏不會停每日定時。", + "radar.watches.openToday": "回商機結果", + "radar.watches.sweepNow": "立即巡邏", + "radar.watches.sweepQueued": "已排入商機巡檢", + "radar.watches.sweepStarted": "商機巡檢已開始(任務 {job}…)", + + "today.radar.title": "今日商機", + "today.radar.total": "找到", + "today.radar.high": "高", + "today.radar.mid": "中", + "today.radar.low": "低", + "today.radar.open": "查看今日商機", + "today.radar.empty": "還沒有今日商機。訂閱關鍵字後會每天自動更新(與海巡手動掃場不同)。", + "today.radar.goProfile": "先填服務檔案", + "today.radar.goWatches": "去訂閱關鍵字", + + "firstRun.title": "先連一個 Threads 帳號", + "firstRun.subtitle": "連上之後就能用其他功能。點按鈕去帳號頁連接。", + "firstRun.skip": "略過,之後自己摸", + "firstRun.go": "去連接", + "firstRun.progress": "第 {current} 步/共 {total} 步", + "firstRun.done": "完成", + "firstRun.aria": "第一次設定", + "firstRun.step.crew": "連 Threads 帳號", + "firstRun.step.crewHint": "之後才能一鍵送出回覆。", + "firstRun.step.brands": "整理品牌與產品", + "firstRun.step.brandsHint": "填受眾、痛點與產品能力。", + "firstRun.step.watch": "建立每日巡邏", + "firstRun.step.watchHint": "選產品後採用建議關鍵字並儲存。", + "firstRun.step.radar": "看第一筆商機", + "firstRun.step.radarHint": "不必等隔天,可按「立即探索」。", + "firstRun.status.pending": "引導中", + "firstRun.status.skipped": "已略過引導", + "firstRun.status.completed": "已完成引導", + + "radar.suggest.title": "關鍵字建議", + "radar.suggest.hint": "依你的服務檔案想幾個客人真的會打的字,逐條或全部採用;採用後仍要按儲存才會建立。", + "radar.suggest.ask": "取得建議", + "radar.suggest.again": "再想幾個", + "radar.suggest.asking": "想關鍵字中…", + "radar.suggest.adopt": "採用", + "radar.suggest.adopted": "已採用", + "radar.suggest.adoptAll": "全部採用", + "radar.suggest.include": "關鍵字", + "radar.suggest.exclude": "排除詞", + "radar.suggest.none": "這次沒想出可用的字,請把服務檔案寫具體一點再試。", + + // demand-radar:今日商機(自動名單) + "radar.today.title": "今日商機", + "radar.today.subtitle": "訂閱後每天自動整理的需求名單(不是海巡那一輪手動掃)。", + "radar.today.link.watches": "商機訂閱", + "radar.today.link.crm": "名單看板", + "radar.today.stats.total": "今日找到", + "radar.today.stats.high": "高意向", + "radar.today.stats.mid": "中意向", + "radar.today.stats.low": "低意向", + "radar.today.truncated": "已達今日上限,{n} 筆較低意向未收錄", + "radar.today.lastSwept": "上次巡檢:{at}", + "radar.today.band.high": "高", + "radar.today.band.mid": "中", + "radar.today.band.low": "低", + "radar.today.status.accepted": "已加入名單", + "radar.today.status.dismissed": "已略過", + "radar.today.status.qualified": "待處理", + "radar.today.status.rejected": "已否決", + "radar.today.status.judging": "判定中", + "radar.today.regionUnknown": "地區不明", + "radar.today.group.high": "高意向", + "radar.today.group.mid": "中意向", + "radar.today.group.low": "低意向", + "radar.today.group.empty": "這一組目前沒有", + "radar.today.group.expand": "展開", + "radar.today.group.collapse": "收合", + "radar.today.action.open": "原文", + "radar.today.action.accept": "加入名單", + "radar.today.action.dismiss": "略過", + "radar.today.action.reply": "產生回覆", + "radar.today.action.hideReply": "收合回覆", + "radar.today.action.reasons": "判定理由", + "radar.today.action.hideReasons": "收合理由", + "radar.today.action.override": "覆寫分級", + "radar.today.reply.hint": "選一個版本產生草稿;私訊版只提供複製,不會自動送出。", + "radar.today.reply.copy": "複製草稿", + "radar.today.reply.variant.public_comment": "公開留言", + "radar.today.reply.variant.dm": "私訊", + "radar.today.reply.variant.no_sales": "不銷售", + "radar.today.reply.variant.professional": "專業", + "radar.today.reply.variant.humorous": "輕鬆", + "radar.today.dim.authenticity": "真實性", + "radar.today.dim.intent": "意圖", + "radar.today.dim.region": "地區", + "radar.today.dim.freshness": "新鮮度", + "radar.today.dim.fit": "服務匹配", + "radar.today.empty.title": "目前沒有今日商機", + "radar.today.empty.fallback": "稍後再回來,或先檢查商機訂閱與服務檔案。", + "radar.today.empty.goProfile": "去填服務檔案", + "radar.today.empty.goWatches": "去訂閱關鍵字", + "radar.today.empty.goAll": "查看全部結果", + "radar.today.empty.reason.no_profile": "還沒有服務檔案,無法判定需求適不適合你。", + "radar.today.empty.reason.no_watch": "還沒有商機訂閱;建立關鍵字後才會每天自動巡(不是海巡那一輪手動掃)。", + "radar.today.empty.reason.all_watches_paused": "訂閱都暫停了,恢復一組才會繼續自動巡。", + "radar.today.empty.reason.not_swept_yet": "每日定時還沒跑完,也可在商機頁按「立即巡邏」。", + "radar.today.empty.reason.sweep_failed": "這輪自動巡失敗,請到商機訂閱頁查看或重試。", + "radar.today.empty.reason.no_hit": "有巡但沒有符合的需求,可放寬關鍵字或排除詞。", + "radar.today.msg.accepted": "已加入名單", + "radar.today.msg.dismissed": "已略過", + "radar.today.msg.replyReady": "回覆草稿已產生", + "radar.today.msg.overridden": "分級已更新", + "radar.today.msg.copied": "已複製到剪貼簿", + "radar.today.msg.copyFail": "無法複製,請手動選取文字", + "radar.today.msg.marked": "已標記為已送出/已複製", + "radar.today.msg.sent": "已送出,稍後可在發送佇列查看進度", + "radar.today.msg.needReply": "請先產生回覆草稿", + "radar.today.sendAccount": "送出帳號", + "radar.today.reply.markCopy": "標記已複製送出", + "radar.today.reply.markOutbox": "一鍵送出(Outbox)", + "radar.today.reply.needAccount": "先連一個 Threads 帳號才能一鍵送出", + "radar.today.reply.used": "已標記使用", + "radar.today.reply.usedOutbox": "已送出(可在發送佇列查看)", + + "radar.reconnectSearch": "重新連線搜尋來源", + "radar.patrol.searchFallback": "搜尋來源暫時不可用時會改走備用通道;只有成功回傳才計點。", + "radar.empty.sweepFailedHint": "巡邏失敗。可再按立即巡邏,或改看近 7 天/全部。", + "radar.inbox.title": "商機", + "radar.inbox.patrolAria": "巡邏狀態", + "radar.inbox.scheduledOn": "每日定時巡邏:開著", + "radar.inbox.scheduledOff": "每日定時巡邏:關著", + "radar.inbox.scheduleHint": "每天台北 06:00 自動巡一輪。關掉立即巡邏不會停每日定時。", + "radar.inbox.lastSweep": "上次巡邏:{time}", + "radar.inbox.neverSwept": "還沒巡邏過", + "radar.inbox.activeWatches": "啟用中 {n} 組", + "radar.inbox.allPaused": "訂閱都暫停了,立即巡邏也需要至少一組開著", + "radar.inbox.noWatches": "還沒設定要巡的產品與關鍵字", + "radar.inbox.sweepNow": "立即巡邏", + "radar.inbox.sweeping": "巡邏中…", + "radar.inbox.sweepAgain": "再巡一次", + "radar.inbox.setupWatches": "設定巡邏", + "radar.inbox.introTitle": "巡邏到痛點就看這裡", + "radar.inbox.introBody": "先讀「為什麼推薦」,留下或丟掉即可。加入名單是可選的,不是看結果的必要步驟。", + "radar.inbox.resultsAria": "商機結果", + "radar.inbox.tabsAria": "結果狀態", + "radar.inbox.tab.pending": "新找到", + "radar.inbox.tab.completed": "已看過", + "radar.inbox.tab.removed": "已丟掉", + "radar.inbox.total": "共 {n} 筆", + "radar.inbox.clearFilters": "清除篩選", + "radar.inbox.defaultToday": "預設先看今天剛巡到的結果", + "radar.inbox.timeScope": "看哪段時間", + "radar.inbox.time.today": "今天", + "radar.inbox.time.7d": "近 7 天", + "radar.inbox.time.all": "全部", + "radar.inbox.sort": "先看哪些", + "radar.inbox.sort.recommended": "最對得上產品", + "radar.inbox.sort.newest": "最新貼文", + "radar.inbox.sort.oldest": "最舊貼文", + "radar.inbox.sort.productFit": "最符合產品", + "radar.inbox.sort.demandIntent": "需求最明確", + "radar.inbox.moreFilters": "更多篩選", + "radar.inbox.moreFiltersN": "更多篩選({n})", + "radar.inbox.hideFilters": "收起更多篩選", + "radar.inbox.moreFiltersAria": "更多篩選", + "radar.inbox.brand": "品牌", + "radar.inbox.allBrands": "全部品牌", + "radar.inbox.product": "產品", + "radar.inbox.allProducts": "全部產品", + "radar.inbox.band": "商機意向", + "radar.inbox.allBands": "全部意向", + "radar.inbox.band.high": "高意向", + "radar.inbox.band.mid": "中意向", + "radar.inbox.band.low": "低意向", + "radar.inbox.match": "產品匹配", + "radar.inbox.allStates": "全部狀態", + "radar.inbox.state.eligible": "可跟進", + "radar.inbox.state.weak": "弱適配", + "radar.inbox.state.excluded": "已排除", + "radar.inbox.state.generic": "未指定產品", + "radar.inbox.state.stale": "超過 14 天", + "radar.inbox.loading": "正在整理巡邏結果…", + "radar.inbox.prevPage": "上一頁", + "radar.inbox.nextPage": "下一頁", + "radar.inbox.pageOf": "第 {page} 頁/共 {pages} 頁", + "radar.inbox.goCrm": "前往名單", + "radar.inbox.see7d": "看近 7 天", + "radar.inbox.empty.filteredPending": "這個篩選下沒有結果", + "radar.inbox.empty.filteredCompleted": "目前沒有已看過的結果", + "radar.inbox.empty.filteredRemoved": "目前沒有已丟掉的結果", + "radar.inbox.empty.filteredHint": "清除篩選或改看其他時間範圍。巡邏剛跑完的結果也可能在「近 7 天」或「全部」。", + "radar.inbox.empty.noCompleted": "還沒有已看過的結果", + "radar.inbox.empty.noCompletedHint": "切回「新找到」繼續看巡邏到的痛點。", + "radar.inbox.empty.noRemoved": "還沒有丟掉的結果", + "radar.inbox.empty.noRemovedHint": "切回「新找到」繼續看巡邏到的痛點。", + "radar.inbox.empty.noWatchesTitle": "還沒設定巡邏", + "radar.inbox.empty.noWatchesHint": "先選產品與客人會搜的關鍵字。設好後可立即巡邏,每日定時巡邏也會接著跑。", + "radar.inbox.empty.pausedTitle": "每日定時巡邏關著", + "radar.inbox.empty.pausedHint": "立即巡邏與每日定時都還在這個頁面。恢復至少一組訂閱後,兩個都能用;關掉其中一個不會藏掉另一個。", + "radar.inbox.empty.openSchedule": "打開每日定時巡邏", + "radar.inbox.empty.neverTitle": "還沒巡邏過", + "radar.inbox.empty.neverHint": "每日定時巡邏已開著,也可現在按「立即巡邏」。不是空白收件匣,只是第一輪還沒跑完。", + "radar.inbox.empty.failedTitle": "上一輪巡邏沒跑完", + "radar.inbox.empty.noHitsTitle": "搜尋沒撈到貼文", + "radar.inbox.empty.noHitsHint": "門檻前就空了:關鍵字太長、太產品名、或 Threads 查無結果。改成客人會打的 2–4 字痛點詞再巡。", + "radar.inbox.empty.editTerms": "改關鍵字", + "radar.inbox.empty.noFitTitle": "這輪有巡,但沒找到符合的痛點", + "radar.inbox.empty.noFitStats": "搜尋命中 {hits}、判定 {judged}、新建 {created}。不是今天發的文可改看近 7 天/全部。", + "radar.inbox.empty.noFitHint": "新文章或產品對得上的需求會出現在這裡。也可改看「近 7 天」或「全部」,或調整要巡的關鍵字。", + "radar.inbox.msg.kept": "已留下。沒有建立名單。", + "radar.inbox.msg.removed": "已丟掉。可從「已丟掉」還原。", + "radar.inbox.msg.restored": "已還原。", + "radar.inbox.msg.accepted": "已加入名單。這步是可選的,之後要追蹤再去名單即可。", + "radar.inbox.msg.widened7d": "巡邏結果不是都在「今天發的文」。已改看近 7 天({n} 筆)。任務上的判定/新建數字包含同一篇再命中,不一定全是新卡片。", + "radar.inbox.msg.widenedAll": "近 7 天沒有待處理結果,已改看全部({n} 筆)。", + "radar.inbox.msg.alreadyReviewed": "這輪判定到的 {n} 筆已在「已看過」,所以「新找到」是空的。任務數字含再次命中的舊文。", + "radar.inbox.msg.waitingWorker": "立即巡邏已排程 · 等待 worker", + "radar.inbox.err.noActive": "沒有開著的每日巡邏。先設定要巡的產品與關鍵字,或恢復一組訂閱。", + "radar.inbox.err.noJob": "沒有排到巡邏任務。", + "radar.inbox.msg.running": "立即巡邏進行中… 跑完才會把痛點列在下面。", + "radar.inbox.err.failed": "巡邏失敗。", + "radar.inbox.err.cancelled": "巡邏已取消,不會誤顯示為已完成。可再按一次立即巡邏。", + "radar.inbox.msg.queuedN": "已排入 {n} 組巡邏,目前仍在後台執行。可先離開這頁,完成後結果會留在這裡。", + "radar.inbox.msg.doneWithSummary": "{summary} 不是今天發的文也會留在下面。", + "radar.inbox.msg.done": "這一輪巡邏跑完了。找到的痛點會留在下面。", + "radar.card.priority.high": "優先跟進", + "radar.card.priority.review": "值得確認", + "radar.card.priority.low": "低順位", + "radar.card.fitProduct": "適合產品 · {label}", + "radar.card.noProduct": "尚未配對產品", + "radar.card.intent": "需求意向 {n}", + "radar.card.why": "為什麼推薦:", + "radar.card.unknownAuthor": "未知作者", + "radar.card.openOriginal": "查看 Threads 原文", + "radar.card.actionsAria": "商機操作", + "radar.card.keep": "留下", + "radar.card.discard": "丟掉", + "radar.card.whyBtn": "為什麼推薦", + "radar.card.accept": "加入名單(可選)", + "radar.card.busy": "處理中…", + "radar.card.restore": "還原到待處理", + "radar.card.accepted": "已加入名單", + "radar.card.done": "已處理", + "radar.card.removeAria": "標示為不適合", + "radar.card.removeTitle": "為什麼丟掉?", + "radar.card.removeHint": "選原因後會移出「新找到」,之後巡邏不會再把同一篇推上來。這個動作不扣點。", + "radar.card.reason": "原因", + "radar.card.reason.pain_mismatch": "不符合產品痛點", + "radar.card.reason.provider_or_ad": "供應商/廣告貼文", + "radar.card.reason.stale": "需求已過期", + "radar.card.reason.already_solved": "對方已解決", + "radar.card.reason.duplicate": "重複商機", + "radar.card.reason.other": "其他原因", + "radar.card.note": "補充說明", + "radar.card.duplicateHint": "詳情中可指定要保留的原始商機。", + "radar.card.confirmRemove": "確認丟掉", + "radar.drawer.aria": "商機詳情", + "radar.drawer.title": "商機詳情", + "radar.drawer.noProduct": "未指定產品", + "radar.drawer.close": "關閉", + "radar.drawer.closeAria": "關閉商機詳情", + "radar.drawer.intent": "意向 {n}", + "radar.drawer.priority": "優先 {n}", + "radar.drawer.evidence": "需求證據", + "radar.drawer.matches": "產品匹配與風險", + "radar.drawer.generic": "尚未指定產品,這筆結果只保留為一般需求。", + "radar.drawer.judge": "原始判定", + "radar.drawer.openOriginal": "開啟 Threads 原文", + "radar.drawer.hint": "先看痛點與產品理由。留下或丟掉即可;加入名單只在你要追這個人時才需要。", + "radar.sweep.aria": "巡邏漏斗摘要", + "radar.sweep.title": "這次巡邏跑到哪裡", + "radar.sweep.hint": "不把合併匹配誤算成新增商機。", + "radar.sweep.status.complete": "完成", + "radar.sweep.status.partial_budget": "預算暫停", + "radar.sweep.status.blocked_budget": "點數不足", + "radar.sweep.status.failed": "失敗", + "radar.sweep.hits": "命中", + "radar.sweep.deduped": "去重", + "radar.sweep.prefilterPass": "前處理通過", + "radar.sweep.prefilterReject": "前處理排除", + "radar.sweep.cached": "快取判定", + "radar.sweep.aiJudge": "AI 判定", + "radar.sweep.created": "新增商機", + "radar.sweep.deferred": "預算延後", + "radar.sweep.credits": "點數:搜尋 {search} · 需求地圖 {map} · 判定 {judge}", + "radar.sweep.total": "合計 {n}", + "radar.sweep.budgetHint": "預算未使用的候選會保留,下次可續跑;不會重複扣已成功判定的筆數。", + "radar.cost.aria": "點數預覽與確認", + "radar.cost.title": "執行前先確認點數", + "radar.cost.hint": "預覽本身不扣點;只有 provider 成功回傳才會計入用量。", + "radar.cost.byok": "BYOK · 平台 0 點", + "radar.cost.platform": "平台點數", + "radar.cost.fixed": "固定", + "radar.cost.range": "預估範圍", + "radar.cost.calls": "搜尋呼叫", + "radar.cost.remaining": "剩餘", + "radar.cost.until": "預覽至 {time}", + "radar.cost.ceiling": "本次最高可用點數", + "radar.cost.ceilingHint": "至少 {min},最多 {max}", + "radar.cost.invalid": "點數上限必須落在固定成本至預估上限之間。", + "radar.cost.confirm": "確認並執行", + "radar.cost.starting": "啟動中…", + "radar.readiness.title": "產品資料完整度", + "radar.readiness.audience": "受眾", + "radar.readiness.context": "情境", + "radar.readiness.pain": "痛點", + "radar.readiness.capability": "能力詞", + "radar.readiness.ok": "已填", + "radar.readiness.todo": "待補", + "radar.readiness.hint": "資料不足會降低適配信心,但仍可建立產品型雷達。", + "radar.match.why": "為什麼適合", + "radar.match.hide": "收合證據", + "radar.match.basis": "產品依據:{text}", + "radar.match.risks": "風險:{text}", + "radar.today.empty.goBrands": "設定品牌與產品", + "radar.today.introTitle": "先看值得跟進的人,再決定怎麼回", + "radar.today.introBody": "系統會把 Threads 貼文和你的產品痛點比對、合併重複貼文,再依商機分數排序。", + "radar.today.navAria": "商機雷達導覽", + "radar.today.manageWatches": "管理每日巡邏", + "radar.today.viewAll": "查看全部結果", + "radar.today.filterAria": "篩選今日商機", + "radar.today.filterTitle": "篩選今日商機", + "radar.today.filterHint": "先看全部;結果多時再縮小到品牌或產品。", + "radar.today.fit": "產品適配", + "radar.today.allFit": "全部適配", + "radar.today.fit.strong": "高適配", + "radar.today.fit.possible": "可能", + "radar.today.fit.weak": "弱適配", + "radar.today.needMore": "沒有想看的貼文?", + "radar.today.setupAria": "第一次使用商機雷達", + "radar.today.setupTitle": "第一次使用,照這三步就好", + "radar.today.setupHint": "完成後系統會每天自動巡邏。", + "radar.today.setupStep": "目前第 {n} 步", + "radar.today.setup.1.title": "整理品牌與產品", + "radar.today.setup.1.body": "填入受眾、痛點與產品能力。", + "radar.today.setup.1.cta": "前往設定", + "radar.today.setup.2.title": "建立每日巡邏", + "radar.today.setup.2.body": "選產品後採用建議關鍵字。", + "radar.today.setup.2.cta": "建立巡邏", + "radar.today.setup.3.title": "回來處理商機", + "radar.today.setup.3.body": "先看高分,再查看產品證據。", + "radar.today.productEyebrow": "推薦產品", + "radar.today.noPrimary": "尚未指定主推產品", + "radar.today.fitScore": "適配 {n}", + "radar.today.overridden": "人工指定", + "radar.today.hideEvidence": "收合產品證據", + "radar.today.showMatches": "查看 {n} 個產品匹配", + "radar.today.genericJudge": "未指定產品(沿用通用商機判定)", + "radar.today.msg.primarySet": "已設定主推產品;後續高分匹配不會覆蓋這個選擇。", + + "radar.primary.empty": "目前沒有產品匹配", + "radar.primary.label": "主推產品", + "radar.primary.placeholder": "選擇產品", + "radar.primary.needsReason": " · 需理由", + "radar.primary.reasonAria": "主推理由", + "radar.primary.reasonPh": "可選:為何這次主推它", + "radar.primary.submit": "設定主推", + "radar.primary.defaultReason": "使用者依證據選定主推產品", + + "radar.watches.needBrandProduct": "請先選擇品牌與產品,產品型雷達才能啟用。", + "radar.watches.needDemandMap": "請先補齊產品需求地圖,再啟動產品型巡邏。", + "radar.watches.needBrandProductShort": "請先選擇品牌與產品。", + "radar.watches.assigned": "已補綁產品;之後不可在原訂閱更換產品。", + "radar.watches.assigning": "補綁中…", + "radar.watches.assign": "補綁這個產品", + "radar.watches.productGone": "產品已失效:請建立新訂閱", + "radar.watches.pickBrand": "請先選品牌", + "radar.watches.pickProduct": "請選這個品牌下的產品", + + "radar.explore.needProduct": "立即探索請先選擇品牌與產品,避免把通用結果誤當成產品商機。", + "radar.explore.pickBrand": "請選品牌", + "radar.explore.pickProduct": "請選產品", + "radar.explore.productStats": "新增商機 {created} · 合併產品匹配 {merged} · 評估 {matched}", + + "radar.import.needProduct": "手動匯入請先選擇品牌與產品。", + + "radar.suggest.basis": "依據:{text}", + + "radar.demand.title": "產品需求地圖", + "radar.demand.hint": "先確認產品真實痛點,再用它縮小巡邏結果。來源會保留在每個詞旁邊。", + "radar.demand.aria": "{label} 需求地圖", + "radar.demand.ready": "可用", + "radar.demand.incomplete": "待補資料", + "radar.demand.version": "版本 {n}", + "radar.demand.loading": "正在整理產品需求…", + "radar.demand.pain": "使用者痛點", + "radar.demand.painHint": "產品要解決的困擾,例如漏水、協作混亂", + "radar.demand.scenario": "使用情境", + "radar.demand.scenarioHint": "使用者會怎麼描述發生的情境", + "radar.demand.outcome": "期待結果", + "radar.demand.outcomeHint": "使用者想要的結果或改善", + "radar.demand.solution": "解法訊號", + "radar.demand.solutionHint": "能判斷你有能力協助的詞", + "radar.demand.exclusion": "排除訊號", + "radar.demand.exclusionHint": "徵才、廣告等不應進入商機的內容", + "radar.demand.custom": "自訂補充", + "radar.demand.customHint": "自訂內容不會覆蓋產品原始資料;下一次產品更新時仍可辨識來源。", + "radar.demand.save": "保存需求地圖", + "radar.demand.saving": "保存中…", + "radar.demand.origin.product": "產品", + "radar.demand.origin.ai": "AI 建議", + "radar.demand.origin.user": "手動", + "radar.demand.customBasis": "手動補充", + + "radar.query.aria": "查詢計畫預覽", + "radar.query.title": "系統會用這些短詞搜尋", + "radar.query.hint": "每組最多兩個詞、中文每詞 2–4 字,才能在 Threads 搜到。產品名稱只作輔助。", + "radar.query.meta": "輸入 {input} · 地圖 v{map}", + "radar.query.group": "查詢組 {n}", + "radar.query.basis": "依據:{text}", + "radar.query.exclude": "排除:{text}", + "radar.query.adopt": "採用這些查詢詞", + "radar.query.empty": "需求地圖尚未有足夠的可搜痛點/情境,暫時無法產生查詢組。", + "radar.query.defaultBasis": "產品痛點", + + "brands.loadFail": "品牌資料載入失敗,請稍後再試", + "brands.deleteImpact": "將暫停 {n} 個相關商機訂閱;歷史商機與接觸紀錄會保留。", + + "crm.board.touchProduct": " · 產品:{label}", + "crm.board.primaryProduct": "主推產品:{label}", + "crm.board.primaryProductWithBrand": "主推產品:{label}({brand})", + "crm.board.noProduct": "未指定產品", + + "utm.title": "UTM 追蹤連結", + "utm.new": "新建", + "utm.dest": "目標 URL", + "utm.label": "標籤", + "utm.create": "建立", + "utm.empty": "尚無連結", + "utm.track": "追蹤:{url}", + "utm.destLine": "目標:{url}", + "utm.clicks": "點擊:{n}", + "utm.copy": "複製連結", + "utm.created": "已建立追蹤連結", + "utm.copied": "已複製追蹤連結", + "utm.copyFail": "複製失敗,請手動選取網址", + + "tools.pain.title": "痛點關鍵字產生器", + "tools.pain.subtitle": "免登入:描述產品,產出海巡可用的掃描詞與痛點。", + "tools.pain.brief": "產品簡述", + "tools.pain.audience": "受眾(選填)", + "tools.pain.run": "產生關鍵字", + "tools.pain.running": "產生中…", + "tools.pain.result": "結果", + "tools.pain.keywords": "關鍵字", + "tools.pain.pains": "痛點", + "tools.pain.scan": "掃描詞", + "tools.pain.login": "登入巡樓海巡", + + "tools.style.title": "風格指紋測驗", + "tools.style.subtitle": "免登入:貼上幾則你的 Threads 貼文,立刻看語氣與節奏。", + "tools.style.samples": "貼文樣本", + "tools.style.run": "開始分析", + "tools.style.running": "分析中…", + "tools.style.result": "結果", + "tools.style.tone": "語氣:{v}", + "tools.style.rhythm": "節奏:{v}", + "tools.style.hooks": "鉤子:{v}", + "tools.style.avoid": "注意:{v}", + "tools.style.login": "登入巡樓", + + "inspire.source": "來源:{label}", + + "bench.title": "成效基準", + "bench.reload": "重新查詢", + "bench.sample": "樣本 {n}", + "bench.median": "中位互動率 {eng}% · 中位瀏覽 {views}", + "bench.yours": "你的互動率 {eng}% · 均覽 {views}", + "bench.insufficient": "樣本不足", + + "insights.summaryTitle": "近 3 個月摘要", + "insights.summaryStats": "貼文 {posts} · 瀏覽 {views} · 讚 {likes} · 回 {replies} · 均互 {eng}%", + "insights.summaryTop": "表現較佳", + "insights.topPostLine": "{eng}% · 覽 {views} · {text}", + + "playbooks.title": "Playbook 市集", + "playbooks.allKinds": "全部類型", + "playbooks.kind.brief": "海巡 brief", + "playbooks.kind.persona": "人設", + "playbooks.kind.play": "互回劇本", + "playbooks.nichePh": "利基(保養/母嬰…)", + "playbooks.mineOnly": "只看我的", + "playbooks.publish": "發布模板", + "playbooks.cancel": "取消", + "playbooks.publishCard": "發布", + "playbooks.fieldTitle": "標題", + "playbooks.fieldNiche": "利基", + "playbooks.fieldBody": "內容", + "playbooks.anonymous": "匿名", + "playbooks.submit": "送出", + "playbooks.empty": "尚無模板", + "playbooks.imports": "引用 {n}", + "playbooks.import": "引用", + "playbooks.published": "已發布", + "playbooks.imported": "已引用到我的 playbook", + + "radar.import.open": "手動匯入", + "radar.import.close": "收起手動匯入", + "radar.import.title": "手動匯入商機", + "radar.import.hint": "貼 Threads/Facebook 貼文網址與內文,跑同一套五問判定。任意網址不會自動讀內文,請直接貼上。量體不足時可用這個補足每日名單。", + "radar.import.url": "貼文網址", + "radar.import.text": "貼文內文", + "radar.import.author": "作者(選填)", + "radar.import.addRow": "+新增一列", + "radar.import.removeRow": "移除", + "radar.import.submit": "送出匯入", + "radar.import.submitting": "匯入中…", + "radar.import.needRow": "至少填一列網址與內文", + "radar.import.csvOpen": "改用貼上 CSV", + "radar.import.csvClose": "收起 CSV", + "radar.import.csvLabel": "貼上 CSV", + "radar.import.csvHint": "格式:url,text,author(author 選填)。第一列若含 url/text 表頭會自動辨識,沒有就照 url,text,author 順序。", + "radar.import.csvApply": "套用到下方列表", + "radar.import.csvEmpty": "沒有解析出任何一列,請確認格式", + "radar.import.status.qualified": "已收進今日商機", + "radar.import.status.rejected": "已判定不符(仍留存)", + "radar.import.status.skipped": "已匯入過,略過", + "radar.import.status.failed": "匯入失敗", + + "radar.explore.open": "立即探索", + "radar.explore.close": "收起探索", + "radar.explore.title": "立即探索", + "radar.explore.hint": "用短關鍵字立刻搜 Threads 上「正在找你」的人,結果走同一套五問判定後進今日商機。每組最多 2 個詞、中文每詞 2–4 字。", + "radar.explore.loadingSuggest": "載入建議關鍵字…", + "radar.explore.suggestions": "建議短詞(點一下加入)", + "radar.explore.selected": "已選關鍵字", + "radar.explore.emptyTerms": "還沒有關鍵字,從上方建議點選或自己加一組。", + "radar.explore.removeChip": "點一下移除", + "radar.explore.addLabel": "自己加一組短詞", + "radar.explore.addPh": "例:保母 求推薦", + "radar.explore.add": "加入", + "radar.explore.run": "開始探索", + "radar.explore.running": "探索中…", + "radar.explore.needTerms": "至少選一組關鍵字", + "radar.explore.result": "找到 {hits} 則、判定 {judged} 則、收進 {created} 則", + "radar.explore.resultZeroHint": "這次沒有新商機。可換更口語的短詞(求推薦、有人知道),或到訂閱管理調整每日監控。", + "radar.explore.resultTruncated": "有 {n} 則因每日上限未收進,明天再來或升級方案。", + "radar.explore.termError.empty": "請輸入關鍵字", + "radar.explore.termError.tooLong": "整組去掉空格後最多 12 字(Threads 長字串常搜不到)", + "radar.explore.termError.tooManyTokens": "最多 2 個詞(用半形空格分隔)", + "radar.explore.termError.invalidToken": "不要標點、emoji、AND/OR 或過短/過長的詞", + "radar.explore.termError.duplicate": "這組詞已經加入了", + "radar.explore.termError.max": "一次最多 6 組關鍵字", + + "scout.promote": "收進商機", + "scout.promoted": "已複製進今日商機({band} · {score}),可到側欄「商機」跟進", + "scout.promoteFail": "收進商機失敗", + + "crm.board.title": "名單看板", + "crm.board.subtitle": "七階段+待追蹤;從商機加入後在這裡推進。", + "crm.board.link.today": "今日商機", + "crm.board.link.followups": "待追蹤", + "crm.board.link.stats": "轉換統計", + "crm.board.filters": "名單篩選", + "crm.board.search": "搜尋名單", + "crm.board.searchPlaceholder": "搜尋名稱或 Threads 帳號", + "crm.board.stageFilter": "階段", + "crm.board.allStages": "全部階段", + "crm.board.sort": "排序", + "crm.board.sortRecent": "最近接觸優先", + "crm.board.sortIntent": "意向分數優先", + "crm.board.clearFilters": "清除篩選", + "crm.board.results": "共 {n} 位聯絡人", + "crm.board.noResults": "找不到符合的聯絡人", + "crm.board.noResultsHint": "換個名稱、帳號或清除階段篩選。", + "crm.board.empty": "還沒有聯絡人", + "crm.board.emptyHint": "在今日商機按「加入名單」就會出現在這裡。", + "crm.board.oppCount": "筆商機", + "crm.board.lastTouch": "最近接觸 {time}", + "crm.board.noTouch": "尚無接觸時間", + "crm.board.stage": "目前階段", + "crm.board.conversion": "成交回報", + "crm.board.amount": "金額(可留空)", + "crm.board.reportWon": "回報成交", + "crm.board.notes": "備註", + "crm.board.noteLabel": "新增備註", + "crm.board.addNote": "儲存備註", + "crm.board.timeline": "時間軸", + "crm.board.timelineEmpty": "還沒有接觸紀錄", + "crm.board.opps": "相關商機", + "crm.board.markFollowUp": "標記待追蹤", + "crm.board.clearFollowUp": "取消待追蹤", + "crm.board.msg.stage": "階段已更新", + "crm.board.msg.followUp": "待追蹤已更新", + "crm.board.msg.won": "已回報成交", + "crm.board.msg.note": "備註已新增", + "crm.board.deleteTitle": "從工作名單移除", + "crm.board.deleteHint": "移除後不再顯示或提醒;原商機、接觸與成交歷史仍保留。", + "crm.board.delete": "移除這位聯絡人", + "crm.board.deleting": "移除中…", + "crm.board.confirmDelete": "確定從名單移除「{name}」嗎?既有商機、接觸與成交紀錄會保留。", + "crm.board.msg.deleted": "已從名單移除", + + "crm.stage.new_found": "新發現", + "crm.stage.engaged": "已接觸", + "crm.stage.dm_sent": "已私訊", + "crm.stage.replied": "已回覆", + "crm.stage.quoted": "已報價", + "crm.stage.won": "成交", + "crm.stage.lost": "流失", + "crm.stage.needs_follow_up": "待追蹤", + + "crm.followups.title": "待追蹤", + "crm.followups.subtitle": "到期回訪、延後與 AI 草稿。", + "crm.followups.link.board": "名單看板", + "crm.followups.link.stats": "轉換統計", + "crm.followups.empty": "目前沒有待追蹤", + "crm.followups.emptyHint": "標記聯絡人待追蹤或送出回覆後會出現在這裡。", + "crm.followups.due": "到期", + "crm.followups.openContact": "開啟聯絡人", + "crm.followups.aiMessage": "AI 追蹤草稿", + "crm.followups.done": "完成", + "crm.followups.snooze": "延後 3 天", + "crm.followups.escalatedHint": "已提醒兩次仍無動作,建議考慮轉未成交。", + "crm.followups.status.scheduled": "排程中", + "crm.followups.status.notified": "已通知", + "crm.followups.status.done": "已完成", + "crm.followups.status.snoozed": "已延後", + "crm.followups.status.escalated": "需升級處理", + "crm.followups.msg.done": "已完成", + "crm.followups.msg.snoozed": "已延後 3 天", + "crm.followups.msg.drafted": "追蹤草稿已產生", + + "crm.stats.title": "轉換統計", + "crm.stats.subtitle": "關鍵字、回覆版本與來源。樣本不足不下結論。", + "crm.stats.terms": "關鍵字轉換", + "crm.stats.variants": "回覆版本成功率", + "crm.stats.sources": "成交來源", + "crm.stats.emptyDim": "尚無足夠資料", + "crm.stats.unavailableDim": "此維度尚未開放(功能開發中,不是你沒有資料)", + "crm.stats.insufficient": "樣本不足", + "crm.stats.col.term": "關鍵字", + "crm.stats.col.variant": "版本", + "crm.stats.col.source": "來源", + "crm.stats.col.accepted": "加入", + "crm.stats.col.replied": "回覆", + "crm.stats.col.won": "成交", + "crm.stats.col.used": "使用", + "crm.stats.col.rate": "比率", +}; diff --git a/apps/web/src/lib/i18n/messages.test.ts b/apps/web/src/lib/i18n/messages.test.ts new file mode 100644 index 0000000..8056a77 --- /dev/null +++ b/apps/web/src/lib/i18n/messages.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { ensureCatalog, getCatalog, isCatalogLoaded, translate } from "./messages"; + +describe("locale catalog loading", () => { + it("ships the default locale and fetches others on demand", async () => { + expect(isCatalogLoaded("zh-TW")).toBe(true); + expect(isCatalogLoaded("en")).toBe(false); + // 還沒載到英文字典時,先用預設語系頂著而不是吐出 key + expect(translate("en", "app.name")).toBe(translate("zh-TW", "app.name")); + + await ensureCatalog("en"); + + expect(isCatalogLoaded("en")).toBe(true); + expect(getCatalog("en")["app.name"]).toBe("Lapras"); + expect(translate("en", "app.name")).toBe("Lapras"); + }); + + it("shares one in-flight request across concurrent callers", async () => { + const [a, b] = await Promise.all([ensureCatalog("en"), ensureCatalog("en")]); + expect(a).toBe(b); + }); +}); diff --git a/apps/web/src/lib/i18n/messages.ts b/apps/web/src/lib/i18n/messages.ts index d0159cb..1cb3655 100644 --- a/apps/web/src/lib/i18n/messages.ts +++ b/apps/web/src/lib/i18n/messages.ts @@ -1,5823 +1,51 @@ -import type { AppLocale } from "./types"; +import { zhTW } from "./catalog.zhTW"; +import type { AppLocale, MessageDict } from "./types"; -/** 扁平 key → 字串;{name} 可插值 */ -export type MessageDict = Record; +export type { MessageDict }; -export const zhTW: MessageDict = { - "app.name": "巡樓", - "app.nameEn": "Lapras", - "app.nameZh": "巡樓", - "app.tagline": "Threads 海巡好幫手", - "app.taglineEn": "Patrol Threads with ease", - "nav.today": "今日", - "nav.crew": "帳號", - "nav.studio": "創作", - /** 每日自動需求名單(vs 海巡=手動掃一輪) */ - "nav.radar": "商機", - "nav.crm": "名單", - /** 手動掃場外展(vs 商機=訂閱後每天自動) */ - "nav.scout": "話題", - "nav.outbox": "發送", - "nav.jobs": "任務", - "nav.brands": "品牌", - "nav.policy": "商機政策", - "nav.playbooks": "市集", - "nav.insights": "成效", - "nav.benchmark": "基準", - "nav.utm": "追蹤", - "nav.more": "更多", - "nav.moreTitle": "更多功能", - "nav.users": "島民管理", - "nav.usage": "用量與方案", - "nav.profile": "會員資料", - "nav.invite": "邀請關係", - "nav.settings": "系統設定", - "nav.logout": "登出", - "nav.navigate": "導覽", - "navGroup.workflow": "主流程", - "navGroup.accounts": "帳號品牌", - "navGroup.growth": "成長工具", +/* +預設語系跟著主程式走,其餘語系各自打包成 chunk 並在切換時才下載。 +translate 維持同步:字典還沒到位前先用預設語系頂著,載完由 I18nProvider 觸發重繪。 +*/ +const catalogs: Partial> = { "zh-TW": zhTW }; - "workspace.label": "工作區", - "workspace.default": "預設", - "workspace.new": "+ 新增工作區", - "workspace.newPrompt": "新工作區名稱", - - "common.save": "儲存", - "common.saving": "儲存中…", - "common.cancel": "取消", - "common.close": "關閉", - "common.loading": "載入中…", - "common.retry": "重試", - "common.back": "返回", - "common.delete": "刪除", - "common.edit": "編輯", - "common.search": "搜尋", - "common.confirm": "確定", - "common.optional": "選填", - "common.success": "已儲存", - "common.error": "發生錯誤", - "common.yes": "是", - "common.no": "否", - - // 本頁說明(頂欄抽屜,不進頁面正文) - "help.open": "說明", - "help.kicker": "本頁說明", - "help.section.what": "這頁在做什麼", - "help.section.how": "怎麼用", - "help.section.tips": "小提醒", - "help.section.related": "相關頁面", - "help.shortcutHint": "入口在頁面標題旁的「?」。快捷鍵:? 開關;Esc 關閉。", - - "help.page.generic.title": "巡樓主控台", - "help.page.generic.what": "這是巡樓的工作桌面。左側(或手機底欄)切換功能,頂欄可看用量、通知與本頁說明。", - "help.page.generic.step1": "從側欄選你要做的事(海巡、雷達、發送等)。", - "help.page.generic.step2": "需要幫助時點頂欄「說明」或按 ?。", - "help.page.generic.step3": "設定、帳號與方案在右上角選單。", - "help.page.generic.tips": "說明不會改你的資料,可隨時開關。", - - "help.page.today.title": "今日", - "help.page.today.what": "今日儀表板:一眼看商機、海巡待回、發送與帳號脈動,決定今天先做哪件事。", - "help.page.today.step1": "看「今日商機」摘要,有名單就點進雷達處理。", - "help.page.today.step2": "海巡待回區處理需要回覆的貼文。", - "help.page.today.step3": "發送失敗或進行中的 Outbox 可從這裡追蹤。", - "help.page.today.tips": "沒有商機時摘要會引導你去填商機政策或訂閱關鍵字。", - - "help.page.crew.title": "帳號(Crew)", - "help.page.crew.what": "管理已連結的 Threads 帳號、健康度與可用性,發文/外展都從這裡的帳號出發。", - "help.page.crew.step1": "用 OAuth 連結至少一個可用帳號。", - "help.page.crew.step2": "確認連線狀態與健康度(throttle 時勿硬送)。", - "help.page.crew.step3": "需要時到設定頁調整 AI 與開發選項。", - "help.page.crew.tips": "帳號健康偏黃/限速時,公開留言自動送出會被擋,請改手動。", - - "help.page.studio.title": "創作", - "help.page.studio.what": "寫貼文、做人設、靈感與劇本;完成後送進 Outbox 排程發送。", - "help.page.studio.step1": "選人設或品牌語氣後開始草稿。", - "help.page.studio.step2": "用靈感/仿寫輔助,再人工改到可發。", - "help.page.studio.step3": "送出到發送匣,到 Outbox 確認排程。", - "help.page.studio.tips": "創作頁不直接等同已發佈;真正送出在 Outbox。", - - "help.page.scout.title": "話題靈感", - "help.page.scout.what": "用關鍵字找 Threads 上可跟的活躍話題,產出草稿、回完就結。找「正在找你的人」請用側欄「商機」的每日巡或立即探索。", - "help.page.scout.step1": "寫話題關鍵字,按「產出關鍵字」檢視/增刪 query。", - "help.page.scout.step2": "確認後「用這些詞開始搜」,在佇列依發文時間處理。", - "help.page.scout.step3": "寫草稿、開 Threads 回覆、標記完成。", - "help.page.scout.tips": "話題=內容靈感;商機=找需求客戶。找客戶請用商機頁,不要在這裡掃痛點。", - - "help.page.radar_today.title": "商機巡邏", - "help.page.radar_today.what": "定期或立刻巡邏,找出產品能解決的痛點或新文章。看懂理由後留下或丟掉即可。", - "help.page.radar_today.step1": "看頁頂巡邏狀態:每日定時是否開著、上次何時巡、要不要立即再巡一輪。", - "help.page.radar_today.step2": "讀「為什麼推薦」。對得上就「留下」,不是你的就「丟掉」。", - "help.page.radar_today.step3": "只有真的要追這個人時才「加入名單」。名單不是看結果的必要步驟。", - "help.page.radar_today.tips": "立即巡邏與每日定時可同時開著。關掉其中一個不會藏掉另一個。", - - "help.page.radar_watches.title": "設定巡邏", - "help.page.radar_watches.what": "選定產品與關鍵字後,每日定時巡邏會自動跑;也可隨時按立即巡邏。結果回到側欄「商機」。", - "help.page.radar_watches.step1": "選品牌與產品,補需求地圖裡的痛點。", - "help.page.radar_watches.step2": "填客人會搜的關鍵字與排除詞。", - "help.page.radar_watches.step3": "打開每日定時,或按「立即巡邏」現在跑一輪。", - "help.page.radar_watches.tips": "啟用數有方案上限;滿了要先暫停一組。", - - "help.page.crm_board.title": "名單管理", - "help.page.crm_board.what": "從「今日商機」加入後的聯絡人工作清單,可搜尋、篩選、排序、備註、成交與查看時間軸。", - "help.page.crm_board.step1": "在今日商機按「加入名單」後,人會出現在「新發現」。", - "help.page.crm_board.step2": "點聯絡人推進階段、加備註或標記待追蹤。", - "help.page.crm_board.step3": "成交時用回報成交(金額可留空)。", - "help.page.crm_board.tips": "不再需要的聯絡人可從工作名單移除;既有商機、接觸與成交歷史會保留。", - - "help.page.crm_followups.title": "待追蹤", - "help.page.crm_followups.what": "到期要回訪的清單:完成、延後、或產生 AI 追蹤草稿。", - "help.page.crm_followups.step1": "看到期日與狀態(含需升級處理)。", - "help.page.crm_followups.step2": "需要文案時用 AI 草稿,再手動複製送出。", - "help.page.crm_followups.step3": "做完按完成,或延後 3 天。", - "help.page.crm_followups.tips": "系統不會替你自動傳訊;草稿僅供複製。", - - "help.page.crm_stats.title": "轉換統計", - "help.page.crm_stats.what": "從關鍵字、回覆版本、來源看轉換;樣本不足時不下結論。", - "help.page.crm_stats.step1": "先看各表的絕對數。", - "help.page.crm_stats.step2": "標「樣本不足」的列不要拿來排名。", - "help.page.crm_stats.step3": "回到看板或雷達調整關鍵字與回覆策略。", - "help.page.crm_stats.tips": "樣本少時比率會隱藏,這是刻意設計。", - - "help.page.outbox.title": "發送", - "help.page.outbox.what": "排程與發送佇列:草稿真正送上 Threads 前的最後一站。", - "help.page.outbox.step1": "檢查待發送與失敗項目。", - "help.page.outbox.step2": "失敗可看原因後重試或改草稿。", - "help.page.outbox.step3": "進行中任務也可在任務中心追蹤。", - "help.page.outbox.tips": "帳號健康限速時自動送出會被擋。", - - "help.page.jobs.title": "任務", - "help.page.jobs.what": "背景工作(掃描、巡檢、分析等)的進度與結果。", - "help.page.jobs.step1": "列表看狀態:排隊/執行中/成功/失敗。", - "help.page.jobs.step2": "點進詳情看進度摘要。", - "help.page.jobs.step3": "失敗時依摘要回對應功能重試。", - "help.page.jobs.tips": "雷達「立即巡」會在這裡產生 radar_sweep 任務。", - - "help.page.brands.title": "品牌", - "help.page.brands.what": "集中維護品牌與產品資料,讓商機搜尋知道你在賣什麼、能解決哪些痛點。", - "help.page.brands.step1": "維護品牌與產品基本資料。", - "help.page.brands.step2": "為產品補上情境、痛點、比對標籤與服務能力詞。", - "help.page.brands.step3": "需要價格、地區、禁語、案例與口吻時,到獨立的「商機政策」設定。", - "help.page.brands.tips": "產品資料越具體,搜尋前處理與產品配對越準。", - - "help.page.policy.title": "商機政策", - "help.page.policy.what": "集中設定商機判定與回覆共用的服務政策,不再混在品牌/案例管理裡。", - "help.page.policy.step1": "填服務項目、價格與可服務地區。", - "help.page.policy.step2": "補上禁語、案例、FAQ、可接案時間與口吻。", - "help.page.policy.step3": "儲存後,商機判定、訂閱啟用與回覆生成會共用這份政策。", - "help.page.policy.tips": "Policy 是工作區共用設定;品牌與產品本身請回品牌頁維護。", - - "help.page.playbooks.title": "市集(Playbooks)", - "help.page.playbooks.what": "分享或引用海巡 brief、人設與劇本模板。", - "help.page.playbooks.step1": "瀏覽可用模板。", - "help.page.playbooks.step2": "引用到你的工作區後再編輯。", - "help.page.playbooks.step3": "回到海巡或創作實際使用。", - "help.page.playbooks.tips": "模板是起點,發文前請依你的品牌改寫。", - - "help.page.insights.title": "成效", - "help.page.insights.what": "貼文與互動成效管線,用來複盤什麼內容有效。", - "help.page.insights.step1": "同步或整理近期貼文成效。", - "help.page.insights.step2": "對照高表現內容調整創作。", - "help.page.insights.step3": "需要全站基準可看基準頁。", - "help.page.insights.tips": "數據延遲取決於平台同步,非即時秒級。", - - "help.page.benchmark.title": "基準", - "help.page.benchmark.what": "全站匿名聚合中位數,樣本足夠才顯示,用來對照自己的水準。", - "help.page.benchmark.step1": "查看有樣本的指標。", - "help.page.benchmark.step2": "樣本不足的項目不要過度解讀。", - "help.page.benchmark.step3": "回到成效與創作調整策略。", - "help.page.benchmark.tips": "通常樣本 ≥ 5 才顯示有意義的中位數。", - - "help.page.utm.title": "追蹤(UTM)", - "help.page.utm.what": "建立帶 UTM 的連結,方便之後看流量來源。", - "help.page.utm.step1": "填活動與來源參數。", - "help.page.utm.step2": "產生連結後用於貼文或私訊。", - "help.page.utm.step3": "在分析工具對照成效。", - "help.page.utm.tips": "參數命名保持一致,後續報表才好彙總。", - - "help.page.settings.title": "系統設定", - "help.page.settings.what": "AI 供應商、搜尋金鑰、介面偏好等系統級選項。", - "help.page.settings.step1": "確認 AI 與金鑰(平台或 BYOK)。", - "help.page.settings.step2": "搜尋金鑰可選填自備,或使用平台預設。", - "help.page.settings.step3": "改完後回業務頁驗證。", - "help.page.settings.tips": "錯誤的金鑰會讓生成與掃描失敗,訊息多在用量或任務裡。", - - "help.page.profile.title": "會員資料", - "help.page.profile.what": "你的帳號資料、密碼與基本偏好。", - "help.page.profile.step1": "更新顯示名稱等基本資料。", - "help.page.profile.step2": "需要時修改密碼。", - "help.page.profile.step3": "語言/主題可在介面偏好調整。", - "help.page.profile.tips": "這是會員層設定,與 Threads 連帳不同(連帳在帳號頁)。", - - "help.page.invite.title": "邀請關係", - "help.page.invite.what": "邀請連結、下線關係與獎勵相關資訊。", - "help.page.invite.step1": "複製邀請連結分享。", - "help.page.invite.step2": "查看已建立的邀請關係。", - "help.page.invite.step3": "獎勵規則以產品內標示為準。", - "help.page.invite.tips": "請勿濫發邀請;違規可能被停權。", - - "help.page.usage.title": "用量與方案", - "help.page.usage.what": "看點數、分項用量與目前方案額度。", - "help.page.usage.step1": "對照各 meter 的使用量。", - "help.page.usage.step2": "接近上限時考慮升級或 BYOK。", - "help.page.usage.step3": "方案細節在方案頁。", - "help.page.usage.tips": "BYOK 通常只計呼叫次數,不占平台點數(以頁面標示為準)。", - - "help.page.usage_plans.title": "方案", - "help.page.usage_plans.what": "比較與選擇付費方案。", - "help.page.usage_plans.step1": "比較額度與功能。", - "help.page.usage_plans.step2": "選方案進入結帳。", - "help.page.usage_plans.step3": "完成後回用量頁確認。", - "help.page.usage_plans.tips": "價格與額度以結帳當下標示為準。", - - "help.page.usage_checkout.title": "結帳", - "help.page.usage_checkout.what": "完成方案購買的付款流程。", - "help.page.usage_checkout.step1": "確認方案與金額。", - "help.page.usage_checkout.step2": "依指示完成付款。", - "help.page.usage_checkout.step3": "成功後回用量確認額度。", - "help.page.usage_checkout.tips": "付款異常請保留單號並聯絡支援。", - - "help.page.admin_users.title": "島民管理", - "help.page.admin_users.what": "管理員維護會員帳號、停權與角色。", - "help.page.admin_users.step1": "搜尋或瀏覽會員。", - "help.page.admin_users.step2": "調整狀態或角色(慎用)。", - "help.page.admin_users.step3": "重大操作保留紀錄與原因。", - "help.page.admin_users.tips": "僅管理員可見;誤操作可能影響他人登入。", - - // 後端 envelope code(apps/backend response + middleware) - "api.err.unknown": "操作失敗,請稍後再試", - "api.err.studioValidation": "資料不符合規則,請檢查後再試", - "api.err.crawlerSession": "請先到設定同步 Chrome 工作階段後再試", - "api.err.network": "無法連線後端,請確認網路或服務是否啟動", - "api.err.400001": "請求格式不正確", - // 與後端 400003 / ValidatePasswordPolicy 同一句(勿各寫各的) - "api.err.400003": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - "password.policy.hint": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - "password.policy.minLen": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - "password.policy.needUpper": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - "password.policy.needLower": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - "password.policy.needDigit": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - "password.policy.needSymbol": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - - "api.err.400004": "驗證碼無效或已過期", - "api.err.400020": "不能停權自己的帳號", - "api.err.400021": "不能移除或停權最後一位管理員", - "auth.unreachable": "連不上伺服器,無法確認登入狀態。請檢查網路後重試。", - "api.err.401001": "請先登入", - "api.err.401002": "登入已失效,請重新登入", - "api.err.401003": "找不到會員資料,請重新登入", - "api.err.401010": "Email 或密碼錯誤", - "api.err.403001": "帳號已停權", - "api.err.403002": "需要管理員權限", - "api.err.404001": "找不到資料", - "api.err.404002": "查無此信箱,請確認後再試", - "api.err.409001": "此 Email 已被註冊", - "api.err.402001": "本月平台點數已用完,請升級方案或下月再試", - "usage.err.meterCap": "此功能本月點數已達上限,請改用其他功能或升級方案", - "usage.err.platformCapacity": "平台 AI/搜尋目前忙碌或已限流。可稍後再試,或到設定填入自己的 API Key(BYOK)繼續使用", - "api.err.500000": "伺服器發生錯誤,請稍後再試", - "api.err.timeoutAI": "AI 回應逾時(模型思考太久)。可縮短結構備註後再試,或到設定換較快的模型", - "api.err.501000": "此功能尚未實作", - "api.err.501010": "這個功能還沒開通,之後的版本會補上", - "api.err.400100": "填寫內容不符合規則,請檢查後再送出", - - // 後端 data.message 成功文案 - "api.ok.verifySent": "驗證碼已寄出,請查收信箱", - "api.ok.verifyIssued": "驗證碼已產生", - "api.ok.resetSent": "重設信已寄出,請查收信箱", - "api.ok.passwordUpdated": "密碼已更新", - "api.ok.loggedOut": "已登出", - "api.ok.unbound": "已解除綁定", - - "topbar.notifications": "通知", - "topbar.unread": "{n} 未讀", - "topbar.markAllRead": "全已讀", - "topbar.noNotifications": "目前沒有通知", - "topbar.jobsCenter": "任務中心", - "topbar.moreOlder": "還有 {n} 則較舊", - "topbar.account": "帳號選單", - - "role.admin": "管理員", - "role.member": "一般會員", - "role.verified": "已驗證", - "role.unverified": "未驗證", - - "login.title": "登入巡樓", - "login.email": "Email", - "login.password": "密碼", - "login.showPassword": "顯示密碼", - "login.hidePassword": "隱藏密碼", - "login.submit": "登入", - "login.submitting": "登入中…", - "login.forgot": "忘記密碼?", - "login.mockHint": "管理員 demo@harbor.local / demo · 一般會員 alice@harbor.local / alice", - "login.liveHint": "Live 後端:admin@haixun.local / admin123(gateway :8888)", - - "forgot.title": "忘記密碼", - "forgot.submit": "寄送重設連結", - "forgot.submitting": "送出中…", - "forgot.back": "返回登入", - "forgot.hint": "輸入註冊用 Email,我們會寄送重設連結與驗證碼。", - "forgot.mockMail": "重設信", - "forgot.openReset": "前往輸入驗證碼/重設密碼", - "forgot.retry": "再試一次", - "forgot.nextStep": "請查收信箱中的驗證碼或連結,再到重設頁完成新密碼。", - - "reset.title": "設定新密碼", - "reset.hint": "請輸入信箱、驗證碼(信內 6 碼或連結參數)與新密碼。", - "reset.code": "驗證碼", - "reset.codePh": "信內 6 位數字", - "reset.needEmail": "請輸入 Email", - "reset.needCode": "請輸入驗證碼", - "reset.newPassword": "新密碼", - "reset.confirm": "確認新密碼", - "reset.submit": "更新密碼", - "reset.submitting": "更新中…", - "reset.missingToken": "連結缺少 token。請從「忘記密碼」重新申請。", - - "verify.title": "驗證信箱", - "verify.pending": "尚未驗證", - "verify.body": "帳號已開通登入,但信箱未驗證前無法使用功能。請輸入寄到你信箱的 6 位驗證碼。", - "verify.code": "驗證碼", - "verify.submit": "完成驗證", - "verify.submitting": "驗證中…", - "verify.resend": "重新寄送驗證碼", - "verify.sending": "寄送中…", - "verify.logout": "登出", - "verify.mockMail": "驗證信", - "verify.mockHint": "請輸入驗證碼:", - "verify.after": "驗證完成後即可使用今日、創作、海巡等功能。", - - "settings.title": "設定", - "settings.loadFail": "載入設定失敗,請重新整理再試", - "settings.localeCurrency": "語言與幣別", - "settings.locale": "介面語言", - "settings.currency": "顯示幣別", - "settings.currencyHint": "金額顯示用;方案計費仍以台幣為準。", - "settings.localeSaved": "語言與幣別已更新", - "settings.appearance": "外觀", - "settings.theme": "主題", - "settings.themeLight": "淺色", - "settings.themeDark": "深色", - "settings.themeSystem": "跟隨系統", - "settings.themeHint": "可選淺色、深色,或跟隨系統外觀。", - "settings.themeSaved": "主題已更新", - "settings.themeToLight": "切換成淺色", - "settings.themeToDark": "切換成深色", - "settings.dataSource": "資料來源", - "settings.dataSourceHint": - "業務資料一律走 live API。Mock 僅保留邀請關係等少數本機模組;請維持 Live 並連 gateway。", - "settings.dataSourceMock": "Mock(已縮)", - "settings.dataSourceLive": "Live(後端 API)", - "settings.dataSourceCurrent": "目前", - "settings.dataSourceSwitchedLive": "已切到 Live — 請用後端帳號重新登入(admin@haixun.local / admin123)", - "settings.dataSourceSwitchedMock": "已切回 Mock(邀請外仍打 live)", - "settings.dataSourceLiveNeedGateway": "需 gateway 在 :8888(或 Vite proxy /api)", - "settings.ai": "AI", - "settings.search": "搜尋", - "settings.threads": "Threads 連帳(平台)", - "settings.threadsHint": - "App ID/Secret 在 gateway 平台設定(yaml/env),Secret 不會顯示。請把 Callback 貼到 Meta 後台,並用 https 公開站連帳。", - "settings.threadsProvider": "目前模式", - "settings.threadsProviderFake": "Fake(開發/本機可測 OAuth 流程)", - "settings.threadsProviderMeta": "Meta 正式", - "settings.threadsConfigured": "App 憑證", - "settings.threadsConfiguredYes": "已設定", - "settings.threadsConfiguredNo": "未設定", - "settings.threadsCallback": "OAuth Callback URL(redirect_uri)", - "settings.threadsCallbackHint": "Meta App → Valid OAuth Redirect URIs 請貼這一整行(必須 https)", - "settings.threadsCopy": "複製", - "settings.threadsCopied": "已複製 Callback URL", - "settings.threadsCopyFail": "複製失敗,請手動選取", - "settings.threadsPublicWeb": "公開站 origin", - "settings.threadsGoCrew": "前往 Crew 連帳", - "settings.member": "會員與登入", - "settings.usageCard": "AI/搜尋額度", - "settings.usageCardHint": "平台代付點數與方案上限;自備 Key 不占平台點。", - "settings.viewUsage": "查看用量", - "settings.editProfile": "編輯會員資料", - "settings.mockLogin": "demo@harbor.local / demo", - "settings.memberHint": "登入帳號、顯示名稱與通知偏好。", - - "usage.title": "用量與方案", - "usage.desc": "查看本月平台點數、分項用量,並變更方案。", - "usage.creditsUsed": "本月已用點數", - "usage.remaining": "剩餘 {n} 點", - "usage.percentUsed": "{n}% 已使用", - "usage.breakdown": "分項用量", - "usage.plans": "方案", - "usage.current": "目前", - "usage.inUse": "使用中", - "usage.switchMock": "切換方案", - "usage.perMonth": "點/月", - "usage.ledger": "最近使用紀錄", - "usage.emptyTitle": "本月還沒有扣點", - "usage.emptyDesc": "開始創作、海巡或生圖後,這裡會出現扣點紀錄。", - "usage.planNote": "自然月重置;未用完點數不累積至下月。", - "usage.switched": "已切換為 {name}", - "usage.meter.times": "{count} 次 · {credits} 點", - "usage.meter.timesShort": "{n} 次", - "usage.meter.pt": "點", - "usage.meter.over": "已超出", - "usage.meter.locked": "已鎖", - "usage.meter.cap": "單項上限 {credits}/{cap} 點", - "usage.side.aiCredits": "AI 相關已用點數(文案+研究+生圖)", - "usage.side.searchCredits": "搜尋已用點數", - "usage.byok.title": "自備 Key 用量", - "usage.byok.hint": "只計呼叫次數,不占平台點數與分項進度。", - "usage.plan.free.blurb": "夠用試用,體驗完整創作流程", - "usage.plan.starter.blurb": "小團隊日常發文與海巡", - "usage.plan.pro.blurb": "多帳、重度 AI 與研究", - - "profile.title": "會員資料", - "profile.desc": "管理顯示名稱、頭像與密碼。", - "profile.accountStatus": "帳號開通狀態", - "profile.basic": "基本資料", - "profile.avatar": "頭像", - "profile.avatarUpload": "上傳頭像", - "profile.avatarRemove": "移除頭像", - "profile.avatarHint": "JPG/PNG/WebP,5MB 內。選圖後按「儲存基本資料」寫入;「移除頭像」會立刻清除。", - "profile.avatarSaved": "頭像已更新", - "profile.avatarCleared": "已移除頭像", - "profile.avatarFail": "無法讀取圖片", - "profile.displayName": "顯示名稱", - "profile.bio": "簡介(選填)", - "profile.timezone": "時區", - "profile.notifyEmail": "Email 通知", - "profile.password": "變更密碼(選填)", - "profile.currentPassword": "目前密碼", - "profile.newPassword": "新密碼", - "profile.confirmPassword": "確認新密碼", - "profile.emailVerified": "信箱已驗證", - "profile.emailUnverified": "信箱未驗證", - "profile.roleAdmin": "管理員", - "profile.roleMember": "一般會員", - "profile.loginEmail": "登入信箱:{email}", - "profile.verifiedAt": " · 驗證於 {time}", - "profile.goVerify": "去驗證信箱", - "profile.roleTags": "權限標籤:{labels}", - "profile.roleNote": "角色由系統指派;改信箱後需重新驗證。", - "profile.saveBasic": "儲存基本資料", - "profile.updatePassword": "更新密碼", - "profile.saved": "會員資料已儲存", - "profile.savedUnverified": "資料已儲存。信箱尚未驗證或已變更,請完成驗證後才能繼續使用功能。", - "profile.saveFail": "儲存失敗", - "profile.needNewPassword": "請輸入新密碼", - "profile.passwordMismatch": "兩次新密碼不一致", - "profile.needCurrentPassword": "請輸入目前密碼", - "profile.wrongCurrentPassword": "目前密碼不正確", - "profile.passwordUpdated": "密碼已更新", - "profile.passwordFail": "變更密碼失敗", - "profile.listJoin": "、", - "profile.tenantUid": " · tenant {tenant} · uid {uid}", - "profile.inviteBadge": "邀請碼", - "profile.inviteHint": "分享給朋友加入;日後活動可依邀請關係計算。", - "profile.gotoInvite": "查看邀請關係", - - "invite.title": "邀請關係", - "invite.desc": "邀請碼與邀請關係;管理端為樹狀結構。", - "invite.tabs": "邀請檢視", - "invite.tab.mine": "我的邀請", - "invite.tab.tree": "關係樹", - "invite.myCode": "我的邀請碼", - "invite.codeLabel": "邀請碼", - "invite.copyCode": "複製", - "invite.copied": "已複製", - "invite.copyFail": "複製失敗", - "invite.stats": "直邀 {direct} · 延伸 {total}", - "invite.rewards.summary": "邀請贈點累計 {total} · 本月 {month} /上限 {cap}", - "invite.rewards.title": "邀請回饋紀錄", - "invite.upline": "邀請人", - "invite.downlines": "直邀成員 · {n}", - "invite.noUpline": "無邀請人", - "invite.noDownline": "尚無直邀", - "invite.directN": "直邀 {n}", - "invite.claimHint": "若註冊時沒填,可在此補上邀請人的邀請碼(綁定後不可自行更改)。", - "invite.claimLabel": "邀請碼", - "invite.claimPh": "例如 HX-DEMO01", - "invite.claimSubmit": "確認綁定", - "invite.claimOk": "已綁定邀請人:{name}", - "invite.claimOkGeneric": "已綁定邀請人", - "invite.claimFail": "綁定失敗", - "invite.claimLocked": "已綁定,如需調整請聯絡管理員。", - "invite.loadFail": "載入失敗", - "invite.treeEmpty": "尚無資料", - "invite.treeSearch": "搜尋", - "invite.treeSearchPh": "名稱 / Email / 邀請碼", - "invite.treeCount": "{n} 人", - "invite.treeMatchCount": "命中 {n} · 共 {total}", - "invite.treeNoMatch": "沒有符合的島民", - "invite.treeNoMatchHint": "換個關鍵字試試", - "invite.clearSearch": "清除", - "invite.moveTitle": "調整歸屬", - "invite.newParent": "邀請人", - "invite.root": "(無 · 獨立)", - "invite.confirmMove": "確認", - "invite.moved": "已調整「{name}」→「{parent}」", - "invite.moveFail": "調整失敗", - "invite.openFromAdmin": "邀請關係", - "invite.parentField": "邀請人", - "invite.orgPickRoot": "選一個起點(含無人邀請、自己進來的)", - "invite.orgRoots": "起點", - "invite.orgPath": "路徑", - "invite.orgChainN": "延伸 {n}", - "invite.orgFocusStats": "直邀 {direct} · 延伸 {total}", - "invite.orgDirects": "直邀成員 · {n}", - "invite.orgNoDirects": "沒有直邀成員", - "invite.err.notFound": "找不到會員", - "invite.err.notLoggedIn": "尚未登入", - "invite.err.needAdmin": "需要管理員權限", - "invite.err.memberNotFound": "找不到島民", - "invite.err.selfParent": "不能把自己設為邀請人", - "invite.err.parentNotFound": "找不到指定的邀請人", - "invite.err.cycle": "不可掛到自己邀請鏈的下層(會形成環)", - "invite.err.repoMissing": "邀請資料層尚未載入,請重新整理頁面", - "invite.err.alreadyBound": "你已有邀請人,無法再補填", - "invite.err.codeRequired": "請輸入邀請碼", - "invite.err.codeNotFound": "找不到此邀請碼", - "invite.err.codeSelf": "不能填自己的邀請碼", - - "admin.users.title": "島民管理", - "admin.users.desc": "管理島民帳號、權限、方案與不擋額度。", - "admin.users.needAdmin": "需要管理員權限", - "admin.users.list": "島民列表 · {n}", - "admin.users.detail": "島民詳情", - "admin.users.pick": "選擇島民", - "admin.users.search": "搜尋名稱 / uid", - "admin.users.searchPh": "島民名稱、Email 或 uid", - "admin.users.create": "新增島民", - "admin.users.suspend": "停權", - "admin.users.unsuspend": "復權", - "admin.users.suspended": "已停權", - "admin.users.active": "正常", - "admin.users.onboarding": "第一次引導", - - "crew.title": "帳號", - "crew.tab.accounts": "帳號", - "crew.tab.personas": "人設", - "crew.connect": "連接帳號", - "crew.connecting": "連線中…", - "crew.tokenRenewHint": "Token 由背景任務自動延長(約第 30 天),可在「任務」查看;無需手動刷新。", - "crew.empty": "尚無帳號", - "crew.loadFail": "載入帳號失敗", - "crew.unusable": "不可用", - "crew.expires": "到期 {time}", - "crew.lastRefresh": "上次延長 {time}", - "crew.refreshSession": "延長 token", - "crew.refreshSessionHint": "用已存授權換新 token,不開授權頁;失敗請「連接帳號」重授權", - "crew.session.ok": "token 有效", - "crew.session.soon": "即將到期", - "crew.session.expired": "token 已過期", - "crew.session.unknown": "未記錄到期", - "crew.connection.connected": "已連線", - "crew.connection.error": "異常", - "crew.connection.disconnected": "已斷開", - "crew.connection.unknown": "未知", - "crew.health.needsReconnect": "需重新連帳", - "crew.health.needsReconnectHint": "token 無法使用,請按「連接帳號」重新授權(不是延長 token)", - "crew.health.disconnectedHint": "已解除綁定", - "crew.health.expiredHint": "請延長 token 或重新連帳", - "crew.opHealthScore": "操作 {n}", - "crew.msg.refreshed": "@{user} 已用現有授權延長 token(約 60 天)", - "crew.msg.refreshedAll": "已用現有授權延長 {n} 個帳號 token", - "crew.msg.refreshFail": "延長失敗(token 失效時請重新「連接帳號」)", - "crew.msg.oauthOk": "Threads 帳號已連線;已排程約 30 天後自動延長 token(見任務)", - "crew.msg.oauthFail": "OAuth 連線失敗", - "crew.msg.oauthUrlFail": "無法取得授權網址,請稍後再試或檢查平台 Threads 設定", - "crew.msg.deleted": "已移除 @{user}", - "crew.confirmDelete": "確定刪除帳號 @{user}?\n刪除後無法再選為 lead / cast。", - - "today.findTopic": "找話題", - "today.refreshTopics": "刷新話題", - "today.reload": "重新整理", - "today.loadFail": "無法載入今日資料", - "today.trendsFail": "無法刷新話題(可能額度不足或搜尋未設定)", - "today.trendsUpdated": "已更新 {n} 個話題", - "today.syncPosts": "同步已發文", - "today.syncPostsFail": "同步貼文失敗", - "today.needAccount": "請先連接 Threads 帳號", - "today.outcome.title": "本週成果", - "today.outcome.reach": "觸達", - "today.outcome.conversations": "對話", - "today.outcome.follows": "追蹤", - "today.outcome.followsHint": "可能相關,尚無強訊號", - "today.outcome.followsConfirmedHint": "/確定 {n}", - "today.outcome.conversions": "成交", - "today.outcome.emptyHint": "本週還沒有海巡外展成果,去海巡試試身手?", - "today.checkup.empty": "尚未產生本週健檢,將於下週一(依你的時區)自動產生。", - "today.checkup.prefix": "健檢:", - "today.pendingReplies": "待回覆", - "today.pendingRepliesN": "待回覆 · {n}", - "today.newThread": "寫一則", - "today.metricsAria": "今日數值", - "today.metric.pending": "待回", - "today.metric.pendingHint": "海巡佇列", - "today.metric.doneGoal": "已回/目標", - "today.metric.doneGoalHint": "今日海巡完成數/目標(海巡標記已發會累加)", - "today.metric.sentToday": "今日已發", - "today.metric.running": "進行中 {n}", - "today.metric.sentDone": "完成的發送", - "today.metric.failed": "發送異常", - "today.metric.needAction": "需處理", - "today.metric.ok": "正常", - "today.metric.mentions": "提及", - "today.metric.mentionsHint": "待回提及", - "today.pending.title": "海巡待回 · {n}", - "today.pending.empty": "目前沒有待回,去海巡掃一輪", - "today.goScout": "去海巡", - "today.pending.more": "還有 {n} 則 →", - "today.pending.handle": "海巡處理", - "today.pending.start": "開始處理", - "today.topics.title": "找話題", - "today.topics.empty": "還沒有話題,按「刷新話題」抓一輪", - "today.goStudio": "去靈感", - "today.heat": "熱度 {n}", - "today.topicAngle": "可當開場角度", - "today.moreInspire": "更多靈感", - "today.useTopic": "用話題發想", - "today.outbox.title": "今日發送", - "today.outbox.empty": "還沒有今日發送。可先", - "today.outbox.emptyMid": ",完成後會出現在", - "today.outbox.emptyEnd": "。", - "today.outbox.summary": "已完成 {sent} · 進行中 {running} · 異常 {failed}", - "today.badge.failed": "失敗", - "today.badge.scheduling": "排程", - "today.badge.sending": "發送中", - "today.badge.drafted": "已草稿", - "today.openOutbox": "開啟發送", - "today.accounts.title": "帳號成效", - "today.accounts.empty": "尚無成效資料,可先同步已發文", - "today.postsCount": "{n} 則貼文", - "today.views": "瀏覽", - "today.likes": "讚", - "today.repliesShort": "回", - "today.fullInsights": "完整成效 · 月比圖", - "today.viewPosts": "看已發文", - "today.manageAccounts": "管理帳號", - - - "outbox.title": "發送", - "outbox.tabsAria": "發送分頁", - "outbox.tab.active": "進行中", - "outbox.tab.history": "歷史", - "outbox.empty": "尚無發送", - "outbox.activeEmpty": "進行中是空的", - "outbox.historyEmpty": "尚無歷史", - "outbox.historyN": "歷史({n})", - "outbox.backActive": "回進行中({n})", - "outbox.progress": "進度 {progress}", - "outbox.detail": "詳情", - "outbox.deleting": "刪除中…", - "outbox.confirmDelete": "刪除發送項目「{title}」?\n無法復原。", - "outbox.deleted": "已刪除「{title}」", - "outbox.deleteFail": "刪除失敗", - "outbox.loadFail": "載入發送列表失敗", - "outbox.status.scheduling": "排程中", - "outbox.status.active": "發送中", - "outbox.status.completed": "已完成", - "outbox.status.partial_failed": "部分失敗", - "outbox.status.cancelled": "已取消", - "outbox.detail.missingId": "缺少 id", - "outbox.detail.notFound": "找不到發送", - "outbox.detail.loadFail": "載入發送內容失敗", - "outbox.detail.loading": "載入中…", - "outbox.detail.sendingHint": "正在發到 Threads(通常 10~30 秒),頁面會自動更新…", - "outbox.detail.doneHint": "已成功發到 Threads。", - "outbox.detail.markAllOk": "標記全部成功", - "outbox.detail.markRootFail": "標記主貼失敗", - "outbox.detail.processing": "處理中…", - "outbox.detail.delete": "刪除這筆", - "outbox.detail.back": "返回列表", - "outbox.detail.root": "主貼", - "outbox.detail.replyN": "回覆 {n}", - "outbox.detail.retry": "重試", - "outbox.detail.opFail": "操作失敗", - "outbox.step.published": "已發佈", - "outbox.step.failed": "失敗", - "outbox.step.publishing": "發佈中…", - "outbox.step.scheduled": "排程中", - "outbox.step.blocked": "已阻擋", - - "studio.title": "創作", - "studio.account": "帳號", - "studio.persona": "人設", - "studio.personaReady": "人設 ready", - "studio.personaNotReady": "人設未就緒", - "studio.tab.posts": "我的貼文", - "studio.tab.mentions": "提及 @", - "studio.tab.compose": "寫一則", - "studio.tab.plays": "互回方案", - "studio.tab.inspire": "靈感", - "studio.tab.insights": "成效", - - "mentions.hint": "誰 @ 你。待回 {n} 則。每則可改帳號/人設(預設用頂部)。", - "mentions.scoutLink": "海巡外展", - "mentions.empty": "尚無提及", - "mentions.emptyHint": "按「從 Threads 同步」拉取別人 @ 你的貼文/回覆/引用。若權限不足請到設定重新連帳。", - "mentions.needAccount": "請先在頂部選 Threads 帳號", - "mentions.sync": "從 Threads 同步", - "mentions.syncing": "同步中…", - "mentions.syncDone": "已同步 {n} 則提及", - "mentions.syncFail": "同步失敗:請確認已連帳,且 OAuth 含 threads_manage_mentions(可能需重新連帳)", - "mentions.openThread": "開原文", - "mentions.status.pending": "待回", - "mentions.status.replied": "已回", - "mentions.status.skipped": "略過", - "mentions.reply": "回覆", - "mentions.skip": "略過", - "mentions.draftLabel": "回覆草稿", - "mentions.repliedPrefix": "已回:{text}", - "mentions.needPersona": "請選 ready 人設再 AI 產文", - "mentions.fail": "失敗", - "mentions.marked": "已將這則提及標記為已回覆", - "mentions.markReplied": "標記為已回覆", - "mentions.markingReplied": "標記中…", - "mentions.withImages": " · 附圖 {n}", - - "compose.hint": "純發文:寫正文後送出 Outbox(非串場)。互回請用", - "compose.hintEnd": "。", - "compose.playsLink": "串場", - "compose.personaOff": "人設未 ready:仿寫/分析等 AI 工具停用。", - "compose.title": "標題(選填)", - "compose.titlePh": "方便在 Outbox 辨識", - "compose.body": "正文", - "compose.bodyPh": "寫下這則貼文…", - "compose.bodyCount": "{n} 字", - "compose.bodyLongWarning": "完整草稿已保留,但可能超過 Threads 單則可發布長度", - "compose.topicTag": "話題標籤(Threads tag)", - "compose.topicTagPh": "例如 寵物展(可不加 #)", - "compose.topicTagHint": "Threads 話題標籤,每則最多一個;1~50 字,勿含 . 或 &。也可在正文寫 #標籤。", - "compose.whoCanReplyHint": "發文時寫入 Threads。已發布的貼文無法再用 API 修改。", - "compose.tool.mimic": "仿寫", - "compose.tool.viral": "爆紅分析", - "compose.tool.research": "上網補資料", - "compose.tool.image": "產圖", - "compose.mimic.title": "仿寫別人貼文", - "compose.mimic.source": "參考全文", - "compose.mimic.sourcePh": "貼上想仿寫的貼文…", - "compose.mimic.direction": "新主題/新角度(可留空)", - "compose.mimic.directionPh": "例如:改寫成『功能越少,產品反而越好用』的觀點…", - "compose.mimic.directionHint": "這是新貼文真正要談的內容。留空時 AI 會從參考文延伸不同角度,不會照原文換句話說。", - "compose.mimic.structureNotes": "結構分析(會帶進仿寫)", - "compose.mimic.structureNotesPh": "可從「我的貼文 → 分析結構」帶入;或手動貼鉤子/結構/可複製點…", - "compose.mimic.structureNotesHint": "只借用敘事骨架、轉折與情緒曲線;內容依新方向重寫,語氣使用目前選擇的人設。", - "compose.mimic.broughtAnalysis": "已帶入參考貼文 + 結構分析,可直接仿寫或再改備註", - "compose.mimic.broughtSource": "已帶入參考貼文(尚未有結構分析;可先回我的貼文按「分析結構」)", - "compose.mimic.running": "仿寫中(背景任務,可離開本頁)…", - "compose.mimic.run": "依人設仿寫到正文", - "compose.mimic.queued": "已排程仿寫任務,完成後會自動填入正文", - "compose.mimic.done": "仿寫完成,可再改", - "compose.mimic.doneWithStructure": "仿寫完成(已套用結構分析骨架),可再改", - "compose.mimic.jobFail": "仿寫任務失敗:{err}", - "compose.viral.title": "爆紅分析", - "compose.viral.hint": "分析參考文或目前正文的鉤子/結構/可複製點。", - "compose.viral.source": "分析對象(可空=用正文)", - "compose.viral.running": "分析中…", - "compose.viral.run": "開始分析", - "compose.viral.result": "分析結果", - "compose.viral.done": "爆紅分析完成", - "compose.viral.needText": "請貼參考文或先寫正文", - "compose.research.title": "上網補專業資料", - "compose.research.q": "關鍵字", - "compose.research.qPh": "例如:無香洗劑 敏感肌", - "compose.research.running": "搜尋中…", - "compose.research.insert": "插入勾選內容到正文", - "compose.research.inserted": "已插入 {n} 條補充", - "compose.image.title": "產圖", - "compose.image.prompt": "畫面描述", - "compose.image.promptPh": "可空=從正文摘要", - "compose.image.running": "產圖中…", - "compose.image.run": "產生圖片", - "compose.image.done": "已產圖", - "compose.scheduleAt": "預計發送時間", - "compose.scheduleHint": "到點後由 Outbox 依序送出;不可早於現在。", - "compose.scheduleHintNow": "立即發送(到點=現在,送出時以當下為準)。", - "compose.schedulePast": "預計發送時間已過期,請改為現在或未來時間。", - "compose.scheduleNow": "設為現在", - "compose.publish": "送出到 Outbox", - "compose.publishing": "送出中…", - "compose.uploadingImages": "上傳圖片 {n}/{total}…", - "compose.uploadImageFail": "圖片上傳失敗,請重試或換較小的圖(≤5MB)", - "compose.waitImageUpload": "圖片還在上傳,請稍候再送出。", - "compose.waitImageUploadBtn": "圖片上傳中…", - "compose.imageUploadNeedRetry": "有圖片上傳失敗,請點縮圖上的重試後再送出。", - "compose.publishFail": "送出失敗", - "compose.fail": "失敗", - "compose.attachN": "附圖 {n}", - "compose.personaStatus": "人設:{status}", - "compose.ready": "ready", - "compose.notReady": "未就緒", - - "posts.sync": "重新同步 Threads", - "posts.syncing": "同步中…", - "posts.syncedAt": "同步 {time}", - "posts.notSynced": "未同步", - "posts.syncDone": "已從 Threads 同步 {n} 則貼文(含成效與留言)", - "posts.syncFail": "同步失敗,請確認已連帳且權限足夠(可能需重新 OAuth)", - "posts.loadingReplies": "載入留言中…", - "posts.loadRepliesFail": "載入留言失敗", - "posts.empty": "尚無貼文", - "posts.openThreads": "開 Threads", - "posts.whoCanReply": "誰可以回覆", - "posts.replyControl.everyone": "所有人", - "posts.replyControl.accounts_you_follow": "你追蹤的帳號", - "posts.replyControl.mentioned_only": "僅被提及的人", - "posts.replyControl.parent_post_author_only": "僅原po", - "posts.replyControl.followers_only": "僅追蹤者", - "posts.replyControlUpdated": "已更新為「{label}」。請到 Threads 確認。", - "posts.replyControlFail": "更新誰可以回覆失敗", - "posts.replyControlPublishOnly": "Threads 只能在發文時設定誰可以回覆。已發布貼文無法用 API 修改,請到創作頁發一則新貼文。", - "posts.hideReply": "隱藏回覆", - "posts.unhideReply": "取消隱藏", - "posts.hidingReply": "處理中…", - "posts.replyHidden": "已在 Threads 隱藏這則回覆。請到 Threads 確認。", - "posts.replyUnhidden": "已在 Threads 取消隱藏。請到 Threads 確認。", - "posts.hideFail": "隱藏/取消隱藏失敗", - "posts.hiddenBadge": "已隱藏", - "posts.insight": "分析洞察:{text}", - "posts.review": "覆盤:{text}", - "posts.formulaResult": "結構分析結果", - "posts.analyzedBadge": "已分析", - "posts.noText": "(此則無文字/純媒體)", - "posts.collapseReplies": "收合留言", - "posts.repliesBtn": "留言({total})· 未回 {pending}", - "posts.replyRoot": "回主貼", - "posts.analyzing": "分析中…", - "posts.reanalyze": "重新分析結構", - "posts.analyze": "分析結構", - "posts.mimicThis": "仿寫這則", - "posts.rootDraft": "回主貼草稿", - "posts.filter.pending": "未回覆({n})", - "posts.filter.replied": "已回覆({n})", - "posts.filter.all": "全部({n})", - "posts.noPending": "沒有未回覆留言", - "posts.noReplied": "還沒有已回覆留言", - "posts.noReplies": "尚無留言", - "posts.status.pending": "未回覆", - "posts.status.replied": "已回覆", - "posts.likesN": "讚 {n}", - "posts.childCount": "{n} 則子留言", - "posts.mine": "我方", - "posts.replyThis": "回這則", - "posts.replyAgain": "再回一則", - "posts.replyTo": "回 @{user}", - "posts.replyAgainTo": "再回 @{user}", - "posts.needPersona": "請先選 ready 人設再 AI 產文", - "posts.genFail": "生成失敗", - "posts.needText": "請先產生或輸入回覆", - "posts.needAccount": "請選擇要送出的 Threads 帳號", - "posts.sending": "送出到 Threads 中…", - "posts.sent": "已用 @{user} 發到 Threads", - "posts.sentImages": "(附圖 {n})", - "posts.accountFallback": "帳號", - "posts.sendFail": "發送失敗", - "posts.analyzeDone": "結構分析完成(手動觸發)", - "posts.analyzeFail": "分析失敗", - - "wizard.newTitle": "編互回劇本", - "wizard.editTitle": "編輯互回劇本", - "wizard.prev": "上一步", - "wizard.next": "下一步", - "wizard.err.topic": "請填主題", - "wizard.err.lead": "請選擇主帳號", - "wizard.err.leadUnusable": "主帳號不可用", - "wizard.submitFail": "提交失敗", - "wizard.unnamedPlay": "未命名串場", - "wizard.step.topic": "聊什麼", - "wizard.step.crew": "誰出場", - "wizard.step.script": "誰說什麼", - "wizard.step.preview": "預覽", - "wizard.step.schedule": "何時發", - "wizard.step.submit": "送出", - "wizard.stepperAria": "wizard 步驟", - "wizard.topic.title": "1. 這串要聊什麼", - "wizard.topic.name": "標題(可選)", - "wizard.topic.namePh": "例如:週末咖啡", - "wizard.topic.topic": "主題一句話", - "wizard.topic.topicPh": "這串文想聊什麼?", - "wizard.topic.aiView": "AI 視角", - "wizard.topic.personaNotReady": "人設未就緒", - "wizard.topic.quickFill": "快速填入", - "wizard.topic.sampleTitle": "週末咖啡話題", - "wizard.topic.sampleTopic": "週末想找間不踩雷的咖啡店,插座要多、能坐久。", - "wizard.crew.title": "2. 出場", - "wizard.crew.lead": "主帳", - "wizard.crew.noUsable": "尚無可用帳號", - "wizard.crew.unusable": "不可用", - "wizard.crew.cast": "配角", - "wizard.script.title": "3. 台詞", - "wizard.script.persona": "人設", - "wizard.script.personaNotReady": "人設未就緒", - "wizard.script.root": "主貼", - "wizard.script.replyN": "回覆 {n}", - "wizard.script.generating": "生成中…", - "wizard.script.ai": "AI 產文", - "wizard.script.account": "帳號:{name}", - "wizard.script.noLead": "(未選 lead)", - "wizard.script.speaker": "發言帳號", - "wizard.script.leadTag": "(lead)", - "wizard.script.text": "文案", - "wizard.script.rootPh": "主貼內容…", - "wizard.script.replyPh": "回覆內容…", - "wizard.script.addReply": "新增回覆步驟", - "wizard.preview.title": "4. 預覽這段對話", - "wizard.preview.unknown": "未知", - "wizard.preview.unknownAccount": "未知帳號", - "wizard.preview.root": "主貼", - "wizard.preview.leadTalk": "lead 接話", - "wizard.preview.empty": "(空白)", - "wizard.schedule.title": "5. 何時發出去", - "wizard.schedule.start": "第一則(主貼)時間", - "wizard.schedule.interval": "回覆間隔(分鐘)", - "wizard.schedule.intervalHint": "相對上一步;送出後後端會加隨機抖動,避免節奏太精準", - "wizard.submit.title": "6. 送出排程", - "wizard.submit.body": "確認後會把「{title}」這串互回(共 {n} 步)送進 Outbox,依序用各帳號發出。", - "wizard.submit.unnamed": "未命名", - "wizard.submit.root": "主貼", - "wizard.submit.replyN": "回覆 {n}", - "wizard.submit.submitting": "提交中…", - "wizard.submit.run": "提交到 Outbox", - - "reply.account": "用哪個帳號回", - "reply.persona": "用人設", - "reply.notReady": "此人設未 ready,無法 AI 產文(仍可手打後發送)。", - "reply.draft": "回覆草稿", - "reply.attach": "附圖", - "reply.generating": "生成中…", - "reply.ai": "AI 產文", - "reply.sending": "發送中…", - "reply.send": "發送", - - "image.attach": "附圖", - "image.attachFail": "附圖失敗", - "image.attachedAria": "已附圖片", - "image.alt": "附圖", - "image.named": "附圖 {n}", - "image.remove": "移除圖片", - "image.full": "已滿 {max} 張", - "image.more": "再附圖({n}/{max})", - "image.uploading": "上傳中", - "image.uploadingN": "正在上傳 {n} 張圖…", - "image.uploadFail": "上傳失敗", - "image.uploadBadUrl": "上傳回應無效", - "image.retry": "重試", - - "metrics.aria": "貼文成效", - "metrics.like": "讚", - "metrics.reply": "回覆", - "metrics.repost": "轉發", - "metrics.quote": "引用", - "metrics.view": "瀏覽", - "metrics.share": "分享", - "metrics.type.quote": "引用貼", - "metrics.type.reply": "回覆貼", - "metrics.type.image": "圖片", - "metrics.type.video": "影片", - "metrics.type.carousel": "輪播", - "metrics.type.repost": "轉發", - "metrics.type.text": "文字", - "metrics.type.post": "貼文", - - "jobs.title": "任務", - "jobs.desc": "背景任務分三區:執行中、定期排程、歷史。每頁可調筆數,不會一次拉完全部。", - "jobs.startDemo": "產生測試任務", - "jobs.demoHint": "需 worker 執行;狀態會自動輪詢更新。", - "jobs.demoLabel": "Demo 測試任務", - "jobs.demoCreated": "已建立測試任務 {id}…,等待 worker 領取", - "jobs.demoFail": "無法建立測試任務", - "jobs.template.tokenRenew": "Threads Token 定期延長(約每 30 天)", - "jobs.template.tokenRenewCadence": "約每 30 天自動執行一次 · 無需手動操作", - "jobs.template.tokenRenewBadge": "定期 · 每 30 天", - "jobs.template.personaAnalyzeAccount": "人設分析 · 公開貼文", - "jobs.template.personaAnalyzeText": "人設分析 · 文字來源", - "jobs.template.composeMimic": "仿寫貼文", - "jobs.template.playGenerateScript": "劇本一次產全文", - "jobs.template.radarSweep": "商機巡邏", - "jobs.template.unknown": "其他任務", - "jobs.stripMore": "還有 {n} 個進行中…", - "jobs.nextRun": "下次執行:{time}", - "jobs.status.pending": "待處理", - "jobs.status.queued": "已排程", - "jobs.status.running": "執行中", - "jobs.status.succeeded": "已完成", - "jobs.status.failed": "失敗", - "jobs.status.cancelled": "已取消", - "jobs.status.cancel_requested": "取消中", - "jobs.loadFail": "無法載入任務列表", - "jobs.empty": "尚無任務", - "jobs.empty.active": "目前沒有執行中或待領取的任務", - "jobs.empty.recurring": "尚無定期/遠期排程任務", - "jobs.empty.history": "尚無歷史紀錄(已完成/失敗/取消)", - "jobs.tabsAria": "任務分類", - "jobs.tab.active": "執行中", - "jobs.tab.recurring": "定期任務", - "jobs.tab.history": "歷史已執行", - "jobs.recurringHint": "尚未到期的排程(例如 Token 約 30 天延長)。到期後會出現在「執行中」。", - "jobs.total": "共 {n} 筆", - "jobs.showing": " · 顯示前 {n}", - "jobs.detail": "詳情", - "jobs.loadMore": "載入更多(還有 {n})", - "jobs.notFound": "找不到任務", - "jobs.detailLoadFail": "載入任務失敗", - "jobs.progress": "進度 {n}%", - "jobs.updated": "更新 {time}", - "jobs.backList": "返回列表", - "jobs.backCompose": "回寫一則", - "jobs.mimicApplyCompose": "套用到寫一則", - "jobs.mimicApplyHint": "仿寫已完成。按下方按鈕回到單篇發文,正文會自動帶入。", - "jobs.mimicNoResult": "找不到仿寫結果,請再跑一次仿寫", - "jobs.mimicApplyFail": "套用失敗", - "jobs.delete": "刪除", - "jobs.deleteConfirm": "確定刪除「{name}」?此操作無法復原。", - "jobs.deleted": "已刪除任務", - "jobs.deleteFail": "無法刪除任務", - "jobs.deleteRunningHint": "執行中的任務無法刪除,請稍候完成。", - "jobs.retentionHint": "終態任務約保留 2 天後自動清除", - - "plans.title": "變更方案", - "plans.current": "目前方案", - "plans.perMonth": "/月", - "plans.monthlyCredits": "每月 {n} 點", - "plans.usageLink": "用量", - "plans.inUse": "使用中", - "plans.recommended": "推薦", - "plans.creditsPerMonth": "每月 {n} 點", - "plans.manage": "管理訂閱", - "plans.loadFail": "無法載入訂閱方案", - - "plan.cta.current": "目前方案", - "plan.cta.upgrade": "升級", - "plan.cta.downgrade": "降級", - "plan.cta.switch": "切換", - - "plan.free.headline": "夠用試用,體驗完整流程", - "plan.free.bullet1": "每月 {n} 點(約 2~3 週輕度日常)", - "plan.free.bullet2": "完整功能:創作、海巡、發送、生圖", - "plan.free.bullet3": "用得出價值再升級 Starter", - "plan.free.bullet4": "平台忙碌時可改填自己的 Key", - "plan.free.right1": "可使用完整功能:帳號、創作、海巡、發送、任務與靈感。", - "plan.free.right2": "點數夠你真實試跑文案、搜尋與幾次生圖,不是空殼 demo。", - "plan.free.right3": "達上限後升級 Starter 繼續;或設定自備 Key(BYOK)不占平台點。", - "plan.free.quota1": "每月配給 {n} 點。", - "plan.free.note1": "Free 使用者無需付款;付費使用者請由帳務入口管理取消。", - "plan.free.note2": "取消付費訂閱後,方案依帳務入口顯示的日期切換。", - - "plan.starter.headline": "小團隊日常發文與海巡", - "plan.starter.bullet1": "每月 {n} 點(約 5× Free)", - "plan.starter.bullet2": "穩定發文、回覆、海巡", - "plan.starter.bullet3": "適合 1~3 人節奏 · 付費主力", - "plan.starter.bullet4": "付款成功立即生效", - "plan.starter.right1": "付款成功後本帳改為 Starter,當月依新額度計算。", - "plan.starter.right2": "點數支撐固定發文、回覆草稿與定期海巡。", - "plan.starter.right3": "功能與 Free 相同,差在能用多久;日常節奏建議由此開始。", - "plan.starter.quota1": "每月 {n} 點 · {price}。", - "plan.starter.note1": "需付款成功才變更方案。", - "plan.starter.note2": "自然月重置,未用完點數不累積至下月。", - - "plan.pro.headline": "多帳、重度 AI 與研究", - "plan.pro.bullet1": "每月 {n} 點(約 3× Starter)", - "plan.pro.bullet2": "高頻文案/研究/生圖 · 重度天花板", - "plan.pro.bullet3": "適合代理與多品牌", - "plan.pro.bullet4": "付款成功立即生效", - "plan.pro.right1": "付款成功後本帳改為 Pro,當月依 Pro 額度計算。", - "plan.pro.right2": "適合多帳、大量回覆與深研究,減少中途額度見底。", - "plan.pro.right3": "功能相同;買的是容量。再高可改 BYOK,平台限流時也不中斷。", - "plan.pro.quota1": "每月 {n} 點 · {price}。", - "plan.pro.note1": "付款失敗不會改方案。", - "plan.pro.note2": "付款完成後可於帳務紀錄查詢收據。", - - "plan.quota2": "分項點數:文案 {copy}(約 {copyCalls} 次)、研究 {research}(約 {researchCalls} 次)、搜尋 {search}(約 {searchCalls} 次)、生圖 {image}(約 {imageCalls} 張)。", - "plan.softCapsLine": "分項點數:文案 {copy} · 研究 {research} · 搜尋 {search} · 生圖 {image}", - "plan.approxCallsLine": "約 {copyCalls} 次文案 · {researchCalls} 次研究 · {searchCalls} 次搜尋 · {imageCalls} 張圖", - - "checkout.title": "確認方案", - "checkout.pickFirst": "請先選擇方案。", - "checkout.viewPlans": "看方案", - "checkout.fail": "無法完成", - "checkout.confirmFree": "確認切換至 Free", - "checkout.payAndAction": "{action}並付款 {price}", - "checkout.subscribe": "訂閱", - "checkout.perMonth": "/月", - "checkout.monthlyCredits": "每月 {n} 點", - "checkout.youGet": "你會得到", - "checkout.quota": "額度", - "checkout.notes": "注意", - "checkout.amountDue": "應付金額", - "checkout.billedMonthly": "{name} · 按月計費", - "checkout.already": "已是此方案", - "checkout.processing": "處理中…", - "checkout.currentPlan": "目前方案", - "checkout.pickOther": "改選其他方案", - "checkout.cancel": "取消", - "checkout.invalidUrl": "付款服務回傳了不安全的網址,未進行跳轉。", - "checkout.redirecting": "正在前往 Stripe 安全付款頁面…", - "checkout.redirectingPortal": "正在前往 Stripe 訂閱管理頁面…", - "checkout.redirectFailed": "無法開啟 Stripe 頁面。請檢查瀏覽器或網路設定後再試一次。", - "checkout.networkError": "無法連線至帳務服務。請檢查網路後再試一次。", - "checkout.unavailable": "帳務服務目前尚未啟用或暫時無法使用,請稍後再試。", - "checkout.sessionExpired": "登入狀態已失效,請重新登入後再試。", - "checkout.portalUnavailable": "目前沒有可管理的 Stripe 訂閱。請先選擇付費方案。", - "checkout.verifying": "正在確認付款與方案生效狀態…", - "checkout.pollFail": "無法查詢付款狀態,請重試。", - "checkout.missingId": "缺少結帳編號,無法確認付款結果。", - "checkout.terminalFail": "結帳未完成({status})。你可以重新選擇方案再試一次。", - "checkout.timeout": "付款可能仍在處理中,但方案尚未於 30 秒內生效。請重試查詢;請勿重複付款。", - "checkout.retry": "重試查詢", - "checkout.canceledTitle": "已取消結帳", - "checkout.canceledBody": "未變更方案,也未執行任何扣款操作。", - "checkout.manageInstead": "此方案異動請在帳務入口管理,避免建立重複訂閱。", - - "usage.widget.titleUsed": "{name} · 已用 {used}/{cap} 點", - "usage.widget.titleUnlimited": "{name} · 不擋額度", - "usage.widget.ariaUsed": "已用 {used} 點,共 {cap} 點", - "usage.widget.dialog": "方案與用量", - "usage.widget.currentPlan": "目前方案", - "usage.widget.unlimited": "不擋額度", - "usage.widget.perMonth": "/月", - "usage.widget.monthUsage": "本月用量", - "usage.widget.remaining": "還剩 {n} 點", - "usage.widget.leftShort": "還剩 {n}", - "usage.widget.usedOfCap": "{used}/{cap}", - "usage.widget.overShort": "已超額", - "usage.widget.upgradeShort": "升級", - "usage.widget.upgrade": "升級方案", - "usage.widget.includes": "這個方案包含", - "usage.widget.nudge": "本月額度快用完了,升級可立刻加大點數。", - "usage.widget.changePlan": "變更方案", - "usage.widget.usageDetail": "用量明細", - - "usage.meter.ai_copy": "AI 文案", - "usage.meter.ai_research": "AI 研究", - "usage.meter.web_search": "搜尋", - "usage.meter.ai_image": "AI 生圖", - "usage.meter.barAria": "{label} 已用 {credits} 點/上限 {cap} 點({count} 次)", - "usage.ledger.costAria": "消耗 {n} 點", - "usage.event.keyMode.platform": "平台點數", - "usage.event.keyMode.byok": "自備 Key", - "usage.event.cost.credits": "−{n}", - "usage.event.cost.byok": "自備", - "usage.event.cost.byokAria": "使用自備 Key,不扣平台點", - "usage.event.label.unknown": "使用紀錄", - "usage.event.label.genericAi": "AI 呼叫", - "usage.event.label.personaAnalyzeText": "人設分析 · 文字", - "usage.event.label.personaAnalyzeAccount": "人設分析 · 公開帳號", - "usage.event.label.composeMimic": "仿寫貼文", - "usage.event.label.composeViral": "爆紅分析", - "usage.event.label.personaPreview": "人設試產", - "usage.event.label.ownPostReply": "自己貼文 · 回覆草稿", - "usage.event.label.mentionReply": "提及 · 回覆草稿", - "usage.event.label.inspireChat": "靈感聊天", - "usage.event.label.researchSearch": "研究搜尋", - "usage.event.label.generateImage": "產生圖片", - "usage.event.label.search": "網頁搜尋", - "usage.event.label.aiComplete": "AI 補全", - "usage.event.source.personaAnalyzeText": "人設分析 · 文字", - "usage.event.source.personaAnalyzeAccount": "人設分析 · 公開帳號", - "usage.event.source.composeMimic": "仿寫貼文", - "usage.event.source.composeViral": "爆紅分析", - "usage.event.source.personaPreview": "人設試產", - "usage.event.source.ownPostAnalyze": "自己貼文 · 結構分析", - "usage.event.source.ownPostReply": "自己貼文 · 回覆草稿", - "usage.event.source.mentionReply": "提及 · 回覆草稿", - "usage.event.source.inspireChat": "靈感聊天", - "usage.event.source.researchSearch": "研究搜尋", - "usage.event.source.generateImage": "產生圖片", - "usage.event.source.proxySearch": "網頁搜尋", - "usage.event.source.proxyAi": "AI 補全", - - "usage.chart.period": "區間", - "usage.chart.allocated": "配給", - "usage.chart.consumed": "消耗", - "usage.chart.pctTitle": "消耗佔配給比例", - "usage.chart.aria": "配給與消耗", - "usage.chart.colAria": "{label} 配給 {purchased} 消耗 {consumed}", - - "settings.provider": "Provider", - "settings.model": "模型", - "settings.aiUnifiedHint": "文案、研究、延伸全部使用同一 provider 與模型。", - "settings.fetchModels": "取得模型", - "settings.fetchingModels": "讀取中…", - "settings.apiKey": "API Key", - "settings.configured": "已設定", - "settings.notConfigured": "未設定", - "settings.platformKeyOk": "可用平台 key(可改填自己的)", - "settings.modelsHint": "模型清單", - "settings.modelsCached": "模型清單來自快取(約 5 分鐘)", - "settings.modelsLoaded": "已取得 {provider} 模型清單", - "settings.aiSaved": "AI 設定已儲存", - "settings.searchSaved": "搜尋設定已儲存", - "settings.clearAiKey": "清除自備 AI Key", - "settings.aiKeyCleared": "自備 AI Key 已清除", - "settings.clearExaKey": "清除 Exa Key", - "settings.exaKeyCleared": "Exa Key 已清除", - "settings.searchProvider": "搜尋 Provider", - "settings.expand": "延伸策略", - "settings.exaKey": "Exa API Key", - "settings.devMode": "測試海巡(本機工作階段)", - "settings.devModeHint": "開啟後,測試海巡可使用已同步的 Chrome 登入態。正式發文/留言仍走官方 API。", - "settings.ext.title": "Chrome 擴充套件", - "settings.ext.desc": "安裝此擴充(v1.2.0+),才能從 Chrome 同步 Threads 登入態到測試海巡。", - "settings.ext.step1": "下載並解壓縮 ZIP,得到 haixun-threads-sync 資料夾", - "settings.ext.step2": "Chrome 開啟 chrome://extensions,開啟「開發人員模式」", - "settings.ext.step3": "點「載入未封裝項目」,選擇解壓後的資料夾(已安裝則按「重新載入」)", - "settings.ext.step4": "在擴充選項填入此站網址(與網址列一致),再重新整理巡樓頁", - "settings.ext.download": "下載擴充套件(ZIP)", - "settings.ext.sessionTitle": "Chrome Session(測試海巡)", - "settings.ext.sessionHint": "從已登入的 Threads 分頁同步登入態,供測試海巡使用。正式發文仍走官方 API。", - "settings.ext.pageOrigin": "目前分頁:{origin}", - "settings.ext.detected": "擴充已偵測", - "settings.ext.notReady": "尚未偵測擴充", - "settings.ext.synced": "Session 已同步", - "settings.ext.notSynced": "Session 未同步", - "settings.ext.syncBtn": "從 Chrome 同步 Session", - "settings.ext.recheck": "重新偵測", - "settings.ext.syncOk": "Chrome 工作階段已同步到測試海巡", - "settings.ext.syncFail": "Chrome session 同步失敗", - "settings.ext.needLogin": "尚未登入,請先登入巡樓後再同步。", - "settings.ext.notDetected": "找不到巡樓 Chrome 擴充(頁面 {origin})。請用 v1.2.1+:chrome://extensions 重新載入 → 擴充選項填入同一網址並儲存 → 回此頁 F5。", - "settings.ext.reloadHint": "安裝或更新擴充後請重新載入擴充,再按 F5 刷新此頁。", - "settings.ext.detectSteps": "裝好了卻偵測不到?請依序:① chrome://extensions 確認「巡樓 Threads Session 同步」已啟用並按「重新載入」(需 v1.2.1)② 擴充「詳細資料/選項」把 Server URL 設成 {origin} 並儲存(彈窗按允許)③ 回此分頁硬重新整理(F5)。本機請勿混用 localhost 與 127.0.0.1。", - - "forgot.fail": "送出失敗", - "forgot.mockHint": "正式環境會寄到信箱;此處直接給連結:", - "forgot.checkInbox": "請檢查信箱(含垃圾郵件)。", - - "reset.mismatch": "兩次密碼不一致", - "reset.fail": "重設失敗", - "reset.cardTitle": "重設密碼", - "reset.redirecting": "即將前往登入頁…", - "reset.loginNow": "立即登入", - "reset.passwordPh": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - "reset.forgotLink": "重新申請重設", - - "verify.sendFail": "寄送失敗", - "verify.fail": "驗證失敗", - "verify.success": "信箱已驗證,可以使用巡樓了。", - "verify.codePh": "6 位數字", - "verify.currentAccount": "目前帳號:{email}", - - "login.brandTitle": "巡樓 · Lapras", - - "home.navLabel": "公開導覽", - "public.localeLabel": "介面語言", - "home.heroTitle": "在 Threads 找到客戶,一路跟到成交", - "home.heroLead": "每天自動整理需求名單,回覆與跟進都在同一張工作台。", - "home.outcomesTitle": "能做到什麼", - "home.outcome.find.title": "找到正在找你的人", - "home.outcome.find.body": "關鍵字訂閱每天自動掃;需要時也可手動掃一輪。", - "home.outcome.reply.title": "回得出、說得準", - "home.outcome.reply.body": "依你的服務與口吻產草稿,禁語不會亂講。", - "home.outcome.close.title": "跟得到成交", - "home.outcome.close.body": "同一人收斂成名單,階段與待追蹤不靠記憶。", - "home.productTitle": "主打功能", - "home.productLead": "從找需求到發送,都在同一張工作台。", - "home.preview.radar.title": "今日商機", - "home.preview.radar.caption": "訂閱關鍵字,每天自動整理需求名單", - "home.preview.scout.title": "海巡掃場", - "home.preview.scout.caption": "手動掃一輪,立刻鎖定可回覆貼文", - "home.preview.studio.title": "回覆與創作", - "home.preview.studio.caption": "依服務與口吻產草稿,禁語不亂講", - "home.preview.crm.title": "名單與階段", - "home.preview.crm.caption": "同一人收斂成一張卡,跟進不靠記憶", - "home.preview.outbox.title": "發送佇列", - "home.preview.outbox.caption": "排程、待送、已送一覽", - "home.preview.mock.radar.badge": "今日 · 高匹配", - "home.preview.mock.radar.meta": "3 則新需求", - "home.preview.mock.radar.row1": "台北|徵求搬家報價", - "home.preview.mock.radar.row2": "有人在問居家清潔方案", - "home.preview.mock.radar.row3": "接案設計 · 預算已備", - "home.preview.mock.scout.badge": "關鍵字掃描", - "home.preview.mock.scout.hit": "求推:有推薦的會計師嗎", - "home.preview.mock.scout.snippet": "想找能處理小型公司報稅的…", - "home.preview.mock.scout.hit2": "有人用過某某 SaaS 嗎", - "home.preview.mock.studio.tab1": "靈感", - "home.preview.mock.studio.tab2": "回覆", - "home.preview.mock.studio.tab3": "排程", - "home.preview.mock.studio.draft1": "嗨,我看到你在找搬家協助——", - "home.preview.mock.studio.draft2": "我們主要服務大台北,可先估趟次與箱數。", - "home.preview.mock.studio.draft3": "(依你的服務檔與禁語產生)", - "home.preview.mock.crm.col1": "新線索", - "home.preview.mock.crm.col2": "洽談中", - "home.preview.mock.crm.col3": "成交", - "home.preview.mock.crm.foot": "待追蹤與到期提醒", - "home.preview.mock.outbox.r1": "回覆 · 今晚 20:00", - "home.preview.mock.outbox.r2": "主貼 · 待確認", - "home.preview.mock.outbox.r3": "已送 · 成效同步中", - "home.pricingTitle": "方案", - "home.pricingLead": "功能相同,差在每月平台點數。", - "home.pricingHoverHint": "把游標移到「每月點數」可看各方案能做什麼。", - "home.ctaLogin": "登入", - "home.privacyLink": "隱私權政策", - "home.termsLink": "服務條款", - "home.dataDeletionLink": "資料刪除說明", - "legal.footerNav": "法律與政策連結", - - "privacy.navLabel": "隱私權政策導覽", - "privacy.title": "隱私權政策", - "privacy.updated": "最後更新:2026-07-31", - "privacy.intro": - "本隱私權政策說明巡樓(Lapras,以下稱「本服務」或「我們」)如何蒐集、使用、儲存、分享與刪除你的個人資料與 Meta/Threads 平台資料。本政策適用於巡樓網頁主控台、公開工具頁,以及你透過 OAuth 連結之 Threads 帳號相關處理。使用本服務即表示你知悉本政策。", - "privacy.section.overview.title": "1. 控管者與適用範圍", - "privacy.section.overview.body": - "本服務由巡樓(Lapras)營運方作為個人資料控管者。\n本政策涵蓋:會員帳號、工作區內容、用量與付款識別、以及你授權本服務自 Meta/Threads API 取得之平台資料。\n若你透過邀請加入他人工作區,工作區擁有者可能另有內部規範,但不取代本政策對平台層與會員層資料之說明。\n本政策為我們自有政策,非 Meta、Threads 或 Instagram 之政策;Meta 如何處理其產品內資料,請另見 Meta 隱私中心。", - "privacy.section.collect.title": "2. 我們蒐集哪些資料", - "privacy.section.collect.body": - "(A)你直接提供的資料\n• 帳號:Email、顯示名稱、密碼雜湊(不保存明文密碼)、角色與信箱驗證狀態。\n• 你輸入的內容:人設、品牌、海巡意圖/關鍵字、創作草稿、回覆文案、Outbox 內容、工作區設定與上傳之媒體(若有)。\n• 支援與個資請求:你寄給我們的 Email 與請求內容。\n\n(B)自動蒐集\n• 登入 session、IP、基本瀏覽器/裝置資訊(安全、除錯、防濫用)。\n• 介面偏好(語言、主題)。\n• 功能用量與任務日誌(方案額度、錯誤診斷)。\n\n(C)來自 Meta/Threads 的平台資料(僅在你授權連結後)\n• Threads 使用者識別(例如 user id、username)、公開或授權範圍內的個人檔摘要。\n• 你授權範圍內的貼文、回覆、媒體中繼資料、以及為完成發送/讀取/成效所需之 API 回應。\n• 存取權杖(access token/refresh 相關憑證):僅用於代你呼叫 Threads API,與會員登入 JWT 分開存放。\n\n(D)付款(若啟用)\n• 方案等級、點數用量、金流交易識別(細節依金流供應商;我們不儲存完整卡號)。\n\n我們不會以一般使用為條件要求無關的政府身分證件。", - "privacy.section.use.title": "3. 處理目的與方式", - "privacy.section.use.body": - "我們處理上述資料的目的包括:\n• 提供帳號、工作區、權限與登入安全。\n• 執行核心功能:海巡掃描/外展、創作與 AI 輔助、Outbox 排程與發送、帳號與貼文同步、成效與用量計量。\n• 以你授權的 Threads token 代表你呼叫 Meta/Threads API(讀取授權內容、發布你確認之內容等,以實際 scope 為準)。\n• 寄送驗證碼、密碼重設與服務通知。\n• 防止濫用、保障服務穩定、除錯與產品改善(儘量去識別化)。\n• 遵守法律義務或回應合法請求。\n\n我們不會將 Meta/Threads 平台資料出售給資料掮客,也不會將該等資料用於與提供本服務無關的獨立行銷檔案建置。", - "privacy.section.threads.title": "4. Threads/Meta 平台資料", - "privacy.section.threads.body": - "連結方式:你透過 Meta OAuth 授權本服務存取 Threads。我們僅請求並使用完成產品功能所需之權限,例如讀取基本檔案、內容發布等;實際清單以授權畫面為準。\n\n用途限制:自 Meta 取得的平台資料,僅用於提供、維護與改善你所使用的巡樓功能(例如綁定帳號、同步貼文、排程/發送回覆或貼文、顯示狀態與除錯),不會轉售,亦不會用於與你授權無關的廣告定向。\n\n儲存與隔離:Threads 授權憑證與會員登入憑證分開管理;工作區與帳號層級有權限控管。\n\n你可隨時:\n• 在本服務解除 Threads 綁定;及/或\n• 在 Meta/Threads/Instagram 設定中撤銷本 App 授權。\n解除後我們停止新的 API 存取,並依第 8 節處理刪除或匿名化。\n\nMeta/Threads 自身如何處理資料,受 Meta 隱私政策與 Threads 補充條款拘束;本服務無法控制 Meta 端行為。", - "privacy.section.share.title": "5. 資料分享與處理者", - "privacy.section.share.body": - "我們可能在下列情況分享或由處理者代為處理資料:\n• 基礎設施:雲端主機、資料庫、物件儲存、電子郵件發送、監控與日誌服務(僅為營運本服務)。\n• AI/搜尋供應商:當你使用靈感、分析、建議文案等功能時,必要提示與內容片段可能傳送至供應商(見第 6 節)。\n• 金流供應商:處理付款時。\n• 法律要求:法院、主管機關之合法要求。\n• 業務承繼:合併、收購等情形下,依適用法律與通知處理。\n\n我們要求處理者僅依指示處理,並採取合理安全措施。我們不會出售個人資料。", - "privacy.section.ai.title": "6. AI 與自動化處理", - "privacy.section.ai.body": - "部分功能會將你提供的提示、貼文樣本、結構化備註或授權同步之公開內容摘要送至 AI/搜尋供應商,以產生建議文案、風格分析或檢索結果。\n你可使用平台預設供應商,或自行設定 BYOK(Bring Your Own Key);BYOK 時請求導向你指定的供應商,請同時閱讀其條款。\n我們不會在未經你另行同意下,把你的內容當作對外行銷素材或公開訓練語料宣傳。", - "privacy.section.retention.title": "7. 保存期間", - "privacy.section.retention.body": - "• 帳號與工作區資料:於帳號有效期間保存,以維持服務。\n• Threads token:於綁定有效期間保存;解除綁定或刪除請求後失效/移除。\n• 任務日誌、用量與安全紀錄:依營運、爭議處理與法令需要保存合理期間後刪除或匿名化。\n• 備份:可能在輪替週期內短暫殘留,期滿清除。\n詳細刪除步驟見第 8 節與「資料刪除說明」頁。", - "privacy.section.deletion.title": "8. 如何請求刪除資料", - "privacy.section.deletion.body": - "你可依下列方式請求刪除我們所持有、與你相關的個人資料與平台資料:\n\n方式一(建議):登入巡樓 → 會員/設定相關頁面解除 Threads 綁定,並依介面指示提出帳號或資料刪除(若已提供自助刪除)。\n\n方式二:寄信至本服務營運方,使用你註冊之 Email 寄出,主旨註明「資料刪除請求」或 Data Deletion Request,並提供:\n• 註冊 Email\n• 若知悉:Threads username 或本服務內顯示之帳號識別\n• 欲刪除範圍(全部帳號/僅 Threads 連線資料/特定工作區)\n\n方式三:在 Meta 移除本 App 並請求刪除時,依 Meta 流程處理;我們也會依平台回呼(若有)處理對應資料,否則請仍依方式二聯絡我們。\n\n更完整的步驟見本站「資料刪除說明」頁(/data-deletion)。\n我們於核對身分後,將在合理期間(通常 30 日內,法令另有規定從其規定)刪除或匿名化,法令要求保留者除外。", - "privacy.section.rights.title": "9. 你的權利", - "privacy.section.rights.body": - "在適用法律允許範圍內,你可要求:查詢、更正、下載(可攜)、刪除、限制或反對特定處理。你可隨時登出、修改個人資料、解除 Threads 綁定或停止使用。行使權利請依第 12 節聯絡我們;我們可能需驗證身分。", - "privacy.section.security.title": "10. 安全措施", - "privacy.section.security.body": - "我們採取合理技術與組織措施,包括 HTTPS 傳輸、密碼雜湊、權限隔離、會員憑證與 Threads token 分離存放、以及存取控管。無系統可保證絕對安全;若發生可能影響你權益之資安事件,我們將依法或依政策通知。", - "privacy.section.children.title": "11. 兒童與未成年人", - "privacy.section.children.body": - "本服務面向內容經營與商業/創作者使用,不以兒童為對象。我們不故意蒐集未滿適用年齡(例如 13 歲,或你所在地更高門檻)者之個人資料。若你認為兒童資料被誤收,請依第 12 節聯絡,我們將儘速刪除。", - "privacy.section.contact.title": "12. 聯絡我們", - "privacy.section.contact.body": - "隱私權、個資查詢、更正或刪除請求,請使用你註冊之 Email 與本服務營運方聯繫,主旨註明「Privacy/Data Request」,並說明帳號 Email 與請求內容,以便核對身分。\n你亦可先登入後於會員/設定頁依指示操作。", - "privacy.section.updates.title": "13. 政策更新", - "privacy.section.updates.body": - "我們可能因功能、Meta 平台規則或法令更新本政策。更新後會修改「最後更新」日期;重大變更時可能以站內通知或 Email 告知。在法律允許範圍內,更新後繼續使用表示你知悉修訂內容。", - "privacy.backHome": "回到介紹首頁", - - "terms.navLabel": "服務條款導覽", - "terms.title": "服務條款", - "terms.updated": "最後更新:2026-07-31", - "terms.intro": - "歡迎使用巡樓(Lapras)。本服務條款(Terms of Service)規範你與本服務營運方之間就使用網頁主控台、公開工具與相關功能之約定。使用本服務即表示你同意本條款;若不同意,請勿使用。", - "terms.section.acceptance.title": "1. 接受條款", - "terms.section.acceptance.body": - "你必須具備締結合約之法定能力,並遵守所在地法律。若你代表公司或組織使用,你保證有權使該組織受本條款拘束。", - "terms.section.service.title": "2. 服務說明", - "terms.section.service.body": - "巡樓提供 Threads 內容經營相關工具,包括但不限於帳號綁定、海巡與外展、創作與發送、人設/品牌、用量與方案等。功能可能隨版本調整;我們得基於維護、安全或法規暫停或變更部分功能。", - "terms.section.accounts.title": "3. 帳號與安全", - "terms.section.accounts.body": - "你應提供正確資料、妥善保管登入憑證,並對帳號下之行為負責。發現未授權使用應立即通知我們。我們得因安全、濫用或違規而限制或停權帳號。", - "terms.section.threads.title": "4. Threads/Meta 連線", - "terms.section.threads.body": - "部分功能需你透過 Meta OAuth 授權連結 Threads。你保證有權連結該帳號,並遵守 Meta、Threads、Instagram 之平台條款與社群規範。\n你應自行確保發送內容合法、不侵權、不違反平台政策。因你的內容或帳號行為導致平台處罰、API 拒絕或第三方主張,由你自行負責;本服務僅提供工具,不保證平台審核結果或觸及成效。\n你可以隨時解除綁定或撤銷 Meta 授權;解除後部分功能將無法使用。", - "terms.section.content.title": "5. 你的內容", - "terms.section.content.body": - "你保留對自己上傳或產生內容之權利。你授予我們為提供服務所需之非專屬、全球、免權利金授權(儲存、處理、傳輸、顯示給你與你授權之工作區成員、以及呼叫必要之第三方 API)。你保證內容不違法、不侵權。", - "terms.section.acceptable.title": "6. 禁止行為", - "terms.section.acceptable.body": - "禁止:濫用 API 或自動化干擾他人;散布垃圾、詐騙、仇恨或違法內容;規避用量或安全機制;逆向工程或未經授權存取系統;將服務用於違反 Meta 平台政策之行為;侵害他人隱私或智慧財產。", - "terms.section.billing.title": "7. 方案與用量", - "terms.section.billing.body": - "付費方案、點數與限制以產品內標示為準。未付款、超額或濫用可能導致功能降級或暫停。退款政策依購買時說明與適用法律。", - "terms.section.disclaimer.title": "8. 免責與責任限制", - "terms.section.disclaimer.body": - "服務依「現況」提供。在法律允許最大範圍內,我們不保證無中斷、無錯誤,或第三方平台(含 Threads/Meta/AI 供應商)持續可用。對間接、附隨、逸失利益等損害,除法律強制規定外不負責任;我們對你的總責任以你於事故前十二個月內就本服務已付費用為上限(若為免費使用則為零),法律禁止限制者除外。", - "terms.section.termination.title": "9. 終止", - "terms.section.termination.body": - "你可隨時停止使用並依隱私權政策請求刪除資料。我們得因違約、濫用、法律要求或停止營運而終止或暫停服務。終止後依隱私權政策處理資料。", - "terms.section.contact.title": "10. 聯絡與準據", - "terms.section.contact.body": - "條款相關問題請透過註冊 Email 聯絡營運方。準據法與管轄,除強制規定外,以本服務主要營運地之法律為準。隱私處理見 /privacy;資料刪除見 /data-deletion。", - - "deletion.navLabel": "資料刪除說明導覽", - "deletion.title": "使用者資料刪除說明", - "deletion.updated": "最後更新:2026-07-31", - "deletion.intro": - "本頁說明如何請求巡樓(Lapras)刪除與你相關的個人資料與 Threads 平台資料。", - "deletion.section.summary.title": "1. 摘要", - "deletion.section.summary.body": - "當你不再使用本服務,或自 Meta 移除本 App 並希望刪除我們所持資料時,請依下列步驟提出請求。我們會在核對身分後刪除或匿名化可刪除之資料(法令或正當利益需保留者除外)。", - "deletion.section.steps.title": "2. 如何提出刪除請求", - "deletion.section.steps.body": - "步驟 1:若仍可登入,先至巡樓解除所有 Threads 帳號綁定(設定/帳號相關頁)。\n步驟 2:使用你註冊本服務時的 Email 寄信給營運方。\n步驟 3:主旨請寫:「資料刪除請求」或 Data Deletion Request。\n步驟 4:信中請提供:\n• 註冊 Email(必填)\n• 顯示名稱或會員識別(若知悉)\n• Threads username 或本服務內帳號 id(若曾連結)\n• 刪除範圍:整個帳號/僅 Threads 連線與同步資料/特定工作區\n步驟 5:我們回覆確認後開始處理;完成後以 Email 告知結果(若該信箱仍可用)。", - "deletion.section.threads.title": "3. 與 Threads/Meta 授權的關係", - "deletion.section.threads.body": - "刪除本服務資料不會自動刪除你在 Threads/Instagram/Meta 上的貼文或帳號。\n請同時在 Meta/Threads 設定中撤銷對本 App 的授權,以停止 Meta 端的授權狀態。\n若平台端另有刪除回呼,我們會一併處理對應資料;否則請依本頁 Email 流程提出請求。", - "deletion.section.scope.title": "4. 會刪除什麼", - "deletion.section.scope.body": - "在合理可行範圍內,我們將刪除或匿名化:\n• 會員個人資料與登入憑證\n• 工作區內由你建立之內容(人設、草稿、海巡與 Outbox 等,依請求範圍)\n• Threads 連線識別、token 與同步快取\n• 非必要之任務與分析紀錄\n\n可能暫時或依法保留:\n• 法令要求之交易/帳務紀錄\n• 安全與防濫用日誌(有限期間)\n• 備份輪替中尚未到期之副本", - "deletion.section.timeline.title": "5. 處理時程", - "deletion.section.timeline.body": - "我們通常在收到可核對之請求後 30 日內完成刪除或匿名化。若需更長時間(例如複雜工作區或法遵審查),我們會告知預估時程。備份清除可能隨後完成。", - "deletion.section.contact.title": "6. 聯絡", - "deletion.section.contact.body": - "請使用註冊 Email 聯絡本服務營運方,主旨「Data Deletion Request」。完整隱私說明見 /privacy;服務條款見 /terms。", - - "common.listSep": "、", - "common.dash": "—", - - "scout.title": "話題靈感", - "scout.topic.intro": "輸入作品、人物、事件或工作方向;系統會判斷你想找的熱門討論、推薦或公開工作訊號。要持續管理客戶需求,請用「商機」。", - "scout.topic.termHint": "已依整體意圖預選最多 3 組;你仍可取消、改詞或加上自己的說法。", - "scout.topic.termsNeedShort": "有 {n} 組關鍵字不合 Threads 短詞規則,請改短後再搜。", - "scout.topic.noTerms": "沒產出可用關鍵字,請換個更具體的主題再試(例:台北 市集、保母 求推薦)。", - "scout.topic.termsReadyPrimary": "已理解輸入並預選最多 3 組高信心查詢,另有 {n} 組可調整。", - "scout.topic.workshopHintSelect": "這些詞會分別搜尋後再去重;已預選最多 3 組高信心查詢,你可以在送出前調整。", - "scout.topic.primaryTerm": "主查詢(建議)", - "scout.topic.variantTerm": "變體 {n}", - "scout.topic.useTerm": "使用此關鍵字搜尋", - "scout.today": "今日出擊", - "scout.purposeValue": "痛點回覆", - "scout.purposeDemand": "找需求痛點", - "scout.purposeProvider": "解法媒合", - "scout.purposeActivity": "活躍短回", - "scout.goal": "今日目標(則)", - "scout.progress": "進度 {done}/{goal}", - "scout.intent": "我想找/回應", - "scout.keyword": "關鍵字", - "scout.intentPh": "例:換季頭皮刺癢、真的無香、週末有插座", - "scout.keywordPh": "例:外包 工程師 後端、鬼滅之刃", - "scout.productOptional": "產品(選填)", - "scout.productRequired": "要解決的產品(必填)", - "scout.selectProduct": "選擇產品", - "scout.noProduct": "不帶產品", - "scout.brandFallback": "品牌", - "scout.placement": "置入:{label}", - "scout.providerProduct": "產品:{label}", - "scout.painPart": " · 痛點「{pain}」", - "scout.noProductsBefore": "尚無產品,可到", - "scout.noProductsAfter": "新增。", - "scout.start": "開始", - "scout.startMore": "再撈一批", - "scout.fetching": "撈取中…", - "scout.planKeywords": "產出關鍵字", - "scout.planning": "整理關鍵字…", - "scout.workshop": "搜尋關鍵字(可改)", - "scout.workshopHint": "確認後才會搜尋。每條是一組獨立 query;刪掉不準的、加上你知道有效的說法。", - "scout.workshopEmpty": "至少保留一條關鍵字才能搜尋", - "scout.addTerm": "新增", - "scout.addTermPh": "再加一條搜尋關鍵字", - "scout.removeTerm": "移除", - "scout.confirmScan": "用這些詞開始搜", - "scout.startImmediate": "立即開始海巡", - "scout.immediateHint": "立即開始會先自動整理建議詞,再用最多 3 組精準搜尋詞送出。", - "scout.openDailySchedule": "查看每日排程", - "scout.scheduleHint": "每日自動巡邏請到商機的巡邏設定。", - "scout.replan": "重新產出", - "scout.clearWorkshop": "取消", - "scout.termsReady": "已產出 {n} 條關鍵字,請確認後再搜。", - "scout.runs": "海巡批次", - "scout.runCount": "批次({n})", - "scout.runCreatedAt": "建立 {time}", - "scout.runSelectAria": "切換海巡批次", - "scout.runPending": "待回 {n} · ", - "scout.runDone": "已清完 · ", - "scout.runTotal": "({n} 則)", - "scout.runStatus.queued": "排隊中", - "scout.runStatus.running": "掃描中", - "scout.runStatus.succeeded": "已完成", - "scout.runStatus.failed": "失敗", - "scout.runStatus.cancelled": "已取消", - "scout.shortfall": "尚缺 {n} 則", - "scout.shortfallReason.source_exhausted": "來源已用盡", - "scout.shortfallReason.duplicate_exhausted": "重複太多", - "scout.shortfallReason.relevance_exhausted": "關聯性不足", - "scout.shortfallReason.source_unavailable": "來源暫不可用", - "scout.shortfallReason.limit_reached": "達到上限", - "scout.shortfallReason.unknown": "來源不足", - "scout.refreshRuns": "重新整理批次", - "scout.refreshingRuns": "整理中…", - "scout.runsRefreshed": "批次已重新整理", - "scout.newRunReady": "有新的海巡批次完成;目前閱讀位置未變,按重新整理批次查看。", - "scout.deleteRun": "刪除這一批", - "scout.deleting": "刪除中…", - "scout.now": "現在這一則", - "scout.emptyBatch": "這一批沒有待回。想每天自動收需求名單?用側欄「商機」訂閱關鍵字。", - "scout.draft": "回覆草稿", - "scout.draftPhActivity": "短回…", - "scout.draftPhValue": "共感 → 建議…", - "scout.sendAccount": "用哪個帳號送", - "scout.noAccount": "無可用帳號", - "scout.personaForRegen": "人設(再產用)", - "scout.notReady": "(未就緒)", - "scout.skip": "略過", - "scout.regen": "再產", - "scout.send": "發送", - "scout.openThreadsReply": "開啟 Threads 留言", - "scout.markManualDone": "已留言,標記完成", - "scout.manualReplyHint": "先確認草稿,開啟 Threads 後直接在原文底下留言;草稿會嘗試自動複製。完成後回到這裡標記完成。", - "scout.openedAndCopied": "已開啟 Threads,草稿也已複製;貼上留言後再回來標記完成。", - "scout.openedManual": "已開啟 Threads;留言完成後請回來標記完成。", - "scout.noPermalink": "這筆命中沒有可開啟的 Threads 原文連結。", - "scout.manualDone": "已標記人工留言完成 · 今日 {done}/{goal}", - "scout.manualDoneFail": "標記人工留言完成失敗", - "scout.resend": "重新送出", - "scout.sending": "發送中…", - "scout.needAccountBefore": "請先到", - "scout.needAccountAfter": "連線可用帳號。", - "scout.loadingKnowledge": "正在整理周邊知識…", - "scout.product": "產品", - "scout.queue": "命中紀錄 · {n}", - "scout.valueQueue": "痛點/產品接話 · {n}", - "scout.providerQueue": "解法提供者 · {n}", - "scout.demandQueue": "需求痛點 · {n}", - "scout.activityQueue": "活躍短回 · {n}", - "scout.noMatchesInQueue": "這個佇列暫無命中", - "scout.collapseQueue": "收合佇列", - "scout.expandQueue": "展開佇列", - "scout.noOtherPending": "沒有其他待回", - "scout.unnamedRun": "未命名批次", - "scout.thisRun": "此批次", - "scout.confirmDeleteRun": "刪除海巡批次「{label}」?\\n會一併刪除這批命中與周邊知識,無法復原。", - "scout.deletedRun": "已刪除批次「{label}」", - "scout.deleteRunFail": "刪除批次失敗", - "scout.err.runBusy": "這批次正在掃描或已完成,暫時不能刪除。", - "scout.err.notFound": "這批次或貼文已不存在,請重新整理批次。", - "scout.err.sourceUnavailable": "海巡來源目前無法使用,請稍後再試。", - "scout.needKeyword": "先填關鍵字", - "scout.needIntent": "先寫這次要找什麼", - "scout.productMissing": "所選產品不在列表中,請重新選擇", - "scout.providerSetupRequired": "解法媒合需要產品的痛點與至少一個標籤或解法能力詞;請到品牌頁補齊後再試。", - "scout.defaultLabel": "海巡", - "scout.newRunActivity": "新批次「{label}」· {n} 則待回", - "scout.newRunValue": "新批次「{label}」· {n} 則 · 請處理「現在這一則」", - "scout.knowledgeReady": "「{label}」周邊知識已備好 · {n} 則可學", - "scout.patrolFail": "這輪海巡失敗", - "scout.loadFail": "海巡資料載入失敗,請重新整理後再試。", - "scout.scanQueued": "已建立海巡任務「{label}」,完成後會提示新的批次。", - "scout.workerWaiting": "海巡任務仍在等待 worker。請確認 apps/backend worker 正在執行。", - "scout.scanReady": "海巡完成,找到 {n} 則待回。", - "scout.queued": "已交由 @{who} 的 Outbox 發送佇列處理 · 今日 {done}/{goal}", - "scout.scanJob": "海巡任務", - "scout.scanInProgress": "正在掃描", - "scout.crawlerSessionRequired": "測試海巡需要有效的 Chrome 工作階段。請在設定頁同步已登入 Threads 分頁的登入態。", - "scout.openSettings": "開啟設定", - "scout.source": "來源:Threads Keyword Search", - "scout.resultKeyword": "關鍵字:{tag}", - "scout.classification": "分類:{classification}", - "scout.postedAt": "發文:{time}", - "scout.postedUnknown": "貼文時間未知", - "scout.scannedAt": "掃入於 {time}", - "scout.createdAt": "建立於 {time}", - "scout.openPermalink": "在 Threads 開啟原文", - "scout.draftFail": "產草稿失敗", - "scout.skipped": "已略過", - "scout.status.new": "待處理", - "scout.status.drafted": "已起草", - "scout.status.queued": "發送佇列中", - "scout.status.published": "已發送", - "scout.status.skipped": "已略過", - "scout.noDraft": "沒有可發送的草稿", - "scout.accountFallback": "帳號", - "scout.sent": "已發送(@{who})· 今日 {done}/{goal}", - "scout.sendFail": "發送失敗", - "scout.confirmDeletePost": "刪除這則命中?", - "scout.deletedPost": "已刪除這則命中", - "scout.stanceActivity": "短回 · 養活躍", - "scout.stanceDemand": "需求痛點 · 可回應", - "scout.stanceProvider": "解法媒合 · 不推產品", - "scout.demandHint": "這是正在求助或比較解法的需求貼文。先閱讀原文,再以有幫助的方式回應。", - "scout.providerHint": "這是解法提供者候選名單。請先看原文與能力證據,再自行決定是否聯絡。", - "scout.stanceProduct": "共感 · 可輕帶產品", - "scout.stanceRelation": "接話 · 建關係", - - "brands.title": "品牌", - "brands.railAria": "品牌列表", - "brands.railLabel": "你的牌子", - "brands.add": "新增", - "brands.brandName": "品牌名稱", - "brands.brandNamePh": "例如:自家品牌", - "brands.creating": "建立中…", - "brands.createBrand": "建立品牌", - "brands.searchAria": "搜尋品牌", - "brands.searchPh": "搜尋品牌…", - "brands.empty": "尚無品牌", - "brands.noMatch": "無符合", - "brands.selectAria": "選擇品牌", - "brands.pickOne": "選一個品牌", - "brands.inUseHint": "使用中 · 海巡與創作會套用此牌", - "brands.inUse": "使用中", - "brands.tabBrands": "品牌庫", - "brands.tabInfo": "牌子資料", - "brands.tabProducts": "產品", - "brands.tabProductsN": "產品({n})", - "brands.displayName": "名稱", - "brands.brief": "摘要", - "brands.briefPh": "一句話說明這個牌子", - "brands.audience": "受眾", - "brands.audiencePh": "誰會在意、為什麼", - "brands.goals": "目標", - "brands.goalsPh": "想在 Threads 達成什麼", - "brands.saving": "儲存中…", - "brands.deleteBrand": "刪除品牌", - "brands.searchProductAria": "搜尋產品", - "brands.searchProductPh": "搜尋產品…", - "brands.addProduct": "新增產品", - "brands.noProducts": "尚無產品", - "brands.hasLink": "有連結", - "brands.painLabel": "痛點 ", - "brands.editProduct": "編輯產品", - "brands.newProduct": "新增產品", - "brands.importFromUrl": "從商品連結帶入", - "brands.fetching": "抓取中…", - "brands.fetch": "抓取", - "brands.pains": "痛點", - "brands.painsPh": "一列一個", - "brands.tags": "標籤", - "brands.tagsPh": "逗號分隔", - "brands.intro": "介紹", - "brands.providerCapabilities": "可解決痛點的能力/服務", - "brands.providerCapabilitiesPh": "例如:皮膚科、過敏原檢測、敏感肌諮詢", - "brands.providerExcludes": "同類型排除詞", - "brands.providerExcludesPh": "例如:洗髮精、護髮產品", - "brands.link": "連結", - "brands.update": "更新", - "brands.createItem": "新增", - "brands.needName": "請輸入名稱", - "brands.created": "已建立「{name}」", - "brands.createFail": "建立失敗", - "brands.saved": "已儲存", - "brands.saveFail": "儲存失敗", - "brands.confirmDelete": "確定刪除「{name}」?", - "brands.deleted": "已刪除", - "brands.deleteFail": "刪除失敗", - "brands.fetchFail": "抓取失敗", - "brands.needLabelContext": "名稱與介紹為必填", - "brands.productUpdated": "已更新", - "brands.productAdded": "已新增", - "brands.confirmDeleteProduct": "刪除此產品?", - - "insights.title": "帳號成效", - "insights.account": "帳號", - "insights.noAccount": "尚無帳號", - "insights.syncing": "同步中…", - "insights.syncPosts": "同步貼文", - "insights.myPosts": "我的貼文", - "insights.pickAccount": "選擇帳號", - "insights.goAccounts": "帳號", - "insights.kpiMonth": "本月指標", - "insights.monthViews": "本月瀏覽", - "insights.monthLikes": "本月讚", - "insights.monthReplies": "本月回覆", - "insights.engRate": "互動率", - "insights.vsPrev": "vs 上月", - "insights.avgNear": "近帖均 {rate}", - "insights.trendTitle": "趨勢與分析 · @{user}", - "insights.metricViews": "瀏覽", - "insights.metricLikes": "讚", - "insights.metricReplies": "回覆", - "insights.metricPosts": "貼文", - "insights.metricPostsFull": "貼文數", - "insights.chartMetrics": "圖表指標", - "insights.barsAria": "近月{metric},點柱查看該月分析", - "insights.barsLabel": "{metric} · 近 {n} 個月", - "insights.clickBar": " · 點柱看分析", - "insights.pickMonthAria": "選擇月份", - "insights.barTitle": "{label}:{value}{est} · 點看分析", - "insights.est": "(估)", - "insights.monthSuffix": "{m}月", - "insights.sparkAria": "趨勢折線,點節點可選月", - "insights.analysisOf": "{label} 分析", - "insights.producedAt": "產出於 {time}", - "insights.hasEstimate": " · 含估測數據", - "insights.viewsVsPrev": " · 瀏覽 vs 前月 {delta}", - "insights.statPosts": "貼文", - "insights.statViews": "瀏覽", - "insights.statLikes": "讚", - "insights.statReplies": "回", - "insights.conclusions": "結論", - "insights.recommendations": "建議", - "insights.highlights": "當月亮點", - "insights.findTopics": "找話題", - "insights.goScout": "去探查", - "insights.selectMonth": "選擇月份", - "insights.topPosts": "表現較佳貼文", - "insights.noPosts": "尚無貼文", - "insights.postStats": "瀏覽 {views} · 讚 {likes} · 回 {replies}", - "insights.openThreads": "開啟 Threads", - "insights.zeroPct": "0%", - "insights.panelHint": "依「我的貼文」同步數據聚合本月成效與近月趨勢", - "insights.lastSynced": "上次同步 {time}", - "insights.neverSynced": "尚未同步", - "insights.emptyTitle": "尚無貼文數據", - "insights.emptyDesc": "先按「同步貼文」從 Threads 拉入你的貼文與成效,再看月比圖與分析。", - "insights.syncDone": "已同步 {n} 則貼文,成效已更新", - "insights.syncFail": "同步失敗", - "insights.loadFail": "載入貼文失敗", - "insights.zeroViewsHint": "已有貼文但瀏覽多為 0:可能 Insights 權限不足或尚未產出,可再按一次同步。", - "insights.postsInMonth": "{n} 則", - "insights.kpiForMonth": "{label} 指標", - "insights.topPostsOfMonth": "{label} · 表現較佳貼文", - "insights.noPostsInMonth": "{label} 尚無貼文", - "insights.pastMonthEmptyHint": "該月沒有已同步的貼文(過去月份只會自動補抓一次)。可手動同步或改選其他月。", - "insights.pastBackfillDone": "已補抓歷史貼文 {n} 則(過去月份只抓一次)", - "insights.autoRefreshDone": "本月成效已更新({n} 則)", - "insights.noDataNoAnalysis": "{label} 沒有貼文數據,不產生結論。", - "insights.noAnalysisYet": "{label} 尚無可寫的結論(需有已同步貼文)。", - "insights.thisMonth": "本月", - "insights.narrative.summary": "{when}彙總:貼文 {posts}、瀏覽 {views}、讚 {likes}、回覆 {replies}(來自已同步貼文)。", - "insights.narrative.viewsDelta": "瀏覽較前月 {delta}({prev} → {curr})。", - "insights.narrative.viewsFlat": "瀏覽與前月大致持平({delta})。", - "insights.narrative.repliesUp": "回覆數 {delta},對話熱度上升。", - "insights.narrative.repliesDelta": "回覆數 {delta}。", - "insights.narrative.engRate": "互動率約 {pct}%(讚+回+轉+引用+分享/瀏覽)。", - "insights.narrative.engLow": "互動率偏低:可多試帶明確條件的提問收尾。", - "insights.narrative.engGood": "互動率不錯:可複製高表現貼的結構再測 1~2 則。", - "insights.narrative.zeroViews": "此月有讚/回覆,但瀏覽為 0(Insights 可能尚未回傳或權限不足)。", - "insights.narrative.highlight": "表現較佳之一:{snippet}", - "insights.narrative.smallSample": "該月貼文偏少,樣本小,月比僅供參考。", - - "plays.tabOwn": "我的貼文", - "plays.tabLink": "Threads 連結", - "plays.noPosts": "尚無貼文", - "plays.targetPost": "目標貼文", - "plays.likesSuffix": " (讚{n})", - "plays.linkCard": "貼 Threads 連結", - "plays.postLink": "貼文連結", - "plays.resolving": "解析中…", - "plays.resolve": "解析連結", - "plays.resolveHint": "解析後可排自家帳號在該則下面回覆。", - "plays.targetOwn": "目標貼文(自己的)", - "plays.openThreads": "開 Threads", - "plays.external": "外站貼", - "plays.addScheme": "新增方案", - "plays.schemeCount": "此目標目前 {n} 個方案", - "plays.noSchemes": "尚無方案", - "plays.replyCount": "{n} 則留言", - "plays.editTitle": "編輯:{title}", - "plays.schemeName": "方案名稱", - "plays.schemeNamePh": "例如:方案 A · 溫和接話", - "plays.speakersOwn": "可出場帳號(貼主帳固定可回)", - "plays.speakers": "可出場帳號", - "plays.postOwner": "(貼文主帳)", - "plays.noAccounts": "沒有可用帳號,請先到設定連線 Threads。", - "plays.interval": "間隔(分)", - "plays.applyInterval": "套用間隔", - "plays.aiEmpty": "空白則 AI", - "plays.aiBusy": "產文中…", - "plays.aiFail": "AI 產文失敗", - "plays.aiStepDone": "已產好此步,可再改", - "plays.aiNoneFilled": "沒有可產的空白步驟(或人設未就緒)", - "plays.needPersonaForStep": "此步請先選就緒人設,才能 AI 產文", - "plays.aiEmptyResult": "AI 回傳空白,請重試或換較快的模型", - "plays.saveBeforeAi": "請先「儲存方案」,再一次產全文(背景任務需要 play id)", - "plays.scriptJobQueued": "已排程一次產全文(背景任務,可離開;完成後步驟會自動填上)", - "plays.scriptJobDone": "劇本產文完成,已填入各步驟(可再改)", - "plays.scriptJobDoneReload": "劇本產文完成,請重新開啟方案查看", - "plays.replies": "留言({n})", - "plays.stepN": "第 {n} 則", - "plays.who": "誰留", - "plays.personaOpt": "人設(選填)", - "plays.brandOpt": "品牌(選填)", - "plays.reply": "留言", - "plays.attach": "附圖", - "plays.addOne": "加一則", - "plays.saving": "儲存中…", - "plays.saveScheme": "儲存方案", - "plays.submitting": "送出中…", - "plays.submitOutbox": "送出到 Outbox", - "plays.closeEdit": "關閉編輯", - "plays.noTarget": "還沒有目標貼文", - "plays.resolved": "已解析連結", - "plays.resolveFail": "解析失敗", - "plays.filled": "已產 {n} 則", - "plays.needTarget": "請先選定目標貼文", - "plays.saved": "方案已儲存", - "plays.saveFail": "儲存失敗", - "plays.submitted": "已送進 Outbox", - "plays.submitFail": "送出失敗", - "plays.confirmDelete": "刪除此方案?", - "plays.accountFallback": "帳號", - - "inspire.loading": "載入中…", - "inspire.trendsAria": "話題靈感", - "inspire.trendsLabel": "話題靈感", - "inspire.trendsHint": "網搜彙整,非官方熱搜 · 找靈感會扣搜尋點數", - "inspire.trendsSeed": "示意", - "inspire.topicSeed": "想寫跟「{topic}」有關的 Threads,幫我發想開場與角度。", - "inspire.trendsEmpty": "點「找靈感」才會搜尋(扣點)", - "inspire.refreshConfirm": "找靈感會消耗 1 次「搜尋」點數,確定?", - "inspire.refreshOk": "已更新 {n} 則話題靈感", - "inspire.refreshFail": "找靈感失敗(點數不足或搜尋失敗)", - "inspire.refresh": "找靈感", - "inspire.clearChat": "新對話", - "inspire.clearedNewSession": "已開新對話", - "inspire.sessionsAria": "靈感對話列表", - "inspire.session": "對話", - "inspire.sessionNew": "新對話", - "inspire.newSession": "+ 新對話", - "inspire.newSessionOk": "已開新對話(舊的還在列表)", - "inspire.deleteSession": "刪除目前對話", - "inspire.deleteSessionShort": "刪除", - "inspire.confirmDeleteSession": "刪除目前對話?此則聊天會永久移除。", - "inspire.deletedSession": "已刪除,已切到其他對話", - "inspire.pinAsElement": "套用為元素", - "inspire.you": "你", - "inspire.ai": "AI", - "inspire.system": "系統", - "inspire.useDraft": "用這則寫", - "inspire.openPlay": "開串場", - "inspire.thinking": "思考中…", - "inspire.stop": "停止產生", - "inspire.stopped": "已停止產生", - "inspire.pinnedAria": "本輪參考(給 AI 看)", - "inspire.pinned": "本輪參考", - "inspire.pinnedCount": "· {n}", - "inspire.pinsLocalShort": "本次工作階段", - "inspire.pinsSessionLocal": "參考項目只套用於本次工作階段;送出訊息時會一併帶入。", - "inspire.pickRight": "右側點選=掛給 AI;點名稱可插入輸入", - "inspire.unpinTitle": "取消參考", - "inspire.insertPinTitle": "插入主輸入", - "inspire.insertBrand": "聊聊「{name}」", - "inspire.insertedPin": "已插入「{name}」到輸入框", - "inspire.flowStep1": "備料/參考", - "inspire.flowStep2": "聊天發想", - "inspire.flowStep3": "用人設定稿", - "inspire.emptyTitle": "先聊清楚,再用人設定稿", - "inspire.emptyDesc": "選一條路開始。不需要先懂全部按鈕。", - "inspire.entryTopic": "從一句話開始發想", - "inspire.entryPaste": "已有草稿,直接用人設改寫", - "inspire.startTopicHint": "在下方輸入主題或想法,Enter 送出", - "inspire.pasteDraftHint": "把草稿貼進「待改寫內容」,再按用人設改寫", - "inspire.needMaterialOrPaste": "沒有聊天素材時,請直接貼上要改寫的文字", - "inspire.flowOneLiner": "先聊清楚;需要資料時開啟查資料,最後一鍵整理成貼文。", - "inspire.showTopics": "找題材", - "inspire.hideTopics": "收起題材", - "inspire.showLibrary": "參考庫", - "inspire.hideLibrary": "收起庫", - "inspire.showAdvanced": "進階", - "inspire.hideAdvanced": "收起進階", - "inspire.readyToWrite": "已聊 {n} 輪,可以定稿了", - "inspire.inputAria": "跟 AI 說", - "inspire.inputPh": "想發想什麼?Enter 送出 · Shift+Enter 換行", - "inspire.send": "送出", - "inspire.generate": "整理成貼文", - "inspire.generateHint": "根據整段對話、人設聲紋與高互動寫法整理成可發布正文", - "inspire.webSearch": "查資料", - "inspire.webSearchOn": "查資料:開", - "inspire.webSearchHint": "開啟後,下一則訊息會先用 Exa 查資料再交給 AI 討論", - "inspire.needConversation": "先聊一句你的想法,再整理成貼文。", - "inspire.needReadyPersona": "請先選擇已完成分析的人設。", - "inspire.generating": "改寫中…", - "inspire.generateOk": "已依人設改寫成草稿", - "inspire.materialTitle": "鎖定要寫的內容", - "inspire.materialHint": "產文只會改寫這段(可編輯),不會另起新主題。聊天負責發想,這裡負責定稿。", - "inspire.materialLabel": "待改寫內容", - "inspire.materialPh": "從對話整理出的重點、角度、想講的事…", - "inspire.rewriteNotes": "改寫指示(可選)", - "inspire.rewriteNotesPh": "例如:短一點、更口語、加問句", - "inspire.rewriteDefault": "用人設寫成 Threads 正文", - "inspire.confirmRewrite": "用人設改寫", - "inspire.needMaterial": "先聊出一些內容,或在素材框貼上要改寫的文字", - "inspire.library": "元素庫", - "inspire.addNew": "+ 新增", - "inspire.kind": "類型", - "inspire.kindRole": "角色指令", - "inspire.kindSnippet": "片段", - "inspire.kindTrendNote": "熱點備註", - "inspire.kindBrand": "品牌", - "inspire.kindTrend": "熱點", - "inspire.name": "名稱", - "inspire.namePh": "例如:專業 Threads 寫手", - "inspire.body": "內容(會進 prompt)", - "inspire.bodyPh": "你是一位…", - "inspire.saveElement": "存進元素庫", - "inspire.citeBrand": "引用品牌", - "inspire.applied": "已套用", - "inspire.clickApply": "點擊套用", - "inspire.noBrands": "尚無品牌", - "inspire.appliedToggle": "已套用 · 再點取消", - "inspire.deleteAria": "刪除", - "inspire.needInput": "先輸入你想聊的方向", - "inspire.fail": "失敗", - "inspire.wantWrite": "想寫關於 {label}:{summary}", - "inspire.trendBody": "主題:{label}。{summary}", - "inspire.pinnedTrend": "已套用熱點 {label}", - "inspire.needTitleBody": "請填名稱與內容", - "inspire.added": "已加入元素庫", - "inspire.addFail": "新增失敗", - "inspire.confirmRemove": "從元素庫移除此項?", - "inspire.confirmClear": "開新對話?(舊對話會保留在列表)", - "inspire.genMessage": "用人設寫成 Threads 正文", - "inspire.previewTitle": "本輪會送出的完整內容", - "inspire.previewPrompt": "完整 prompt(人設/品牌產品/元素/對話,與送 AI 相同)", - "inspire.previewSections": "已帶入段落", - "inspire.previewPinnedCount": "套用元素 {n} 個", - "inspire.previewNoPins": "目前沒有套用元素(右側可點選)", - "inspire.runes": "字", - "inspire.copyAll": "複製全文", - "inspire.copied": "已複製完整 prompt", - "inspire.copyFail": "複製失敗", - "inspire.rawPrompt": "送給 AI 的原文(一字不差)", - "inspire.verifyHow": "怎麼確認一致:先按 ? 看 fingerprint → 不改內容直接送出 → 狀態列顯示「與預覽一致」。", - "inspire.verifyHowShort": "先輸入文字 → 按 ? → 不改內容按送出 → 應一致。對話不會整包重送:只帶前情摘要 + 最近幾則。", - "inspire.previewModeNote": "此為 mode={mode} 的實際組裝(與同 mode 送出相同)。", - "inspire.lastSentFp": "剛送出指紋", - "inspire.matchOk": "與上次送出一致 ✓", - "inspire.matchBad": "與預覽不一致(輸入/pin/人設/mode 不同)", - "inspire.matchBadShort": "≠ 上次送出 {sent}", - "inspire.fpMatch": "送出指紋 {fp} 與預覽一致", - "inspire.fpMismatch": "指紋不同:預覽 {preview} ≠ 送出 {sent}(是否改過字/mode?)", - "inspire.fpSent": "送出指紋 {fp}", - "inspire.viewSent": "看剛送出的全文", - "inspire.viewSentShort": "剛送", - "inspire.sentPrompt": "剛送出的完整 prompt", - "inspire.sentPromptNote": "後端實際丟給 AI 的 prompt(stream done 回傳)。", - - "persona.add": "新增人設", - "persona.empty": "尚無人設", - "persona.emptyDesc": "新增後做分析即可用於產文。", - "persona.statusReady": "ready", - "persona.statusAnalyzing": "分析中", - "persona.statusPending": "待分析", - "persona.default": "預設", - "persona.backList": "← 人設列表", - "persona.tabOverview": "概要", - "persona.tabAnalyze": "分析", - "persona.tabFingerprint": "指紋", - "persona.tabPreview": "試產", - "persona.name": "名稱", - "persona.brief": "定位 brief", - "persona.briefPh": "是誰、對誰說、核心訊息…", - "persona.avoid": "護欄 · 禁止詞(逗號分隔)", - "persona.guardChars": "{n} 字", - "persona.banAi": " · 禁 AI 腔", - "persona.notReadySuffix": " · 未就緒", - "persona.setDefault": "設為預設", - "persona.modeAccount": "公開帳號", - "persona.modeText": "貼文字", - "persona.username": "Threads username", - "persona.fromBound": "從已綁帳號帶入", - "persona.select": "選擇…", - "persona.crawlAnalyze": "分析公開貼文", - "persona.crawlBusy": "分析中…", - "persona.refText": "參考文字(--- 分隔多篇)", - "persona.refTextPh": "第一段…\n\n---\n\n第二段…", - "persona.sourceLabel": "來源說明(選填)", - "persona.sourcePh": "自己的舊文", - "persona.analyzeText": "從文字分析", - "persona.analyzeBusy": "分析中…", - "persona.sampleMeta": "樣本 {n}", - "persona.sourceManual": "貼文", - "persona.analyzeHint": "完成分析後會顯示 8D 摘要。", - "persona.fingerprintHint": "產文主體。可改口頭禪、節奏、禁忌;儲存後 Studio/回覆會吃這份。", - "persona.fingerprint": "語言指紋", - "persona.fingerprintPh": "分析後自動填入…", - "persona.saveFingerprint": "儲存指紋", - "persona.tryGen": "試產主貼 + 回覆", - "persona.previewHint": "依目前指紋寫一則可能的主貼與回文;會嘗試用即時新聞當話題靈感(轉成這個人的口吻,不是新聞稿)。", - "persona.previewRunning": "產文中…", - "persona.previewDone": "試產完成 · 話題:{topic}({source})", - "persona.previewFail": "試產失敗,請再試一次", - "persona.previewTopicLabel": "話題靈感:{topic} · {source}", - "persona.topicNews": "即時新聞", - "persona.topicManual": "手動", - "persona.topicFallback": "生活靈感", - "persona.notReadyMsg": "人設未就緒", - "persona.rootPost": "主貼", - "persona.reply": "回覆", - "persona.hidePrompt": "隱藏 prompt block", - "persona.showPrompt": "顯示注入的 prompt", - "persona.promptBlock": "prompt block(post)", - "persona.pickOne": "選一個人設", - "persona.pickDesc": "或按新增開始分析。", - "persona.created": "已建立,請到「分析」完成帳號分析或貼文字", - "persona.saved": "已儲存", - "persona.textDone": "文字分析完成 · {n} 段 → ready", - "persona.analyzeFail": "分析失敗", - "persona.reading": "正在讀取公開貼文…", - "persona.accountDone": "@{user} · {n} 則 → ready", - "persona.jobQueued": "已排入背景任務 · 可離開此頁,完成後自動存檔", - "persona.jobQueuedCrawl": "已排入分析任務 · 可離開此頁,完成後自動寫入人設", - "persona.jobRunning": "背景分析中… 完成後會自動更新(也可到「任務」查看進度)", - "persona.jobDone": "背景分析完成 · 已寫入指紋/範本", - "persona.jobFailed": "背景分析失敗,請到任務頁查看錯誤或重試", - "persona.loadFail": "載入人設失敗", - "persona.openJob": "開啟任務詳情", - "persona.setDefaultMsg": "「{name}」已設為預設", - "persona.confirmDelete": "確定刪除人設「{name}」?", - "persona.deleted": "人設已刪除", - "persona.needReady": "請先完成分析(ready)", - "persona.dim.d1Tone": "D1 語氣人格", - "persona.dim.d2Structure": "D2 結構模板", - "persona.dim.d3Interaction": "D3 互動方式", - "persona.dim.d4Topics": "D4 主題分布", - "persona.dim.d5Rhythm": "D5 發文節奏", - "persona.dim.d6Visual": "D6 視覺語法", - "persona.dim.d7Conversion": "D7 轉換方式", - "persona.dim.d8Risk": "D8 風險紅線", - - "admin.users.loadFail": "載入失敗", - "admin.users.created": "已新增島民「{name}」· 請複製下方密碼", - "admin.users.createFail": "新增失敗", - "admin.users.unlimitedOn": "「{name}」已設不擋額度(用量仍計算)", - "admin.users.unlimitedOff": "「{name}」已改回依方案擋額度", - "admin.users.updateFail": "更新失敗", - "admin.users.planSet": "「{name}」方案 → {plan}", - "admin.users.confirmSuspend": "確定停權「{name}」?\\n停權後無法登入。", - "admin.users.confirmUnsuspend": "確定復權「{name}」?\\n復權後可重新登入。", - "admin.users.didSuspend": "已停權「{name}」", - "admin.users.didUnsuspend": "已復權「{name}」", - "admin.users.suspendFail": "停權失敗", - "admin.users.unsuspendFail": "復權失敗", - "admin.users.markedVerified": "已將 {name} 標為信箱已驗證", - "admin.users.markedUnverified": "已將 {name} 標為未驗證", - "admin.users.rolesUpdated": "已更新 {name} 的權限:{roles}", - "admin.users.rolesFail": "權限更新失敗", - "admin.users.confirmReset": "確定幫「{name}」重設密碼?\\n臨時密碼會固定顯示直到你按關閉(可重整)。", - "admin.users.resetDone": "已重設 {name} 的密碼(下方可持續顯示,請複製後再關閉)", - "admin.users.resetFail": "重設失敗", - "admin.users.copied": "已複製到剪貼簿", - "admin.users.copyFail": "複製失敗,請手動選取密碼", - "admin.users.confirmDismissTemp": "關閉後此頁將不再顯示這組臨時密碼(若尚未複製請先複製)。確定關閉?", - "admin.users.tempPwNew": "新島民臨時密碼", - "admin.users.tempPw": "臨時密碼", - "admin.users.tempPwPersist": "(持續顯示 · 可重整)", - "admin.users.copyPw": "複製密碼", - "admin.users.close": "關閉", - "admin.users.createTitle": "新增島民", - "admin.users.memberName": "島民名稱", - "admin.users.displayNamePh": "顯示名稱", - "admin.users.email": "Email", - "admin.users.initPassword": "初始密碼(選填)", - "admin.users.initPasswordPh": "空白則自動產生;若填寫須符合密碼政策", - "admin.users.markVerifiedCheck": "信箱標為已驗證(可直接使用)", - "admin.users.alsoAdmin": "同時設為管理員", - "admin.users.creating": "建立中…", - "admin.users.createSubmit": "建立島民", - "admin.users.clear": "清除", - "admin.users.searchActive": "搜尋「{query}」· 可匹配名稱、Email、uid", - "admin.users.noMatch": "無符合", - "admin.users.none": "尚無島民", - "admin.users.you": "這是你", - "admin.users.status": "狀態", - "admin.users.role": "角色", - "admin.users.emailVerify": "信箱驗證", - "admin.users.bio": "簡介", - "admin.users.timezone": "時區", - "admin.users.notifyEmail": "Email 通知", - "admin.users.on": "開", - "admin.users.off": "關", - "admin.users.createdAt": "建立", - "admin.users.updatedAt": "更新", - "admin.users.accountStatus": "帳號狀態", - "admin.users.updating": "更新中…", - "admin.users.usageTitle": "用量與方案", - "admin.users.usageLiveSkip": "方案/額度屬 Usage 域,live 後端尚未接上(M3);此區僅 mock 可改。", - "admin.users.plan": "方案", - "admin.users.planOption": "{name}({credits} 點/月)", - "admin.users.unlimited": "不擋額度", - "admin.users.byPlan": "依方案", - "admin.users.setUnlimited": "設為不擋額度", - "admin.users.setLimited": "改回擋額度", - "admin.users.unlimitedHint": "不擋額度:達方案上限仍可繼續用;AI/Search 次數與點數照樣計算。", - "admin.users.loadingUsage": "載入用量設定…", - "admin.users.assignRoles": "指派權限", - "admin.users.memberBase": "{role}(基底,不可關閉)", - "admin.users.adminDesc": "{role} — 可管理島民與系統", - "admin.users.saving": "儲存中…", - "admin.users.saveRoles": "儲存權限", - "admin.users.markUnverifiedBtn": "標為未驗證", - "admin.users.markVerifiedBtn": "標為已驗證", - "admin.users.resetting": "重設中…", - "admin.users.resetTemp": "重設密碼(產生臨時)", - "admin.users.customPw": "或指定新密碼(選填)", - "admin.users.customPwPh": "密碼須至少 12 碼,且含大寫、小寫、數字與符號", - "admin.users.resetWithCustom": "用指定密碼重設", - - "usage.tabMine": "我的用量", - "usage.tabTenant": "全體用量", - "usage.currentPlan": "目前方案", - "usage.planMeta": "/月 · 每月 {n} 點額度", - "usage.changePlan": "變更方案", - "usage.upgradePlan": "升級方案", - "usage.outcome.title": "本月成果", - "usage.outcome.summary": "觸達 {reach} · 對話 {conversations} · 成交 {conversions}", - "usage.outcome.amount": "(約 ${amount})", - "usage.outcome.emptyHint": "本月還沒有可歸因的成果,去海巡或發文試試。", - "usage.warn.unlimitedOver": "本月已用 {used} 點(方案 {cap},不擋額度,仍可繼續)。", - "usage.warn.exhausted": "本月點數已用完。可升級方案或等待下月重置。", - "usage.warn.high": "本月已用 {pct}% 點數。", - "usage.warn.meterNear": "{label} 接近單項上限({credits}/{cap} 點)。", - "usage.usedThisMonth": "本月已用", - "usage.remainLabel": "剩餘", - "usage.ledgerToggle": "使用紀錄", - "usage.collapse": "收合", - "usage.eventsCount": "{n} 筆", - "usage.granularity": "粒度", - "usage.day": "日", - "usage.monthUnit": "月", - "usage.year": "年", - "usage.from": "起", - "usage.to": "迄", - "usage.callCounts": "呼叫次數", - "usage.noMembers": "尚無會員", - "usage.planAria": "{name} 方案", - "usage.unlimitedTitle": "不擋額度", - "usage.setLimited": "改回擋額度", - "usage.setUnlimited": "設為不擋額度", - "usage.limitShort": "擋", - "usage.subscribed": "已訂閱 {name}", - "usage.unlimitedSet": "已設不擋額度", - "usage.limitedSet": "已改回擋額度", - "usage.planUpdated": "已更新方案 {name}", - "usage.fail": "失敗", - - "currency.TWD": "新台幣 (TWD)", - "currency.USD": "美元 (USD)", - "currency.JPY": "日圓 (JPY)", - "currency.EUR": "歐元 (EUR)", - "currency.HKD": "港幣 (HKD)", - - "locale.zh-TW": "繁體中文", - "locale.en": "English", - - "pager.nav": "分頁", - "pager.pageSize": "每頁筆數", - "pager.perPage": "{n}/頁", - "pager.prev": "上一頁", - "pager.next": "下一頁", - - "plays.defaultTitle": "新方案", - "plays.topicOnPost": "掛在:{snippet}", - "plays.topicOnExternal": "掛在:{label} · {snippet}", - "plays.externalFallback": "外站貼", - - "persona.newName": "新人設", - "persona.previewTopic": "週末想找能坐久的咖啡店", - "persona.previewReplySample": "大安那間還行但人很多", - - "inspire.playTitle": "靈感串場", - - "play.err.needLead": "請選擇主帳號", - "play.err.needRoot": "請至少有一則主貼", - "play.err.firstMustRoot": "第一則必須是主貼", - "play.err.rootMustLead": "主貼必須使用主帳", - "play.err.rootEmpty": "主貼文案不可空白", - "play.err.replyAccount": "回覆只能用主帳或已選配角", - "play.err.replyEmpty": "回覆文案不可空白", - "play.err.needTarget": "請選擇自己的貼文,或貼上 Threads 連結", - "play.err.needReplies": "請至少排 1 則留言", - "play.err.needReplyAccounts": "請至少選一個可回覆帳號", - "play.err.stepAccount": "每則留言都要指定可用帳號", - "play.err.stepEmpty": "留言內容不可空白", - "play.err.notFound": "找不到互回方案", - - "time.justNow": "剛剛", - "time.minAgo": "{n} 分前", - "time.hourAgo": "{n} 小時前", - "time.dayAgo": "{n} 天前", - "time.min": "{n} 分鐘", - "time.hour": "{n} 小時", - "time.day": "{n} 天", - "time.expired": "已過期 {span}", - "time.remaining": "剩餘 {span}", - "time.sessionUnknown": "未記錄到期時間", - "time.sessionExpired": "Token 已過期 · {absolute}", - "time.sessionSoon": "即將到期 · {relative}({absolute})", - "time.sessionOk": "有效 · {relative}({absolute})", - - // demand-radar:服務檔案(每會員一份,判定與回覆都讀它) - "policy.title": "商機與回覆政策", - "policy.subtitle": "設定服務範圍、禁語、案例、FAQ 與口吻;商機判定和生成回覆會共用這份政策。", - "radar.profile.title": "服務檔案", - "radar.profile.subtitle": "雷達用這份資料判斷商機是否值得回,回覆也照這裡的價格與口吻寫。", - "radar.profile.firstTimeHint": "先填服務檔案,才能開啟雷達訂閱。內容越具體,判定與回覆越準。", - "radar.profile.updatedAt": "上次更新:{at}", - "radar.profile.saved": "服務檔案已儲存", - "radar.profile.services": "服務與價格", - "radar.profile.servicesHint": "至少填一項。價格留空代表面議,填了就會出現在回覆裡。", - "radar.profile.serviceName": "服務名稱", - "radar.profile.serviceNamePh": "例:室內設計丈量規劃", - "radar.profile.priceMin": "價格下限", - "radar.profile.priceMax": "價格上限", - "radar.profile.addService": "+ 新增服務", - "radar.profile.areas": "服務區域", - "radar.profile.areasHint": "選你實際接得到的縣市;地區不合的商機會被降分。可遠端就勾下面那項。", - "radar.profile.remoteOk": "可遠端服務(不限地區)", - "radar.profile.forbidden": "禁語", - "radar.profile.forbiddenHint": "一行一條。這些字不會出現在任何生成的回覆裡。", - "radar.profile.forbiddenPh": "保證\n最便宜\n第一名", - "radar.profile.cases": "案例", - "radar.profile.casesHint": "選填。回覆需要舉證時會引用,沒有就不引用。", - "radar.profile.caseTitle": "案例標題", - "radar.profile.caseSummary": "一句話說明", - "radar.profile.addCase": "+ 新增案例", - "radar.profile.faq": "常見問答", - "radar.profile.faqHint": "選填。對方問到類似問題時,回覆會照這裡的答案講。", - "radar.profile.faqQuestion": "問題", - "radar.profile.faqAnswer": "回答", - "radar.profile.addFaq": "+ 新增問答", - "radar.profile.availability": "可接案時間", - "radar.profile.availabilityHint": "例:兩週內可開工、只接週末", - "radar.profile.toneNote": "口吻備註", - "radar.profile.toneNoteHint": "例:講話直接不客套、不用驚嘆號", - - // demand-radar:商機訂閱(每日自動;對照海巡=手動掃場) - "radar.watches.title": "商機訂閱", - "radar.watches.subtitle": "訂好關鍵字後每天自動巡,整理成「今日商機」。和海巡「按一次掃一輪」不同。", - "radar.watches.needProfile": "先填服務檔案,才能開商機訂閱", - "radar.watches.needProfileHint": "系統靠服務檔案判斷需求適不適合你;沒有它會誤判。", - "radar.watches.goProfile": "去填服務檔案", - "radar.watches.quota": "啟用中 {used} / {max}", - "radar.watches.quotaFull": "已達方案上限,要新增請先暫停或封存一個", - "radar.watches.add": "+ 新增訂閱", - "radar.watches.newTitle": "新增商機訂閱", - "radar.watches.editTitle": "編輯商機訂閱", - "radar.watches.requiredHint": "為必填欄位", - "radar.watches.terms": "關鍵字", - "radar.watches.termsHint": "一行一個短詞。每組最多 2 詞、中文每詞 2–4 字,才能在 Threads 搜到;長句儲存時會自動收成短詞。", - "radar.watches.threadsWarn": "有關鍵字不合 Threads 短詞規則:每組最多 2 詞、中文每詞 2–4 字、整組 ≤12 字、勿用標點/#/emoji。儲存時會自動收成可搜短詞。", - "radar.watches.threadsRequired": "這些關鍵字收不成 Threads 可搜的短詞。請改成每組最多 2 詞、中文每詞 2–4 字。", - "radar.watches.termsPh": "室內設計\n找設計師", - "radar.watches.excludeTerms": "排除詞", - "radar.watches.excludeHint": "命中這些字就整筆跳過,例如同業自我推銷、抽獎文。", - "radar.watches.excludePh": "徵才\n抽獎", - "radar.watches.regionsHint": "地區留空=沿用服務檔案的服務區域;只有這個訂閱要縮小範圍時才選。", - "radar.watches.regionsFromProfile": "地區沿用服務檔案", - "radar.watches.enableNow": "建立後立刻啟用(會佔用啟用配額)", - "radar.watches.filterStatus": "狀態", - "radar.watches.statusAll": "全部", - "radar.watches.status.active": "啟用中", - "radar.watches.status.paused": "已暫停", - "radar.watches.status.archived": "已封存", - "radar.watches.pause": "暫停", - "radar.watches.resume": "恢復", - "radar.watches.archive": "封存", - "radar.watches.confirmArchive": "封存後不會再巡,也不能恢復。要繼續嗎?", - "radar.watches.deleteArchived": "刪除封存設定", - "radar.watches.deletingArchived": "刪除中…", - "radar.watches.confirmDeleteArchived": "確定永久刪除這個封存巡邏設定嗎?既有商機、巡邏紀錄與統計會保留,但這個設定之後不會再出現在列表。", - "radar.watches.deletedArchived": "封存巡邏設定已刪除", - "radar.watches.created": "訂閱已建立", - "radar.watches.createdFirstSweep": "訂閱已建立,首巡已排入,約幾分鐘後回今日商機頁看結果(之後每天自動巡,不用再等)", - "radar.watches.updated": "訂閱已更新", - "radar.watches.paused": "訂閱已暫停", - "radar.watches.resumed": "訂閱已恢復", - "radar.watches.archived": "訂閱已封存", - "radar.watches.lastSwept": "上次巡:{at}", - "radar.watches.neverSwept": "還沒巡過", - "radar.watches.empty": "還沒有商機訂閱", - "radar.watches.emptyHint": "加客人會用的短詞(例如「室內設計」「找設計師」),系統會每天自動幫你巡。若要現在手動掃痛點/話題,用側欄「海巡」。", - "radar.watches.emptyFiltered": "這個狀態下沒有訂閱", - "radar.watches.scheduleTitle": "每日定時巡邏:每天台北 06:00(UTC 22:00)", - "radar.watches.scheduleHint": "開著的訂閱每天自動巡一輪。要現在看結果,按「立即巡邏」。關掉立即巡邏不會停每日定時。", - "radar.watches.openToday": "回商機結果", - "radar.watches.sweepNow": "立即巡邏", - "radar.watches.sweepQueued": "已排入商機巡檢", - "radar.watches.sweepStarted": "商機巡檢已開始(任務 {job}…)", - - "today.radar.title": "今日商機", - "today.radar.total": "找到", - "today.radar.high": "高", - "today.radar.mid": "中", - "today.radar.low": "低", - "today.radar.open": "查看今日商機", - "today.radar.empty": "還沒有今日商機。訂閱關鍵字後會每天自動更新(與海巡手動掃場不同)。", - "today.radar.goProfile": "先填服務檔案", - "today.radar.goWatches": "去訂閱關鍵字", - - "firstRun.title": "先連一個 Threads 帳號", - "firstRun.subtitle": "連上之後就能用其他功能。點按鈕去帳號頁連接。", - "firstRun.skip": "略過,之後自己摸", - "firstRun.go": "去連接", - "firstRun.progress": "第 {current} 步/共 {total} 步", - "firstRun.done": "完成", - "firstRun.aria": "第一次設定", - "firstRun.step.crew": "連 Threads 帳號", - "firstRun.step.crewHint": "之後才能一鍵送出回覆。", - "firstRun.step.brands": "整理品牌與產品", - "firstRun.step.brandsHint": "填受眾、痛點與產品能力。", - "firstRun.step.watch": "建立每日巡邏", - "firstRun.step.watchHint": "選產品後採用建議關鍵字並儲存。", - "firstRun.step.radar": "看第一筆商機", - "firstRun.step.radarHint": "不必等隔天,可按「立即探索」。", - "firstRun.status.pending": "引導中", - "firstRun.status.skipped": "已略過引導", - "firstRun.status.completed": "已完成引導", - - "radar.suggest.title": "關鍵字建議", - "radar.suggest.hint": "依你的服務檔案想幾個客人真的會打的字,逐條或全部採用;採用後仍要按儲存才會建立。", - "radar.suggest.ask": "取得建議", - "radar.suggest.again": "再想幾個", - "radar.suggest.asking": "想關鍵字中…", - "radar.suggest.adopt": "採用", - "radar.suggest.adopted": "已採用", - "radar.suggest.adoptAll": "全部採用", - "radar.suggest.include": "關鍵字", - "radar.suggest.exclude": "排除詞", - "radar.suggest.none": "這次沒想出可用的字,請把服務檔案寫具體一點再試。", - - // demand-radar:今日商機(自動名單) - "radar.today.title": "今日商機", - "radar.today.subtitle": "訂閱後每天自動整理的需求名單(不是海巡那一輪手動掃)。", - "radar.today.link.watches": "商機訂閱", - "radar.today.link.crm": "名單看板", - "radar.today.stats.total": "今日找到", - "radar.today.stats.high": "高意向", - "radar.today.stats.mid": "中意向", - "radar.today.stats.low": "低意向", - "radar.today.truncated": "已達今日上限,{n} 筆較低意向未收錄", - "radar.today.lastSwept": "上次巡檢:{at}", - "radar.today.band.high": "高", - "radar.today.band.mid": "中", - "radar.today.band.low": "低", - "radar.today.status.accepted": "已加入名單", - "radar.today.status.dismissed": "已略過", - "radar.today.status.qualified": "待處理", - "radar.today.status.rejected": "已否決", - "radar.today.status.judging": "判定中", - "radar.today.regionUnknown": "地區不明", - "radar.today.group.high": "高意向", - "radar.today.group.mid": "中意向", - "radar.today.group.low": "低意向", - "radar.today.group.empty": "這一組目前沒有", - "radar.today.group.expand": "展開", - "radar.today.group.collapse": "收合", - "radar.today.action.open": "原文", - "radar.today.action.accept": "加入名單", - "radar.today.action.dismiss": "略過", - "radar.today.action.reply": "產生回覆", - "radar.today.action.hideReply": "收合回覆", - "radar.today.action.reasons": "判定理由", - "radar.today.action.hideReasons": "收合理由", - "radar.today.action.override": "覆寫分級", - "radar.today.reply.hint": "選一個版本產生草稿;私訊版只提供複製,不會自動送出。", - "radar.today.reply.copy": "複製草稿", - "radar.today.reply.variant.public_comment": "公開留言", - "radar.today.reply.variant.dm": "私訊", - "radar.today.reply.variant.no_sales": "不銷售", - "radar.today.reply.variant.professional": "專業", - "radar.today.reply.variant.humorous": "輕鬆", - "radar.today.dim.authenticity": "真實性", - "radar.today.dim.intent": "意圖", - "radar.today.dim.region": "地區", - "radar.today.dim.freshness": "新鮮度", - "radar.today.dim.fit": "服務匹配", - "radar.today.empty.title": "目前沒有今日商機", - "radar.today.empty.fallback": "稍後再回來,或先檢查商機訂閱與服務檔案。", - "radar.today.empty.goProfile": "去填服務檔案", - "radar.today.empty.goWatches": "去訂閱關鍵字", - "radar.today.empty.goAll": "查看全部結果", - "radar.today.empty.reason.no_profile": "還沒有服務檔案,無法判定需求適不適合你。", - "radar.today.empty.reason.no_watch": "還沒有商機訂閱;建立關鍵字後才會每天自動巡(不是海巡那一輪手動掃)。", - "radar.today.empty.reason.all_watches_paused": "訂閱都暫停了,恢復一組才會繼續自動巡。", - "radar.today.empty.reason.not_swept_yet": "每日定時還沒跑完,也可在商機頁按「立即巡邏」。", - "radar.today.empty.reason.sweep_failed": "這輪自動巡失敗,請到商機訂閱頁查看或重試。", - "radar.today.empty.reason.no_hit": "有巡但沒有符合的需求,可放寬關鍵字或排除詞。", - "radar.today.msg.accepted": "已加入名單", - "radar.today.msg.dismissed": "已略過", - "radar.today.msg.replyReady": "回覆草稿已產生", - "radar.today.msg.overridden": "分級已更新", - "radar.today.msg.copied": "已複製到剪貼簿", - "radar.today.msg.copyFail": "無法複製,請手動選取文字", - "radar.today.msg.marked": "已標記為已送出/已複製", - "radar.today.msg.sent": "已送出,稍後可在發送佇列查看進度", - "radar.today.msg.needReply": "請先產生回覆草稿", - "radar.today.sendAccount": "送出帳號", - "radar.today.reply.markCopy": "標記已複製送出", - "radar.today.reply.markOutbox": "一鍵送出(Outbox)", - "radar.today.reply.needAccount": "先連一個 Threads 帳號才能一鍵送出", - "radar.today.reply.used": "已標記使用", - "radar.today.reply.usedOutbox": "已送出(可在發送佇列查看)", - - "radar.reconnectSearch": "重新連線搜尋來源", - "radar.patrol.searchFallback": "搜尋來源暫時不可用時會改走備用通道;只有成功回傳才計點。", - "radar.empty.sweepFailedHint": "巡邏失敗。可再按立即巡邏,或改看近 7 天/全部。", - "radar.inbox.title": "商機", - "radar.inbox.patrolAria": "巡邏狀態", - "radar.inbox.scheduledOn": "每日定時巡邏:開著", - "radar.inbox.scheduledOff": "每日定時巡邏:關著", - "radar.inbox.scheduleHint": "每天台北 06:00 自動巡一輪。關掉立即巡邏不會停每日定時。", - "radar.inbox.lastSweep": "上次巡邏:{time}", - "radar.inbox.neverSwept": "還沒巡邏過", - "radar.inbox.activeWatches": "啟用中 {n} 組", - "radar.inbox.allPaused": "訂閱都暫停了,立即巡邏也需要至少一組開著", - "radar.inbox.noWatches": "還沒設定要巡的產品與關鍵字", - "radar.inbox.sweepNow": "立即巡邏", - "radar.inbox.sweeping": "巡邏中…", - "radar.inbox.sweepAgain": "再巡一次", - "radar.inbox.setupWatches": "設定巡邏", - "radar.inbox.introTitle": "巡邏到痛點就看這裡", - "radar.inbox.introBody": "先讀「為什麼推薦」,留下或丟掉即可。加入名單是可選的,不是看結果的必要步驟。", - "radar.inbox.resultsAria": "商機結果", - "radar.inbox.tabsAria": "結果狀態", - "radar.inbox.tab.pending": "新找到", - "radar.inbox.tab.completed": "已看過", - "radar.inbox.tab.removed": "已丟掉", - "radar.inbox.total": "共 {n} 筆", - "radar.inbox.clearFilters": "清除篩選", - "radar.inbox.defaultToday": "預設先看今天剛巡到的結果", - "radar.inbox.timeScope": "看哪段時間", - "radar.inbox.time.today": "今天", - "radar.inbox.time.7d": "近 7 天", - "radar.inbox.time.all": "全部", - "radar.inbox.sort": "先看哪些", - "radar.inbox.sort.recommended": "最對得上產品", - "radar.inbox.sort.newest": "最新貼文", - "radar.inbox.sort.oldest": "最舊貼文", - "radar.inbox.sort.productFit": "最符合產品", - "radar.inbox.sort.demandIntent": "需求最明確", - "radar.inbox.moreFilters": "更多篩選", - "radar.inbox.moreFiltersN": "更多篩選({n})", - "radar.inbox.hideFilters": "收起更多篩選", - "radar.inbox.moreFiltersAria": "更多篩選", - "radar.inbox.brand": "品牌", - "radar.inbox.allBrands": "全部品牌", - "radar.inbox.product": "產品", - "radar.inbox.allProducts": "全部產品", - "radar.inbox.band": "商機意向", - "radar.inbox.allBands": "全部意向", - "radar.inbox.band.high": "高意向", - "radar.inbox.band.mid": "中意向", - "radar.inbox.band.low": "低意向", - "radar.inbox.match": "產品匹配", - "radar.inbox.allStates": "全部狀態", - "radar.inbox.state.eligible": "可跟進", - "radar.inbox.state.weak": "弱適配", - "radar.inbox.state.excluded": "已排除", - "radar.inbox.state.generic": "未指定產品", - "radar.inbox.state.stale": "超過 14 天", - "radar.inbox.loading": "正在整理巡邏結果…", - "radar.inbox.prevPage": "上一頁", - "radar.inbox.nextPage": "下一頁", - "radar.inbox.pageOf": "第 {page} 頁/共 {pages} 頁", - "radar.inbox.goCrm": "前往名單", - "radar.inbox.see7d": "看近 7 天", - "radar.inbox.empty.filteredPending": "這個篩選下沒有結果", - "radar.inbox.empty.filteredCompleted": "目前沒有已看過的結果", - "radar.inbox.empty.filteredRemoved": "目前沒有已丟掉的結果", - "radar.inbox.empty.filteredHint": "清除篩選或改看其他時間範圍。巡邏剛跑完的結果也可能在「近 7 天」或「全部」。", - "radar.inbox.empty.noCompleted": "還沒有已看過的結果", - "radar.inbox.empty.noCompletedHint": "切回「新找到」繼續看巡邏到的痛點。", - "radar.inbox.empty.noRemoved": "還沒有丟掉的結果", - "radar.inbox.empty.noRemovedHint": "切回「新找到」繼續看巡邏到的痛點。", - "radar.inbox.empty.noWatchesTitle": "還沒設定巡邏", - "radar.inbox.empty.noWatchesHint": "先選產品與客人會搜的關鍵字。設好後可立即巡邏,每日定時巡邏也會接著跑。", - "radar.inbox.empty.pausedTitle": "每日定時巡邏關著", - "radar.inbox.empty.pausedHint": "立即巡邏與每日定時都還在這個頁面。恢復至少一組訂閱後,兩個都能用;關掉其中一個不會藏掉另一個。", - "radar.inbox.empty.openSchedule": "打開每日定時巡邏", - "radar.inbox.empty.neverTitle": "還沒巡邏過", - "radar.inbox.empty.neverHint": "每日定時巡邏已開著,也可現在按「立即巡邏」。不是空白收件匣,只是第一輪還沒跑完。", - "radar.inbox.empty.failedTitle": "上一輪巡邏沒跑完", - "radar.inbox.empty.noHitsTitle": "搜尋沒撈到貼文", - "radar.inbox.empty.noHitsHint": "門檻前就空了:關鍵字太長、太產品名、或 Threads 查無結果。改成客人會打的 2–4 字痛點詞再巡。", - "radar.inbox.empty.editTerms": "改關鍵字", - "radar.inbox.empty.noFitTitle": "這輪有巡,但沒找到符合的痛點", - "radar.inbox.empty.noFitStats": "搜尋命中 {hits}、判定 {judged}、新建 {created}。不是今天發的文可改看近 7 天/全部。", - "radar.inbox.empty.noFitHint": "新文章或產品對得上的需求會出現在這裡。也可改看「近 7 天」或「全部」,或調整要巡的關鍵字。", - "radar.inbox.msg.kept": "已留下。沒有建立名單。", - "radar.inbox.msg.removed": "已丟掉。可從「已丟掉」還原。", - "radar.inbox.msg.restored": "已還原。", - "radar.inbox.msg.accepted": "已加入名單。這步是可選的,之後要追蹤再去名單即可。", - "radar.inbox.msg.widened7d": "巡邏結果不是都在「今天發的文」。已改看近 7 天({n} 筆)。任務上的判定/新建數字包含同一篇再命中,不一定全是新卡片。", - "radar.inbox.msg.widenedAll": "近 7 天沒有待處理結果,已改看全部({n} 筆)。", - "radar.inbox.msg.alreadyReviewed": "這輪判定到的 {n} 筆已在「已看過」,所以「新找到」是空的。任務數字含再次命中的舊文。", - "radar.inbox.msg.waitingWorker": "立即巡邏已排程 · 等待 worker", - "radar.inbox.err.noActive": "沒有開著的每日巡邏。先設定要巡的產品與關鍵字,或恢復一組訂閱。", - "radar.inbox.err.noJob": "沒有排到巡邏任務。", - "radar.inbox.msg.running": "立即巡邏進行中… 跑完才會把痛點列在下面。", - "radar.inbox.err.failed": "巡邏失敗。", - "radar.inbox.err.cancelled": "巡邏已取消,不會誤顯示為已完成。可再按一次立即巡邏。", - "radar.inbox.msg.queuedN": "已排入 {n} 組巡邏,目前仍在後台執行。可先離開這頁,完成後結果會留在這裡。", - "radar.inbox.msg.doneWithSummary": "{summary} 不是今天發的文也會留在下面。", - "radar.inbox.msg.done": "這一輪巡邏跑完了。找到的痛點會留在下面。", - "radar.card.priority.high": "優先跟進", - "radar.card.priority.review": "值得確認", - "radar.card.priority.low": "低順位", - "radar.card.fitProduct": "適合產品 · {label}", - "radar.card.noProduct": "尚未配對產品", - "radar.card.intent": "需求意向 {n}", - "radar.card.why": "為什麼推薦:", - "radar.card.unknownAuthor": "未知作者", - "radar.card.openOriginal": "查看 Threads 原文", - "radar.card.actionsAria": "商機操作", - "radar.card.keep": "留下", - "radar.card.discard": "丟掉", - "radar.card.whyBtn": "為什麼推薦", - "radar.card.accept": "加入名單(可選)", - "radar.card.busy": "處理中…", - "radar.card.restore": "還原到待處理", - "radar.card.accepted": "已加入名單", - "radar.card.done": "已處理", - "radar.card.removeAria": "標示為不適合", - "radar.card.removeTitle": "為什麼丟掉?", - "radar.card.removeHint": "選原因後會移出「新找到」,之後巡邏不會再把同一篇推上來。這個動作不扣點。", - "radar.card.reason": "原因", - "radar.card.reason.pain_mismatch": "不符合產品痛點", - "radar.card.reason.provider_or_ad": "供應商/廣告貼文", - "radar.card.reason.stale": "需求已過期", - "radar.card.reason.already_solved": "對方已解決", - "radar.card.reason.duplicate": "重複商機", - "radar.card.reason.other": "其他原因", - "radar.card.note": "補充說明", - "radar.card.duplicateHint": "詳情中可指定要保留的原始商機。", - "radar.card.confirmRemove": "確認丟掉", - "radar.drawer.aria": "商機詳情", - "radar.drawer.title": "商機詳情", - "radar.drawer.noProduct": "未指定產品", - "radar.drawer.close": "關閉", - "radar.drawer.closeAria": "關閉商機詳情", - "radar.drawer.intent": "意向 {n}", - "radar.drawer.priority": "優先 {n}", - "radar.drawer.evidence": "需求證據", - "radar.drawer.matches": "產品匹配與風險", - "radar.drawer.generic": "尚未指定產品,這筆結果只保留為一般需求。", - "radar.drawer.judge": "原始判定", - "radar.drawer.openOriginal": "開啟 Threads 原文", - "radar.drawer.hint": "先看痛點與產品理由。留下或丟掉即可;加入名單只在你要追這個人時才需要。", - "radar.sweep.aria": "巡邏漏斗摘要", - "radar.sweep.title": "這次巡邏跑到哪裡", - "radar.sweep.hint": "不把合併匹配誤算成新增商機。", - "radar.sweep.status.complete": "完成", - "radar.sweep.status.partial_budget": "預算暫停", - "radar.sweep.status.blocked_budget": "點數不足", - "radar.sweep.status.failed": "失敗", - "radar.sweep.hits": "命中", - "radar.sweep.deduped": "去重", - "radar.sweep.prefilterPass": "前處理通過", - "radar.sweep.prefilterReject": "前處理排除", - "radar.sweep.cached": "快取判定", - "radar.sweep.aiJudge": "AI 判定", - "radar.sweep.created": "新增商機", - "radar.sweep.deferred": "預算延後", - "radar.sweep.credits": "點數:搜尋 {search} · 需求地圖 {map} · 判定 {judge}", - "radar.sweep.total": "合計 {n}", - "radar.sweep.budgetHint": "預算未使用的候選會保留,下次可續跑;不會重複扣已成功判定的筆數。", - "radar.cost.aria": "點數預覽與確認", - "radar.cost.title": "執行前先確認點數", - "radar.cost.hint": "預覽本身不扣點;只有 provider 成功回傳才會計入用量。", - "radar.cost.byok": "BYOK · 平台 0 點", - "radar.cost.platform": "平台點數", - "radar.cost.fixed": "固定", - "radar.cost.range": "預估範圍", - "radar.cost.calls": "搜尋呼叫", - "radar.cost.remaining": "剩餘", - "radar.cost.until": "預覽至 {time}", - "radar.cost.ceiling": "本次最高可用點數", - "radar.cost.ceilingHint": "至少 {min},最多 {max}", - "radar.cost.invalid": "點數上限必須落在固定成本至預估上限之間。", - "radar.cost.confirm": "確認並執行", - "radar.cost.starting": "啟動中…", - "radar.readiness.title": "產品資料完整度", - "radar.readiness.audience": "受眾", - "radar.readiness.context": "情境", - "radar.readiness.pain": "痛點", - "radar.readiness.capability": "能力詞", - "radar.readiness.ok": "已填", - "radar.readiness.todo": "待補", - "radar.readiness.hint": "資料不足會降低適配信心,但仍可建立產品型雷達。", - "radar.match.why": "為什麼適合", - "radar.match.hide": "收合證據", - "radar.match.basis": "產品依據:{text}", - "radar.match.risks": "風險:{text}", - "radar.today.empty.goBrands": "設定品牌與產品", - "radar.today.introTitle": "先看值得跟進的人,再決定怎麼回", - "radar.today.introBody": "系統會把 Threads 貼文和你的產品痛點比對、合併重複貼文,再依商機分數排序。", - "radar.today.navAria": "商機雷達導覽", - "radar.today.manageWatches": "管理每日巡邏", - "radar.today.viewAll": "查看全部結果", - "radar.today.filterAria": "篩選今日商機", - "radar.today.filterTitle": "篩選今日商機", - "radar.today.filterHint": "先看全部;結果多時再縮小到品牌或產品。", - "radar.today.fit": "產品適配", - "radar.today.allFit": "全部適配", - "radar.today.fit.strong": "高適配", - "radar.today.fit.possible": "可能", - "radar.today.fit.weak": "弱適配", - "radar.today.needMore": "沒有想看的貼文?", - "radar.today.setupAria": "第一次使用商機雷達", - "radar.today.setupTitle": "第一次使用,照這三步就好", - "radar.today.setupHint": "完成後系統會每天自動巡邏。", - "radar.today.setupStep": "目前第 {n} 步", - "radar.today.setup.1.title": "整理品牌與產品", - "radar.today.setup.1.body": "填入受眾、痛點與產品能力。", - "radar.today.setup.1.cta": "前往設定", - "radar.today.setup.2.title": "建立每日巡邏", - "radar.today.setup.2.body": "選產品後採用建議關鍵字。", - "radar.today.setup.2.cta": "建立巡邏", - "radar.today.setup.3.title": "回來處理商機", - "radar.today.setup.3.body": "先看高分,再查看產品證據。", - "radar.today.productEyebrow": "推薦產品", - "radar.today.noPrimary": "尚未指定主推產品", - "radar.today.fitScore": "適配 {n}", - "radar.today.overridden": "人工指定", - "radar.today.hideEvidence": "收合產品證據", - "radar.today.showMatches": "查看 {n} 個產品匹配", - "radar.today.genericJudge": "未指定產品(沿用通用商機判定)", - "radar.today.msg.primarySet": "已設定主推產品;後續高分匹配不會覆蓋這個選擇。", - - "radar.primary.empty": "目前沒有產品匹配", - "radar.primary.label": "主推產品", - "radar.primary.placeholder": "選擇產品", - "radar.primary.needsReason": " · 需理由", - "radar.primary.reasonAria": "主推理由", - "radar.primary.reasonPh": "可選:為何這次主推它", - "radar.primary.submit": "設定主推", - "radar.primary.defaultReason": "使用者依證據選定主推產品", - - "radar.watches.needBrandProduct": "請先選擇品牌與產品,產品型雷達才能啟用。", - "radar.watches.needDemandMap": "請先補齊產品需求地圖,再啟動產品型巡邏。", - "radar.watches.needBrandProductShort": "請先選擇品牌與產品。", - "radar.watches.assigned": "已補綁產品;之後不可在原訂閱更換產品。", - "radar.watches.assigning": "補綁中…", - "radar.watches.assign": "補綁這個產品", - "radar.watches.productGone": "產品已失效:請建立新訂閱", - "radar.watches.pickBrand": "請先選品牌", - "radar.watches.pickProduct": "請選這個品牌下的產品", - - "radar.explore.needProduct": "立即探索請先選擇品牌與產品,避免把通用結果誤當成產品商機。", - "radar.explore.pickBrand": "請選品牌", - "radar.explore.pickProduct": "請選產品", - "radar.explore.productStats": "新增商機 {created} · 合併產品匹配 {merged} · 評估 {matched}", - - "radar.import.needProduct": "手動匯入請先選擇品牌與產品。", - - "radar.suggest.basis": "依據:{text}", - - "radar.demand.title": "產品需求地圖", - "radar.demand.hint": "先確認產品真實痛點,再用它縮小巡邏結果。來源會保留在每個詞旁邊。", - "radar.demand.aria": "{label} 需求地圖", - "radar.demand.ready": "可用", - "radar.demand.incomplete": "待補資料", - "radar.demand.version": "版本 {n}", - "radar.demand.loading": "正在整理產品需求…", - "radar.demand.pain": "使用者痛點", - "radar.demand.painHint": "產品要解決的困擾,例如漏水、協作混亂", - "radar.demand.scenario": "使用情境", - "radar.demand.scenarioHint": "使用者會怎麼描述發生的情境", - "radar.demand.outcome": "期待結果", - "radar.demand.outcomeHint": "使用者想要的結果或改善", - "radar.demand.solution": "解法訊號", - "radar.demand.solutionHint": "能判斷你有能力協助的詞", - "radar.demand.exclusion": "排除訊號", - "radar.demand.exclusionHint": "徵才、廣告等不應進入商機的內容", - "radar.demand.custom": "自訂補充", - "radar.demand.customHint": "自訂內容不會覆蓋產品原始資料;下一次產品更新時仍可辨識來源。", - "radar.demand.save": "保存需求地圖", - "radar.demand.saving": "保存中…", - "radar.demand.origin.product": "產品", - "radar.demand.origin.ai": "AI 建議", - "radar.demand.origin.user": "手動", - "radar.demand.customBasis": "手動補充", - - "radar.query.aria": "查詢計畫預覽", - "radar.query.title": "系統會用這些短詞搜尋", - "radar.query.hint": "每組最多兩個詞、中文每詞 2–4 字,才能在 Threads 搜到。產品名稱只作輔助。", - "radar.query.meta": "輸入 {input} · 地圖 v{map}", - "radar.query.group": "查詢組 {n}", - "radar.query.basis": "依據:{text}", - "radar.query.exclude": "排除:{text}", - "radar.query.adopt": "採用這些查詢詞", - "radar.query.empty": "需求地圖尚未有足夠的可搜痛點/情境,暫時無法產生查詢組。", - "radar.query.defaultBasis": "產品痛點", - - "brands.loadFail": "品牌資料載入失敗,請稍後再試", - "brands.deleteImpact": "將暫停 {n} 個相關商機訂閱;歷史商機與接觸紀錄會保留。", - - "crm.board.touchProduct": " · 產品:{label}", - "crm.board.primaryProduct": "主推產品:{label}", - "crm.board.primaryProductWithBrand": "主推產品:{label}({brand})", - "crm.board.noProduct": "未指定產品", - - "utm.title": "UTM 追蹤連結", - "utm.new": "新建", - "utm.dest": "目標 URL", - "utm.label": "標籤", - "utm.create": "建立", - "utm.empty": "尚無連結", - "utm.track": "追蹤:{url}", - "utm.destLine": "目標:{url}", - "utm.clicks": "點擊:{n}", - "utm.copy": "複製連結", - "utm.created": "已建立追蹤連結", - "utm.copied": "已複製追蹤連結", - "utm.copyFail": "複製失敗,請手動選取網址", - - "tools.pain.title": "痛點關鍵字產生器", - "tools.pain.subtitle": "免登入:描述產品,產出海巡可用的掃描詞與痛點。", - "tools.pain.brief": "產品簡述", - "tools.pain.audience": "受眾(選填)", - "tools.pain.run": "產生關鍵字", - "tools.pain.running": "產生中…", - "tools.pain.result": "結果", - "tools.pain.keywords": "關鍵字", - "tools.pain.pains": "痛點", - "tools.pain.scan": "掃描詞", - "tools.pain.login": "登入巡樓海巡", - - "tools.style.title": "風格指紋測驗", - "tools.style.subtitle": "免登入:貼上幾則你的 Threads 貼文,立刻看語氣與節奏。", - "tools.style.samples": "貼文樣本", - "tools.style.run": "開始分析", - "tools.style.running": "分析中…", - "tools.style.result": "結果", - "tools.style.tone": "語氣:{v}", - "tools.style.rhythm": "節奏:{v}", - "tools.style.hooks": "鉤子:{v}", - "tools.style.avoid": "注意:{v}", - "tools.style.login": "登入巡樓", - - "inspire.source": "來源:{label}", - - "bench.title": "成效基準", - "bench.reload": "重新查詢", - "bench.sample": "樣本 {n}", - "bench.median": "中位互動率 {eng}% · 中位瀏覽 {views}", - "bench.yours": "你的互動率 {eng}% · 均覽 {views}", - "bench.insufficient": "樣本不足", - - "insights.summaryTitle": "近 3 個月摘要", - "insights.summaryStats": "貼文 {posts} · 瀏覽 {views} · 讚 {likes} · 回 {replies} · 均互 {eng}%", - "insights.summaryTop": "表現較佳", - "insights.topPostLine": "{eng}% · 覽 {views} · {text}", - - "playbooks.title": "Playbook 市集", - "playbooks.allKinds": "全部類型", - "playbooks.kind.brief": "海巡 brief", - "playbooks.kind.persona": "人設", - "playbooks.kind.play": "互回劇本", - "playbooks.nichePh": "利基(保養/母嬰…)", - "playbooks.mineOnly": "只看我的", - "playbooks.publish": "發布模板", - "playbooks.cancel": "取消", - "playbooks.publishCard": "發布", - "playbooks.fieldTitle": "標題", - "playbooks.fieldNiche": "利基", - "playbooks.fieldBody": "內容", - "playbooks.anonymous": "匿名", - "playbooks.submit": "送出", - "playbooks.empty": "尚無模板", - "playbooks.imports": "引用 {n}", - "playbooks.import": "引用", - "playbooks.published": "已發布", - "playbooks.imported": "已引用到我的 playbook", - - "radar.import.open": "手動匯入", - "radar.import.close": "收起手動匯入", - "radar.import.title": "手動匯入商機", - "radar.import.hint": "貼 Threads/Facebook 貼文網址與內文,跑同一套五問判定。任意網址不會自動讀內文,請直接貼上。量體不足時可用這個補足每日名單。", - "radar.import.url": "貼文網址", - "radar.import.text": "貼文內文", - "radar.import.author": "作者(選填)", - "radar.import.addRow": "+新增一列", - "radar.import.removeRow": "移除", - "radar.import.submit": "送出匯入", - "radar.import.submitting": "匯入中…", - "radar.import.needRow": "至少填一列網址與內文", - "radar.import.csvOpen": "改用貼上 CSV", - "radar.import.csvClose": "收起 CSV", - "radar.import.csvLabel": "貼上 CSV", - "radar.import.csvHint": "格式:url,text,author(author 選填)。第一列若含 url/text 表頭會自動辨識,沒有就照 url,text,author 順序。", - "radar.import.csvApply": "套用到下方列表", - "radar.import.csvEmpty": "沒有解析出任何一列,請確認格式", - "radar.import.status.qualified": "已收進今日商機", - "radar.import.status.rejected": "已判定不符(仍留存)", - "radar.import.status.skipped": "已匯入過,略過", - "radar.import.status.failed": "匯入失敗", - - "radar.explore.open": "立即探索", - "radar.explore.close": "收起探索", - "radar.explore.title": "立即探索", - "radar.explore.hint": "用短關鍵字立刻搜 Threads 上「正在找你」的人,結果走同一套五問判定後進今日商機。每組最多 2 個詞、中文每詞 2–4 字。", - "radar.explore.loadingSuggest": "載入建議關鍵字…", - "radar.explore.suggestions": "建議短詞(點一下加入)", - "radar.explore.selected": "已選關鍵字", - "radar.explore.emptyTerms": "還沒有關鍵字,從上方建議點選或自己加一組。", - "radar.explore.removeChip": "點一下移除", - "radar.explore.addLabel": "自己加一組短詞", - "radar.explore.addPh": "例:保母 求推薦", - "radar.explore.add": "加入", - "radar.explore.run": "開始探索", - "radar.explore.running": "探索中…", - "radar.explore.needTerms": "至少選一組關鍵字", - "radar.explore.result": "找到 {hits} 則、判定 {judged} 則、收進 {created} 則", - "radar.explore.resultZeroHint": "這次沒有新商機。可換更口語的短詞(求推薦、有人知道),或到訂閱管理調整每日監控。", - "radar.explore.resultTruncated": "有 {n} 則因每日上限未收進,明天再來或升級方案。", - "radar.explore.termError.empty": "請輸入關鍵字", - "radar.explore.termError.tooLong": "整組去掉空格後最多 12 字(Threads 長字串常搜不到)", - "radar.explore.termError.tooManyTokens": "最多 2 個詞(用半形空格分隔)", - "radar.explore.termError.invalidToken": "不要標點、emoji、AND/OR 或過短/過長的詞", - "radar.explore.termError.duplicate": "這組詞已經加入了", - "radar.explore.termError.max": "一次最多 6 組關鍵字", - - "scout.promote": "收進商機", - "scout.promoted": "已複製進今日商機({band} · {score}),可到側欄「商機」跟進", - "scout.promoteFail": "收進商機失敗", - - "crm.board.title": "名單看板", - "crm.board.subtitle": "七階段+待追蹤;從商機加入後在這裡推進。", - "crm.board.link.today": "今日商機", - "crm.board.link.followups": "待追蹤", - "crm.board.link.stats": "轉換統計", - "crm.board.filters": "名單篩選", - "crm.board.search": "搜尋名單", - "crm.board.searchPlaceholder": "搜尋名稱或 Threads 帳號", - "crm.board.stageFilter": "階段", - "crm.board.allStages": "全部階段", - "crm.board.sort": "排序", - "crm.board.sortRecent": "最近接觸優先", - "crm.board.sortIntent": "意向分數優先", - "crm.board.clearFilters": "清除篩選", - "crm.board.results": "共 {n} 位聯絡人", - "crm.board.noResults": "找不到符合的聯絡人", - "crm.board.noResultsHint": "換個名稱、帳號或清除階段篩選。", - "crm.board.empty": "還沒有聯絡人", - "crm.board.emptyHint": "在今日商機按「加入名單」就會出現在這裡。", - "crm.board.oppCount": "筆商機", - "crm.board.lastTouch": "最近接觸 {time}", - "crm.board.noTouch": "尚無接觸時間", - "crm.board.stage": "目前階段", - "crm.board.conversion": "成交回報", - "crm.board.amount": "金額(可留空)", - "crm.board.reportWon": "回報成交", - "crm.board.notes": "備註", - "crm.board.noteLabel": "新增備註", - "crm.board.addNote": "儲存備註", - "crm.board.timeline": "時間軸", - "crm.board.timelineEmpty": "還沒有接觸紀錄", - "crm.board.opps": "相關商機", - "crm.board.markFollowUp": "標記待追蹤", - "crm.board.clearFollowUp": "取消待追蹤", - "crm.board.msg.stage": "階段已更新", - "crm.board.msg.followUp": "待追蹤已更新", - "crm.board.msg.won": "已回報成交", - "crm.board.msg.note": "備註已新增", - "crm.board.deleteTitle": "從工作名單移除", - "crm.board.deleteHint": "移除後不再顯示或提醒;原商機、接觸與成交歷史仍保留。", - "crm.board.delete": "移除這位聯絡人", - "crm.board.deleting": "移除中…", - "crm.board.confirmDelete": "確定從名單移除「{name}」嗎?既有商機、接觸與成交紀錄會保留。", - "crm.board.msg.deleted": "已從名單移除", - - "crm.stage.new_found": "新發現", - "crm.stage.engaged": "已接觸", - "crm.stage.dm_sent": "已私訊", - "crm.stage.replied": "已回覆", - "crm.stage.quoted": "已報價", - "crm.stage.won": "成交", - "crm.stage.lost": "流失", - "crm.stage.needs_follow_up": "待追蹤", - - "crm.followups.title": "待追蹤", - "crm.followups.subtitle": "到期回訪、延後與 AI 草稿。", - "crm.followups.link.board": "名單看板", - "crm.followups.link.stats": "轉換統計", - "crm.followups.empty": "目前沒有待追蹤", - "crm.followups.emptyHint": "標記聯絡人待追蹤或送出回覆後會出現在這裡。", - "crm.followups.due": "到期", - "crm.followups.openContact": "開啟聯絡人", - "crm.followups.aiMessage": "AI 追蹤草稿", - "crm.followups.done": "完成", - "crm.followups.snooze": "延後 3 天", - "crm.followups.escalatedHint": "已提醒兩次仍無動作,建議考慮轉未成交。", - "crm.followups.status.scheduled": "排程中", - "crm.followups.status.notified": "已通知", - "crm.followups.status.done": "已完成", - "crm.followups.status.snoozed": "已延後", - "crm.followups.status.escalated": "需升級處理", - "crm.followups.msg.done": "已完成", - "crm.followups.msg.snoozed": "已延後 3 天", - "crm.followups.msg.drafted": "追蹤草稿已產生", - - "crm.stats.title": "轉換統計", - "crm.stats.subtitle": "關鍵字、回覆版本與來源。樣本不足不下結論。", - "crm.stats.terms": "關鍵字轉換", - "crm.stats.variants": "回覆版本成功率", - "crm.stats.sources": "成交來源", - "crm.stats.emptyDim": "尚無足夠資料", - "crm.stats.insufficient": "樣本不足", - "crm.stats.col.term": "關鍵字", - "crm.stats.col.variant": "版本", - "crm.stats.col.source": "來源", - "crm.stats.col.accepted": "加入", - "crm.stats.col.replied": "回覆", - "crm.stats.col.won": "成交", - "crm.stats.col.used": "使用", - "crm.stats.col.rate": "比率", +const loaders: Record Promise> = { + "zh-TW": async () => zhTW, + en: async () => (await import("./catalog.en")).en, }; -export const en: MessageDict = { - "app.name": "Lapras", - "app.nameEn": "Lapras", - "app.nameZh": "巡樓", - "app.tagline": "Patrol Threads with ease", - "app.taglineEn": "Patrol Threads with ease", - "nav.today": "Today", - "nav.crew": "Accounts", - "nav.studio": "Studio", - "nav.radar": "Demand", - "nav.crm": "CRM", - "nav.scout": "Topics", - "nav.outbox": "Outbox", - "nav.jobs": "Jobs", - "nav.brands": "Brands", - "nav.policy": "Opportunity policy", - "nav.playbooks": "Playbooks", - "nav.insights": "Insights", - "nav.benchmark": "Benchmark", - "nav.utm": "UTM", - "nav.more": "More", - "nav.moreTitle": "More", - "nav.users": "Islanders", - "nav.usage": "Usage & plans", - "nav.profile": "Profile", - "nav.invite": "Invites", - "nav.settings": "Settings", - "nav.logout": "Log out", - "nav.navigate": "Navigate", - "navGroup.workflow": "Workflow", - "navGroup.accounts": "Accounts & brands", - "navGroup.growth": "Growth tools", +const inflight = new Map>(); - "workspace.label": "Workspace", - "workspace.default": "Default", - "workspace.new": "+ New workspace", - "workspace.newPrompt": "New workspace name", - - "common.save": "Save", - "common.saving": "Saving…", - "common.cancel": "Cancel", - "common.close": "Close", - "common.loading": "Loading…", - "common.retry": "Retry", - "common.back": "Back", - "common.delete": "Delete", - "common.edit": "Edit", - "common.search": "Search", - "common.confirm": "Confirm", - "common.optional": "Optional", - "common.success": "Saved", - "common.error": "Something went wrong", - "common.yes": "Yes", - "common.no": "No", - - "help.open": "Help", - "help.kicker": "This page", - "help.section.what": "What this page is for", - "help.section.how": "How to use it", - "help.section.tips": "Tips", - "help.section.related": "Related", - "help.shortcutHint": "Open via the “?” next to the page title. Shortcut: ? toggles; Esc closes.", - - "help.page.generic.title": "Lapras console", - "help.page.generic.what": "Your work desk. Use the sidebar (or mobile dock) to switch features; the top bar has usage, notifications, and this help.", - "help.page.generic.step1": "Pick a task from the nav (patrol, radar, outbox, …).", - "help.page.generic.step2": "Open Help from the top bar or press ? when you need context.", - "help.page.generic.step3": "Settings, profile, and plans live in the account menu.", - "help.page.generic.tips": "Help never changes your data; open and close anytime.", - - "help.page.today.title": "Today", - "help.page.today.what": "Daily dashboard: opportunities, patrol queue, outbox pulse, and account health so you know what to do first.", - "help.page.today.step1": "Check the opportunities summary; open Radar if there are leads.", - "help.page.today.step2": "Clear pending patrol replies.", - "help.page.today.step3": "Track failed or active Outbox items from here.", - "help.page.today.tips": "When there are no opportunities, the summary guides you to Opportunity policy or watches.", - - "help.page.crew.title": "Accounts (Crew)", - "help.page.crew.what": "Connected Threads accounts, health, and usability. Publishing and outreach start from accounts here.", - "help.page.crew.step1": "Connect at least one usable account via OAuth.", - "help.page.crew.step2": "Check connection and health (avoid auto-send on throttle).", - "help.page.crew.step3": "Tune AI and dev options under Settings if needed.", - "help.page.crew.tips": "Warn/throttle health blocks automatic public sends; copy manually instead.", - - "help.page.studio.title": "Studio", - "help.page.studio.what": "Draft posts, personas, inspiration, and plays; finished work goes to Outbox to publish.", - "help.page.studio.step1": "Pick a persona or brand voice, then draft.", - "help.page.studio.step2": "Use inspiration/mimic tools, then edit by hand.", - "help.page.studio.step3": "Send to Outbox and confirm the schedule there.", - "help.page.studio.tips": "Studio drafts are not published until Outbox succeeds.", - - "help.page.scout.title": "Topic ideas", - "help.page.scout.what": "Find lively Threads topics to join, draft a reply, and mark done. To find people looking for your service, use Demand (daily watch or Explore now).", - "help.page.scout.step1": "Enter topic keywords, generate queries, then edit them.", - "help.page.scout.step2": "Confirm search, then work the queue sorted by post time.", - "help.page.scout.step3": "Draft, open Threads to reply, mark done.", - "help.page.scout.tips": "Topics = content ideas. Demand = finding customers. Use the Demand page for leads.", - - "help.page.radar_today.title": "Demand patrol", - "help.page.radar_today.what": "Run a scheduled or immediate patrol to find pains your product can solve, or new posts. Keep or discard after you read the reason.", - "help.page.radar_today.step1": "Check the patrol desk: whether daily patrol is on, when it last ran, and run one now.", - "help.page.radar_today.step2": "Read why it was recommended. Keep a fit, discard the rest.", - "help.page.radar_today.step3": "Add to contacts only if you want to follow that person. Contacts are optional.", - "help.page.radar_today.tips": "Immediate and daily patrol can both stay on. Turning one off does not hide the other.", - - "help.page.radar_watches.title": "Patrol setup", - "help.page.radar_watches.what": "Pick a product and keywords. Daily patrol runs on a schedule; you can also run one immediately. Results land on Demand.", - "help.page.radar_watches.step1": "Choose brand and product, then fill the pain map.", - "help.page.radar_watches.step2": "Add terms buyers type and excludes.", - "help.page.radar_watches.step3": "Leave daily patrol on, or hit Run now.", - "help.page.radar_watches.tips": "Active slots are plan-capped; pause one to free a slot.", - - "help.page.crm_board.title": "Contact management", - "help.page.crm_board.what": "A searchable, filterable work list of contacts accepted from Today’s demand, with notes, wins, and timelines.", - "help.page.crm_board.step1": "On Today’s demand, hit Add to CRM; people land in New.", - "help.page.crm_board.step2": "Open a contact to move stage, note, or flag follow-up.", - "help.page.crm_board.step3": "Report a win when you close (amount optional).", - "help.page.crm_board.tips": "Remove contacts you no longer need from the work list; opportunity, touch, and conversion history remains.", - - "help.page.crm_followups.title": "Follow-ups", - "help.page.crm_followups.what": "Due revisits: done, snooze, or AI draft for a manual message.", - "help.page.crm_followups.step1": "Check due date and status (including escalated).", - "help.page.crm_followups.step2": "Generate an AI draft if you need copy, then send yourself.", - "help.page.crm_followups.step3": "Mark done or snooze three days.", - "help.page.crm_followups.tips": "Nothing is auto-messaged; drafts are for copy only.", - - "help.page.crm_stats.title": "Conversion stats", - "help.page.crm_stats.what": "Conversion by term, reply variant, and source. No ranking when samples are thin.", - "help.page.crm_stats.step1": "Read absolute counts first.", - "help.page.crm_stats.step2": "Ignore ranking on “insufficient sample” rows.", - "help.page.crm_stats.step3": "Tune watches and reply style from what you learn.", - "help.page.crm_stats.tips": "Rates stay hidden on small samples on purpose.", - - "help.page.outbox.title": "Outbox", - "help.page.outbox.what": "Schedule and send queue — last stop before Threads publish.", - "help.page.outbox.step1": "Review pending and failed items.", - "help.page.outbox.step2": "Retry failures or edit the draft.", - "help.page.outbox.step3": "Track long jobs under Jobs as well.", - "help.page.outbox.tips": "Health throttle blocks automatic send.", - - "help.page.jobs.title": "Jobs", - "help.page.jobs.what": "Background work (scans, sweeps, analysis) with progress and outcomes.", - "help.page.jobs.step1": "Scan status: queued / running / success / failed.", - "help.page.jobs.step2": "Open a job for the progress summary.", - "help.page.jobs.step3": "On failure, retry from the related feature.", - "help.page.jobs.tips": "Radar “Sweep now” creates a radar_sweep job here.", - - "help.page.brands.title": "Brands", - "help.page.brands.what": "Maintain brands and products so demand search knows what you sell and which pains you solve.", - "help.page.brands.step1": "Maintain brand and product basics.", - "help.page.brands.step2": "Add product contexts, pain points, match tags, and capability terms.", - "help.page.brands.step3": "Set pricing, regions, forbidden words, cases, and tone under the standalone Opportunity policy page.", - "help.page.brands.tips": "Richer product data improves query preprocessing and product matching.", - - "help.page.policy.title": "Opportunity policy", - "help.page.policy.what": "Shared qualification and reply policy, kept separate from brand and case management.", - "help.page.policy.step1": "Set services, pricing, and service areas.", - "help.page.policy.step2": "Add forbidden words, cases, FAQs, availability, and tone.", - "help.page.policy.step3": "Save once; qualification, watch activation, and reply generation use this policy.", - "help.page.policy.tips": "Policy is workspace-wide. Maintain individual brands and products on Brands.", - - "help.page.playbooks.title": "Playbooks", - "help.page.playbooks.what": "Share or adopt patrol briefs, personas, and play templates.", - "help.page.playbooks.step1": "Browse templates.", - "help.page.playbooks.step2": "Adopt into your workspace and edit.", - "help.page.playbooks.step3": "Use them in Patrol or Studio.", - "help.page.playbooks.tips": "Templates are starting points — rewrite for your brand.", - - "help.page.insights.title": "Insights", - "help.page.insights.what": "Post and engagement performance so you can double down on what works.", - "help.page.insights.step1": "Sync or review recent post metrics.", - "help.page.insights.step2": "Feed winners back into Studio.", - "help.page.insights.step3": "Compare with Benchmark when available.", - "help.page.insights.tips": "Sync lag depends on the platform; not second-level live.", - - "help.page.benchmark.title": "Benchmark", - "help.page.benchmark.what": "Anonymous site-wide medians when sample size is enough.", - "help.page.benchmark.step1": "Read metrics that have samples.", - "help.page.benchmark.step2": "Don’t over-read thin samples.", - "help.page.benchmark.step3": "Adjust content strategy from the gap.", - "help.page.benchmark.tips": "Meaningful medians usually need sample size ≥ 5.", - - "help.page.utm.title": "UTM", - "help.page.utm.what": "Build UTM links so you can attribute traffic later.", - "help.page.utm.step1": "Fill campaign and source params.", - "help.page.utm.step2": "Use the link in posts or DMs.", - "help.page.utm.step3": "Match results in your analytics tool.", - "help.page.utm.tips": "Keep naming consistent for clean reports.", - - "help.page.settings.title": "Settings", - "help.page.settings.what": "AI providers, search keys, and interface preferences.", - "help.page.settings.step1": "Confirm AI keys (platform or BYOK).", - "help.page.settings.step2": "Search keys are optional — use your own or the platform default.", - "help.page.settings.step3": "Return to a feature page and verify.", - "help.page.settings.tips": "Bad keys show up as generation/scan failures in usage or jobs.", - - "help.page.profile.title": "Profile", - "help.page.profile.what": "Your member account, password, and basic prefs.", - "help.page.profile.step1": "Update display name and basics.", - "help.page.profile.step2": "Change password if needed.", - "help.page.profile.step3": "Language/theme live in UI prefs.", - "help.page.profile.tips": "Member profile is separate from Threads connections (Crew).", - - "help.page.invite.title": "Invites", - "help.page.invite.what": "Invite links, downline relations, and rewards info.", - "help.page.invite.step1": "Copy and share your invite link.", - "help.page.invite.step2": "Review established relations.", - "help.page.invite.step3": "Rewards follow in-product rules.", - "help.page.invite.tips": "Don’t spam invites; abuse can suspend accounts.", - - "help.page.usage.title": "Usage & plan", - "help.page.usage.what": "Credits, meter usage, and plan caps.", - "help.page.usage.step1": "Check each meter.", - "help.page.usage.step2": "Near limits, upgrade or use BYOK.", - "help.page.usage.step3": "Plan details are on the Plans page.", - "help.page.usage.tips": "BYOK usually counts calls only (see on-page labels).", - - "help.page.usage_plans.title": "Plans", - "help.page.usage_plans.what": "Compare and choose a paid plan.", - "help.page.usage_plans.step1": "Compare caps and features.", - "help.page.usage_plans.step2": "Start checkout for a plan.", - "help.page.usage_plans.step3": "Confirm entitlements under Usage.", - "help.page.usage_plans.tips": "Price and caps are those shown at checkout.", - - "help.page.usage_checkout.title": "Checkout", - "help.page.usage_checkout.what": "Pay for the selected plan.", - "help.page.usage_checkout.step1": "Confirm plan and amount.", - "help.page.usage_checkout.step2": "Complete payment.", - "help.page.usage_checkout.step3": "Return to Usage to verify.", - "help.page.usage_checkout.tips": "Keep the receipt if payment fails and contact support.", - - "help.page.admin_users.title": "Members admin", - "help.page.admin_users.what": "Admin tools for members, suspend, and roles.", - "help.page.admin_users.step1": "Search or browse members.", - "help.page.admin_users.step2": "Change status or role carefully.", - "help.page.admin_users.step3": "Record reasons for sensitive actions.", - "help.page.admin_users.tips": "Admins only; mistakes can lock others out.", - - "api.err.unknown": "Something went wrong. Please try again.", - "api.err.studioValidation": "Invalid data. Please check and try again.", - "api.err.crawlerSession": "Sync the Chrome session in Settings, then try again.", - "api.err.network": "Cannot reach the server. Check your network or if the API is running.", - "api.err.400001": "Invalid request", - // Same sentence as backend 400003 / ValidatePasswordPolicy - "api.err.400003": "Password must be at least 12 characters with upper, lower, digit, and symbol", - "password.policy.hint": "Password must be at least 12 characters with upper, lower, digit, and symbol", - "password.policy.minLen": "Password must be at least 12 characters with upper, lower, digit, and symbol", - "password.policy.needUpper": "Password must be at least 12 characters with upper, lower, digit, and symbol", - "password.policy.needLower": "Password must be at least 12 characters with upper, lower, digit, and symbol", - "password.policy.needDigit": "Password must be at least 12 characters with upper, lower, digit, and symbol", - "password.policy.needSymbol": "Password must be at least 12 characters with upper, lower, digit, and symbol", - - "api.err.400004": "Invalid or expired verification code", - "api.err.400020": "You cannot suspend your own account", - "api.err.400021": "Cannot demote or suspend the last active admin", - "auth.unreachable": "Can't reach the server, so your sign-in state is unknown. Check your connection and retry.", - "api.err.401001": "Please sign in", - "api.err.401002": "Session expired. Please sign in again.", - "api.err.401003": "Account not found. Please sign in again.", - "api.err.401010": "Invalid email or password", - "api.err.403001": "Account suspended", - "api.err.403002": "Admin access required", - "api.err.404001": "Not found", - "api.err.404002": "This email is not registered", - "api.err.409001": "This email is already registered", - "api.err.402001": "Monthly platform credits exhausted. Upgrade or wait for next month.", - "usage.err.meterCap": "This feature hit its monthly credit cap. Try another feature or upgrade.", - "usage.err.platformCapacity": "Platform AI/search is busy or rate-limited. Try again later, or add your own API key (BYOK) in Settings.", - "api.err.500000": "Server error. Please try again later.", - "api.err.timeoutAI": "AI timed out. Shorten structure notes, or pick a faster model in Settings", - "api.err.501000": "This feature is not implemented yet", - "api.err.501010": "This capability is not available yet; a later release will ship it", - "api.err.400100": "Some fields do not meet the rules. Please review and resubmit", - - "api.ok.verifySent": "Verification code sent. Please check your email.", - "api.ok.verifyIssued": "Verification code issued", - "api.ok.resetSent": "Reset email sent. Please check your inbox.", - "api.ok.passwordUpdated": "Password updated", - "api.ok.loggedOut": "Signed out", - "api.ok.unbound": "Unbound", - - "topbar.notifications": "Notifications", - "topbar.unread": "{n} unread", - "topbar.markAllRead": "Mark all read", - "topbar.noNotifications": "No notifications", - "topbar.jobsCenter": "Job center", - "topbar.moreOlder": "{n} older", - "topbar.account": "Account menu", - - "role.admin": "Admin", - "role.member": "Member", - "role.verified": "Verified", - "role.unverified": "Unverified", - - "login.title": "Sign in to Lapras", - "login.email": "Email", - "login.password": "Password", - "login.showPassword": "Show password", - "login.hidePassword": "Hide password", - "login.submit": "Sign in", - "login.submitting": "Signing in…", - "login.forgot": "Forgot password?", - "login.mockHint": "Admin demo@harbor.local / demo · member alice@harbor.local / alice", - "login.liveHint": "Live backend: admin@haixun.local / admin123 (gateway :8888)", - - "forgot.title": "Forgot password", - "forgot.submit": "Send reset link", - "forgot.submitting": "Sending…", - "forgot.back": "Back to sign in", - "forgot.hint": "Enter your registered email. We'll send a reset link and code.", - "forgot.mockMail": "Reset email", - "forgot.openReset": "Enter code / reset password", - "forgot.retry": "Try again", - "forgot.nextStep": "Check your email for the code or link, then open the reset page to set a new password.", - - "reset.title": "Set new password", - "reset.hint": "Enter email, verification code (from the email), and a new password.", - "reset.code": "Verification code", - "reset.codePh": "6-digit code from email", - "reset.needEmail": "Email is required", - "reset.needCode": "Verification code is required", - "reset.newPassword": "New password", - "reset.confirm": "Confirm password", - "reset.submit": "Update password", - "reset.submitting": "Updating…", - "reset.missingToken": "Missing token. Please request a new reset link.", - - "verify.title": "Verify email", - "verify.pending": "Not verified", - "verify.body": "You're signed in, but features stay locked until you verify your email with the 6-digit code.", - "verify.code": "Verification code", - "verify.submit": "Verify", - "verify.submitting": "Verifying…", - "verify.resend": "Resend code", - "verify.sending": "Sending…", - "verify.logout": "Log out", - "verify.mockMail": "Verification email", - "verify.mockHint": "Enter the code:", - "verify.after": "After verification you can use Today, Studio, Patrol, and more.", - - "settings.title": "Settings", - "settings.loadFail": "Could not load settings. Refresh to try again.", - "settings.localeCurrency": "Language & currency", - "settings.locale": "Interface language", - "settings.currency": "Display currency", - "settings.currencyHint": "Display only; plan billing stays in TWD.", - "settings.localeSaved": "Language and currency updated", - "settings.appearance": "Appearance", - "settings.theme": "Theme", - "settings.themeLight": "Light", - "settings.themeDark": "Dark", - "settings.themeSystem": "System", - "settings.themeHint": "Light, dark, or follow system appearance.", - "settings.themeSaved": "Theme updated", - "settings.themeToLight": "Switch to light", - "settings.themeToDark": "Switch to dark", - "settings.dataSource": "Data source", - "settings.dataSourceHint": - "Business data always uses live API. Mock keeps only a few local modules (e.g. invite). Prefer Live + gateway.", - "settings.dataSourceMock": "Mock (reduced)", - "settings.dataSourceLive": "Live (backend API)", - "settings.dataSourceCurrent": "Current", - "settings.dataSourceSwitchedLive": - "Switched to Live — re-login with backend account (admin@haixun.local / admin123)", - "settings.dataSourceSwitchedMock": "Switched back to Mock (non-invite still hits live)", - "settings.dataSourceLiveNeedGateway": "Requires gateway on :8888 (or Vite proxy /api)", - "settings.ai": "AI", - "settings.search": "Search", - "settings.threads": "Threads OAuth (platform)", - "settings.threadsHint": - "App ID/Secret live in gateway platform config (yaml/env); Secret is never shown. Paste Callback into Meta app settings and connect via the public https site.", - "settings.threadsProvider": "Mode", - "settings.threadsProviderFake": "Fake (dev OAuth flow)", - "settings.threadsProviderMeta": "Meta production", - "settings.threadsConfigured": "App credentials", - "settings.threadsConfiguredYes": "Configured", - "settings.threadsConfiguredNo": "Not set", - "settings.threadsCallback": "OAuth Callback URL (redirect_uri)", - "settings.threadsCallbackHint": "Paste this exact URL into Meta App → Valid OAuth Redirect URIs (https required)", - "settings.threadsCopy": "Copy", - "settings.threadsCopied": "Callback URL copied", - "settings.threadsCopyFail": "Copy failed — select manually", - "settings.threadsPublicWeb": "Public site origin", - "settings.threadsGoCrew": "Open Crew to connect", - "settings.member": "Account & sign-in", - "settings.usageCard": "AI / search quota", - "settings.usageCardHint": "Platform credits and plan caps; BYOK does not use platform credits.", - "settings.viewUsage": "View usage", - "settings.editProfile": "Edit profile", - "settings.mockLogin": "demo@harbor.local / demo", - "settings.memberHint": "Sign-in account, display name, and notification prefs.", - - "usage.title": "Usage & plans", - "usage.desc": "See platform credits this month, per-feature usage, and change plans.", - "usage.creditsUsed": "Credits used this month", - "usage.remaining": "{n} left", - "usage.percentUsed": "{n}% used", - "usage.breakdown": "Breakdown", - "usage.plans": "Plans", - "usage.current": "Current", - "usage.inUse": "Active", - "usage.switchMock": "Switch plan", - "usage.perMonth": "credits / mo", - "usage.ledger": "Recent usage", - "usage.emptyTitle": "No usage this month", - "usage.emptyDesc": "After you create, patrol, or generate images, usage events show up here.", - "usage.planNote": "Resets each calendar month; unused credits do not roll over.", - "usage.switched": "Switched to {name}", - "usage.meter.times": "{count} runs · {credits} credits", - "usage.meter.timesShort": "{n} runs", - "usage.meter.pt": "pt", - "usage.meter.over": "over", - "usage.meter.locked": "locked", - "usage.meter.cap": "Cap {credits}/{cap} credits", - "usage.side.aiCredits": "AI credits used (copy + research + image)", - "usage.side.searchCredits": "Search credits used", - "usage.byok.title": "BYOK usage", - "usage.byok.hint": "Calls only; does not affect platform credits or progress bars.", - "usage.plan.free.blurb": "Enough to try the full creation flow", - "usage.plan.starter.blurb": "Small teams posting and patrolling daily", - "usage.plan.pro.blurb": "Multi-account, heavy AI and research", - - "profile.title": "Profile", - "profile.desc": "Manage display name, avatar, and password.", - "profile.accountStatus": "Account status", - "profile.basic": "Basics", - "profile.avatar": "Avatar", - "profile.avatarUpload": "Upload avatar", - "profile.avatarRemove": "Remove avatar", - "profile.avatarHint": "JPG / PNG / WebP, up to 5MB. Pick a file then Save basic info; Remove clears immediately.", - "profile.avatarSaved": "Avatar updated", - "profile.avatarCleared": "Avatar removed", - "profile.avatarFail": "Could not read image", - "profile.displayName": "Display name", - "profile.bio": "Bio (optional)", - "profile.timezone": "Timezone", - "profile.notifyEmail": "Email notifications", - "profile.password": "Change password (optional)", - "profile.currentPassword": "Current password", - "profile.newPassword": "New password", - "profile.confirmPassword": "Confirm new password", - "profile.emailVerified": "Email verified", - "profile.emailUnverified": "Email not verified", - "profile.roleAdmin": "Admin", - "profile.roleMember": "Member", - "profile.loginEmail": "Sign-in email: {email}", - "profile.verifiedAt": " · verified {time}", - "profile.goVerify": "Verify email", - "profile.roleTags": "Role tags: {labels}", - "profile.roleNote": "Roles are assigned by the system; re-verify after changing email.", - "profile.saveBasic": "Save profile", - "profile.updatePassword": "Update password", - "profile.saved": "Profile saved", - "profile.savedUnverified": "Saved. Verify your email before using features.", - "profile.saveFail": "Could not save", - "profile.needNewPassword": "Enter a new password", - "profile.passwordMismatch": "New passwords do not match", - "profile.needCurrentPassword": "Enter your current password", - "profile.wrongCurrentPassword": "Current password is incorrect", - "profile.passwordUpdated": "Password updated", - "profile.passwordFail": "Could not change password", - "profile.listJoin": ", ", - "profile.tenantUid": " · tenant {tenant} · uid {uid}", - "profile.inviteBadge": "Invite code", - "profile.inviteHint": "Share with friends to join; later events can use invite relations.", - "profile.gotoInvite": "View invites", - - "invite.title": "Invites", - "invite.desc": "Invite codes and relations; admin view is a tree.", - "invite.tabs": "Invite views", - "invite.tab.mine": "My invites", - "invite.tab.tree": "Tree", - "invite.myCode": "My invite code", - "invite.codeLabel": "Invite code", - "invite.copyCode": "Copy", - "invite.copied": "Copied", - "invite.copyFail": "Copy failed", - "invite.stats": "{direct} direct · {total} in chain", - "invite.rewards.summary": "Reward points total {total} · this month {month} / cap {cap}", - "invite.rewards.title": "Reward history", - "invite.upline": "Invited by", - "invite.downlines": "Direct invites · {n}", - "invite.noUpline": "No inviter", - "invite.noDownline": "None yet", - "invite.directN": "{n} direct", - "invite.claimHint": "If you didn’t enter a code at signup, add your inviter’s code here (cannot change later).", - "invite.claimLabel": "Invite code", - "invite.claimPh": "e.g. HX-DEMO01", - "invite.claimSubmit": "Bind", - "invite.claimOk": "Bound to {name}", - "invite.claimOkGeneric": "Inviter bound", - "invite.claimFail": "Could not bind", - "invite.claimLocked": "Already bound. Contact an admin to change.", - "invite.loadFail": "Failed to load", - "invite.treeEmpty": "No data", - "invite.treeSearch": "Search", - "invite.treeSearchPh": "Name / email / code", - "invite.treeCount": "{n} people", - "invite.treeMatchCount": "{n} matches · {total} total", - "invite.treeNoMatch": "No matches", - "invite.treeNoMatchHint": "Try another keyword", - "invite.clearSearch": "Clear", - "invite.moveTitle": "Reassign", - "invite.newParent": "Inviter", - "invite.root": "(None · standalone)", - "invite.confirmMove": "Confirm", - "invite.moved": "Reassigned “{name}” → “{parent}”", - "invite.moveFail": "Failed", - "invite.openFromAdmin": "Invites", - "invite.parentField": "Invited by", - "invite.orgPickRoot": "Pick a starting root (includes self-joined)", - "invite.orgRoots": "Roots", - "invite.orgPath": "Path", - "invite.orgChainN": "{n} in chain", - "invite.orgFocusStats": "{direct} direct · {total} in chain", - "invite.orgDirects": "Direct invites · {n}", - "invite.orgNoDirects": "No direct invites", - "invite.err.notFound": "Member not found", - "invite.err.notLoggedIn": "Not signed in", - "invite.err.needAdmin": "Admin access required", - "invite.err.memberNotFound": "Islander not found", - "invite.err.selfParent": "Cannot set yourself as inviter", - "invite.err.parentNotFound": "Inviter not found", - "invite.err.cycle": "Cannot place under your own invite chain (would create a cycle)", - "invite.err.repoMissing": "Invite data layer not loaded — please refresh the page", - "invite.err.alreadyBound": "You already have an inviter", - "invite.err.codeRequired": "Enter an invite code", - "invite.err.codeNotFound": "Invite code not found", - "invite.err.codeSelf": "You can’t use your own code", - - "admin.users.title": "Islanders", - "admin.users.desc": "Manage islander accounts, roles, plans, and unlimited flags.", - "admin.users.needAdmin": "Admin access required", - "admin.users.list": "Islanders · {n}", - "admin.users.detail": "Islander detail", - "admin.users.pick": "Select an islander", - "admin.users.search": "Search name / uid", - "admin.users.searchPh": "Name, email, or uid", - "admin.users.create": "Add islander", - "admin.users.suspend": "Suspend", - "admin.users.unsuspend": "Restore", - "admin.users.suspended": "Suspended", - "admin.users.active": "Active", - "admin.users.onboarding": "First-run", - - "crew.title": "Accounts", - "crew.tab.accounts": "Accounts", - "crew.tab.personas": "Personas", - "crew.connect": "Connect account", - "crew.connecting": "Connecting…", - "crew.tokenRenewHint": "Tokens auto-renew via background jobs (~day 30). Check Jobs; no manual refresh.", - "crew.empty": "No accounts yet", - "crew.loadFail": "Could not load accounts", - "crew.unusable": "Unavailable", - "crew.expires": "Expires {time}", - "crew.lastRefresh": "Last extended {time}", - "crew.refreshSession": "Extend token", - "crew.refreshSessionHint": "Uses stored auth to refresh the token — no OAuth page. On failure, reconnect.", - "crew.session.ok": "Token valid", - "crew.session.soon": "Expiring soon", - "crew.session.expired": "Token expired", - "crew.session.unknown": "Expiry unknown", - "crew.connection.connected": "Connected", - "crew.connection.error": "Error", - "crew.connection.disconnected": "Disconnected", - "crew.connection.unknown": "Unknown", - "crew.health.needsReconnect": "Reconnect required", - "crew.health.needsReconnectHint": "Token unusable — use Connect account (not Extend token)", - "crew.health.disconnectedHint": "Unlinked", - "crew.health.expiredHint": "Extend token or reconnect", - "crew.opHealthScore": "Health {n}", - "crew.msg.refreshed": "@{user} token extended with stored auth (~60 days)", - "crew.msg.refreshedAll": "Extended {n} account tokens with stored auth", - "crew.msg.refreshFail": "Extend failed (reconnect if token is invalid)", - "crew.msg.oauthOk": "Threads connected; token renew scheduled ~day 30 (see Jobs)", - "crew.msg.oauthFail": "OAuth connection failed", - "crew.msg.oauthUrlFail": "Could not get authorize URL. Try again or check platform Threads settings.", - "crew.msg.deleted": "Removed @{user}", - "crew.confirmDelete": "Delete account @{user}?\nIt can no longer be used as lead / cast.", - - "today.findTopic": "Find topics", - "today.refreshTopics": "Refresh topics", - "today.reload": "Reload", - "today.loadFail": "Could not load Today", - "today.trendsFail": "Could not refresh topics (quota or search not set)", - "today.trendsUpdated": "Updated {n} topics", - "today.syncPosts": "Sync own posts", - "today.syncPostsFail": "Could not sync posts", - "today.needAccount": "Connect a Threads account first", - "today.outcome.title": "This week's outcomes", - "today.outcome.reach": "Reach", - "today.outcome.conversations": "Conversations", - "today.outcome.follows": "Follows", - "today.outcome.followsHint": "Possibly related, no strong signal yet", - "today.outcome.followsConfirmedHint": " / confirmed {n}", - "today.outcome.conversions": "Conversions", - "today.outcome.emptyHint": "No patrol outreach outcomes this week yet — go give patrol a try?", - "today.checkup.empty": "This week's checkup hasn't been generated yet. It runs automatically next Monday in your timezone.", - "today.checkup.prefix": "Checkup: ", - "today.pendingReplies": "Pending replies", - "today.pendingRepliesN": "Pending · {n}", - "today.newThread": "Compose", - "today.metricsAria": "Today metrics", - "today.metric.pending": "Pending", - "today.metric.pendingHint": "Patrol queue", - "today.metric.doneGoal": "Done / goal", - "today.metric.doneGoalHint": "Patrol completions today / goal (mark published counts)", - "today.metric.sentToday": "Sent today", - "today.metric.running": "{n} in progress", - "today.metric.sentDone": "Completed sends", - "today.metric.failed": "Send issues", - "today.metric.needAction": "Needs action", - "today.metric.ok": "All good", - "today.metric.mentions": "Mentions", - "today.metric.mentionsHint": "Pending mentions", - "today.pending.title": "Patrol pending · {n}", - "today.pending.empty": "Nothing pending — run a patrol", - "today.goScout": "Go patrol", - "today.pending.more": "{n} more →", - "today.pending.handle": "Handle in patrol", - "today.pending.start": "Start handling", - "today.topics.title": "Find topics", - "today.topics.empty": "No topics yet — tap Refresh topics", - "today.goStudio": "Open inspire", - "today.heat": "Heat {n}", - "today.topicAngle": "Good opening angle", - "today.moreInspire": "More inspiration", - "today.useTopic": "Brainstorm topic", - "today.outbox.title": "Today's outbox", - "today.outbox.empty": "No sends today. Try", - "today.outbox.emptyMid": ", then check", - "today.outbox.emptyEnd": ".", - "today.outbox.summary": "Done {sent} · In progress {running} · Issues {failed}", - "today.badge.failed": "Failed", - "today.badge.scheduling": "Scheduled", - "today.badge.sending": "Sending", - "today.badge.drafted": "Drafted", - "today.openOutbox": "Open outbox", - "today.accounts.title": "Account performance", - "today.accounts.empty": "No stats yet — sync own posts", - "today.postsCount": "{n} posts", - "today.views": "Views", - "today.likes": "Likes", - "today.repliesShort": "Replies", - "today.fullInsights": "Full insights · MoM charts", - "today.viewPosts": "View posts", - "today.manageAccounts": "Manage accounts", - - - "outbox.title": "Outbox", - "outbox.tabsAria": "Outbox tabs", - "outbox.tab.active": "Active", - "outbox.tab.history": "History", - "outbox.empty": "No outbox items", - "outbox.activeEmpty": "Nothing in progress", - "outbox.historyEmpty": "No history yet", - "outbox.historyN": "History ({n})", - "outbox.backActive": "Back to active ({n})", - "outbox.progress": "Progress {progress}", - "outbox.detail": "Details", - "outbox.deleting": "Deleting…", - "outbox.confirmDelete": "Delete outbox item “{title}”?\nThis cannot be undone.", - "outbox.deleted": "Deleted “{title}”", - "outbox.deleteFail": "Delete failed", - "outbox.loadFail": "Failed to load outbox", - "outbox.status.scheduling": "Scheduling", - "outbox.status.active": "Sending", - "outbox.status.completed": "Completed", - "outbox.status.partial_failed": "Partial failure", - "outbox.status.cancelled": "Cancelled", - "outbox.detail.missingId": "Missing id", - "outbox.detail.notFound": "Outbox item not found", - "outbox.detail.loadFail": "Could not load this outbox item", - "outbox.detail.loading": "Loading…", - "outbox.detail.sendingHint": "Publishing to Threads (often 10–30s). This page updates automatically…", - "outbox.detail.doneHint": "Published to Threads successfully.", - "outbox.detail.markAllOk": "Mark all success", - "outbox.detail.markRootFail": "Mark root failed", - "outbox.detail.processing": "Working…", - "outbox.detail.delete": "Delete this", - "outbox.detail.back": "Back to list", - "outbox.detail.root": "Root post", - "outbox.detail.replyN": "Reply {n}", - "outbox.detail.retry": "Retry", - "outbox.detail.opFail": "Action failed", - "outbox.step.published": "Published", - "outbox.step.failed": "Failed", - "outbox.step.publishing": "Publishing…", - "outbox.step.scheduled": "Scheduled", - "outbox.step.blocked": "Blocked", - - "studio.title": "Studio", - "studio.account": "Account", - "studio.persona": "Persona", - "studio.personaReady": "Persona ready", - "studio.personaNotReady": "Persona not ready", - "studio.tab.posts": "My posts", - "studio.tab.mentions": "Mentions @", - "studio.tab.compose": "Compose", - "studio.tab.plays": "Plays", - "studio.tab.inspire": "Inspire", - "studio.tab.insights": "Insights", - - "mentions.hint": "Who @ you. {n} pending. You can change account/persona per item (defaults from top bar).", - "mentions.scoutLink": "Patrol outreach", - "mentions.empty": "No mentions", - "mentions.emptyHint": "Click “Sync from Threads” to pull posts/replies/quotes that @ you. Re-connect if missing threads_manage_mentions.", - "mentions.needAccount": "Select a Threads account in the top bar first", - "mentions.sync": "Sync from Threads", - "mentions.syncing": "Syncing…", - "mentions.syncDone": "Synced {n} mentions", - "mentions.syncFail": "Sync failed — reconnect Threads with threads_manage_mentions scope", - "mentions.openThread": "Open post", - "mentions.status.pending": "Pending", - "mentions.status.replied": "Replied", - "mentions.status.skipped": "Skipped", - "mentions.reply": "Reply", - "mentions.skip": "Skip", - "mentions.draftLabel": "Reply draft", - "mentions.repliedPrefix": "Replied: {text}", - "mentions.needPersona": "Pick a ready persona before AI draft", - "mentions.fail": "Failed", - "mentions.marked": "Mention marked as replied", - "mentions.markReplied": "Mark as replied", - "mentions.markingReplied": "Marking…", - "mentions.withImages": " · {n} images", - - "compose.hint": "Publish only: write body and send to Outbox (not a play). For multi-account threads use", - "compose.hintEnd": ".", - "compose.playsLink": "Plays", - "compose.personaOff": "Persona not ready: AI tools (mimic / analyze) disabled.", - "compose.title": "Title (optional)", - "compose.titlePh": "Helps identify in Outbox", - "compose.body": "Body", - "compose.bodyPh": "Write your post…", - "compose.bodyCount": "{n} characters", - "compose.bodyLongWarning": "The full draft is preserved, but it may exceed the Threads single-post limit", - "compose.topicTag": "Topic tag (Threads)", - "compose.topicTagPh": "e.g. petshow (optional #)", - "compose.topicTagHint": "One topic tag per post, 1–50 chars, no . or &. You can also put #tag in the body.", - "compose.whoCanReplyHint": "Applied when the post is published. Threads cannot change this on an existing post via API.", - "compose.tool.mimic": "Mimic", - "compose.tool.viral": "Viral analysis", - "compose.tool.research": "Research", - "compose.tool.image": "Image", - "compose.mimic.title": "Mimic another post", - "compose.mimic.source": "Source text", - "compose.mimic.sourcePh": "Paste the post to mimic…", - "compose.mimic.direction": "New topic or angle (optional)", - "compose.mimic.directionPh": "e.g. Fewer features can make a product easier to use…", - "compose.mimic.directionHint": "This drives the new post. Leave it blank and AI will choose a related but distinctly different angle.", - "compose.mimic.structureNotes": "Structure notes (used in mimic)", - "compose.mimic.structureNotesPh": "From Own posts → Analyze, or paste hooks/structure…", - "compose.mimic.structureNotesHint": "Only the narrative skeleton, turns, and emotional arc are reused; content follows the new direction and selected persona.", - "compose.mimic.broughtAnalysis": "Loaded source + structure analysis — mimic or edit notes", - "compose.mimic.broughtSource": "Loaded source (no structure yet — analyze on Own posts first)", - "compose.mimic.running": "Mimicking in background (you can leave)…", - "compose.mimic.run": "Mimic into body", - "compose.mimic.queued": "Mimic job queued — will fill the body when done", - "compose.mimic.done": "Mimic done — edit as needed", - "compose.mimic.doneWithStructure": "Mimic done (structure applied) — edit as needed", - "compose.mimic.jobFail": "Mimic job failed: {err}", - "compose.viral.title": "Viral analysis", - "compose.viral.hint": "Hooks, structure, and copyable patterns from source or body.", - "compose.viral.source": "Target (empty = use body)", - "compose.viral.running": "Analyzing…", - "compose.viral.run": "Analyze", - "compose.viral.result": "Analysis", - "compose.viral.done": "Viral analysis done", - "compose.viral.needText": "Paste a reference or write the body first", - "compose.research.title": "Research notes", - "compose.research.q": "Keywords", - "compose.research.qPh": "e.g. fragrance-free detergent sensitive skin", - "compose.research.running": "Searching…", - "compose.research.insert": "Insert selected into body", - "compose.research.inserted": "Inserted {n} notes", - "compose.image.title": "Generate image", - "compose.image.prompt": "Scene description", - "compose.image.promptPh": "Empty = summarize from body", - "compose.image.running": "Generating…", - "compose.image.run": "Generate image", - "compose.image.done": "Image added", - "compose.scheduleAt": "Publish at", - "compose.scheduleHint": "Outbox sends at this time; must not be in the past.", - "compose.scheduleHintNow": "Send immediately (timestamp taken at submit).", - "compose.schedulePast": "Scheduled time is in the past. Set to now or a future time.", - "compose.scheduleNow": "Now", - "compose.publish": "Send to Outbox", - "compose.publishing": "Sending…", - "compose.uploadingImages": "Uploading image {n}/{total}…", - "compose.uploadImageFail": "Image upload failed — retry or use a smaller file (≤5MB)", - "compose.waitImageUpload": "Images still uploading — wait a moment.", - "compose.waitImageUploadBtn": "Uploading images…", - "compose.imageUploadNeedRetry": "Some images failed — tap retry on the thumbnail.", - "compose.publishFail": "Send failed", - "compose.fail": "Failed", - "compose.attachN": "{n} images", - "compose.personaStatus": "Persona: {status}", - "compose.ready": "ready", - "compose.notReady": "not ready", - - "posts.sync": "Resync Threads", - "posts.syncing": "Syncing…", - "posts.syncedAt": "Synced {time}", - "posts.notSynced": "Not synced", - "posts.syncDone": "Synced {n} posts from Threads (metrics + replies)", - "posts.syncFail": "Sync failed — reconnect Threads if permissions are missing", - "posts.loadingReplies": "Loading replies…", - "posts.loadRepliesFail": "Could not load replies", - "posts.empty": "No posts yet", - "posts.openThreads": "Open Threads", - "posts.whoCanReply": "Who can reply", - "posts.replyControl.everyone": "Everyone", - "posts.replyControl.accounts_you_follow": "Profiles you follow", - "posts.replyControl.mentioned_only": "Mentioned only", - "posts.replyControl.parent_post_author_only": "Parent post author only", - "posts.replyControl.followers_only": "Followers only", - "posts.replyControlUpdated": "Updated to “{label}”. Open Threads to verify.", - "posts.replyControlFail": "Could not update who can reply", - "posts.replyControlPublishOnly": "Threads can only set who can reply when publishing. Published posts cannot be changed via API — create a new post in Compose.", - "posts.hideReply": "Hide reply", - "posts.unhideReply": "Unhide reply", - "posts.hidingReply": "Working…", - "posts.replyHidden": "Reply hidden on Threads. Open Threads to verify.", - "posts.replyUnhidden": "Reply unhidden on Threads. Open Threads to verify.", - "posts.hideFail": "Hide / unhide failed", - "posts.hiddenBadge": "Hidden", - "posts.insight": "Insight: {text}", - "posts.review": "Review: {text}", - "posts.formulaResult": "Structure analysis", - "posts.analyzedBadge": "Analyzed", - "posts.noText": "(No text / media-only)", - "posts.collapseReplies": "Collapse replies", - "posts.repliesBtn": "Replies ({total}) · pending {pending}", - "posts.replyRoot": "Reply to post", - "posts.analyzing": "Analyzing…", - "posts.reanalyze": "Re-analyze structure", - "posts.analyze": "Analyze structure", - "posts.mimicThis": "Mimic this", - "posts.rootDraft": "Root reply draft", - "posts.filter.pending": "Pending ({n})", - "posts.filter.replied": "Replied ({n})", - "posts.filter.all": "All ({n})", - "posts.noPending": "No pending replies", - "posts.noReplied": "No replied items yet", - "posts.noReplies": "No replies yet", - "posts.status.pending": "Pending", - "posts.status.replied": "Replied", - "posts.likesN": "{n} likes", - "posts.childCount": "{n} child replies", - "posts.mine": "Ours", - "posts.replyThis": "Reply", - "posts.replyAgain": "Reply again", - "posts.replyTo": "Reply to @{user}", - "posts.replyAgainTo": "Reply again to @{user}", - "posts.needPersona": "Pick a ready persona before AI draft", - "posts.genFail": "Generate failed", - "posts.needText": "Generate or type a reply first", - "posts.needAccount": "Pick a Threads account to send", - "posts.sending": "Publishing to Threads…", - "posts.sent": "Sent to Threads as @{user}", - "posts.sentImages": " ({n} images)", - "posts.accountFallback": "account", - "posts.sendFail": "Send failed", - "posts.analyzeDone": "Structure analysis done (manual)", - "posts.analyzeFail": "Analyze failed", - - "wizard.newTitle": "New play", - "wizard.editTitle": "Edit play", - "wizard.prev": "Back", - "wizard.next": "Next", - "wizard.err.topic": "Enter a topic", - "wizard.err.lead": "Select a lead account", - "wizard.err.leadUnusable": "Lead account unavailable", - "wizard.submitFail": "Submit failed", - "wizard.unnamedPlay": "Untitled play", - "wizard.step.topic": "Topic", - "wizard.step.crew": "Cast", - "wizard.step.script": "Script", - "wizard.step.preview": "Preview", - "wizard.step.schedule": "Schedule", - "wizard.step.submit": "Submit", - "wizard.stepperAria": "Wizard steps", - "wizard.topic.title": "1. What is this thread about?", - "wizard.topic.name": "Title (optional)", - "wizard.topic.namePh": "e.g. Weekend coffee", - "wizard.topic.topic": "One-line topic", - "wizard.topic.topicPh": "What should this thread discuss?", - "wizard.topic.aiView": "AI persona view", - "wizard.topic.personaNotReady": "Persona not ready", - "wizard.topic.quickFill": "Quick fill", - "wizard.topic.sampleTitle": "Weekend coffee chat", - "wizard.topic.sampleTopic": "Looking for a reliable café this weekend — lots of outlets, stay-all-day friendly.", - "wizard.crew.title": "2. Cast", - "wizard.crew.lead": "Lead", - "wizard.crew.noUsable": "No usable accounts", - "wizard.crew.unusable": "Unavailable", - "wizard.crew.cast": "Supporting", - "wizard.script.title": "3. Lines", - "wizard.script.persona": "Persona", - "wizard.script.personaNotReady": "Persona not ready", - "wizard.script.root": "Root post", - "wizard.script.replyN": "Reply {n}", - "wizard.script.generating": "Generating…", - "wizard.script.ai": "AI draft", - "wizard.script.account": "Account: {name}", - "wizard.script.noLead": "(no lead)", - "wizard.script.speaker": "Speaker", - "wizard.script.leadTag": "(lead)", - "wizard.script.text": "Copy", - "wizard.script.rootPh": "Root post text…", - "wizard.script.replyPh": "Reply text…", - "wizard.script.addReply": "Add reply step", - "wizard.preview.title": "4. Preview the thread", - "wizard.preview.unknown": "Unknown", - "wizard.preview.unknownAccount": "Unknown account", - "wizard.preview.root": "Root", - "wizard.preview.leadTalk": "lead reply", - "wizard.preview.empty": "(empty)", - "wizard.schedule.title": "5. When to send", - "wizard.schedule.start": "First post (root) time", - "wizard.schedule.interval": "Reply interval (minutes)", - "wizard.schedule.intervalHint": "Relative to previous step; backend adds random jitter so timing is less robotic", - "wizard.submit.title": "6. Submit schedule", - "wizard.submit.body": "Confirm to send “{title}” ({n} steps) to Outbox and publish in order.", - "wizard.submit.unnamed": "Untitled", - "wizard.submit.root": "Root", - "wizard.submit.replyN": "Reply {n}", - "wizard.submit.submitting": "Submitting…", - "wizard.submit.run": "Submit to Outbox", - - "reply.account": "Reply as", - "reply.persona": "Persona", - "reply.notReady": "This persona is not ready for AI draft (you can still type and send).", - "reply.draft": "Reply draft", - "reply.attach": "Attach", - "reply.generating": "Generating…", - "reply.ai": "AI draft", - "reply.sending": "Sending…", - "reply.send": "Send", - - "image.attach": "Attach", - "image.attachFail": "Attach failed", - "image.attachedAria": "Attached images", - "image.alt": "Attachment", - "image.named": "Image {n}", - "image.remove": "Remove image", - "image.full": "Full ({max})", - "image.more": "Add more ({n}/{max})", - "image.uploading": "Uploading", - "image.uploadingN": "Uploading {n} image(s)…", - "image.uploadFail": "Upload failed", - "image.uploadBadUrl": "Invalid upload response", - "image.retry": "Retry", - - "metrics.aria": "Post metrics", - "metrics.like": "Likes", - "metrics.reply": "Replies", - "metrics.repost": "Reposts", - "metrics.quote": "Quotes", - "metrics.view": "Views", - "metrics.share": "Shares", - "metrics.type.quote": "Quote", - "metrics.type.reply": "Reply", - "metrics.type.image": "Image", - "metrics.type.video": "Video", - "metrics.type.carousel": "Carousel", - "metrics.type.repost": "Repost", - "metrics.type.text": "Text", - "metrics.type.post": "Post", - - "jobs.title": "Jobs", - "jobs.desc": "Jobs in three tabs: active, scheduled, and history. Paginated so we never load everything at once.", - "jobs.startDemo": "Create test job", - "jobs.demoHint": "Requires worker; list auto-refreshes while jobs are active.", - "jobs.demoLabel": "Demo test job", - "jobs.demoCreated": "Test job {id}… created — waiting for worker", - "jobs.demoFail": "Could not create test job", - "jobs.template.tokenRenew": "Threads token renew (~every 30 days)", - "jobs.template.tokenRenewCadence": "Runs about every 30 days · no manual action needed", - "jobs.template.tokenRenewBadge": "Recurring · every 30 days", - "jobs.template.personaAnalyzeAccount": "Persona analyze · public posts", - "jobs.template.personaAnalyzeText": "Persona analyze · from text", - "jobs.template.composeMimic": "Mimic post", - "jobs.template.playGenerateScript": "Play full-script AI", - "jobs.template.radarSweep": "Demand patrol", - "jobs.template.unknown": "Other job", - "jobs.stripMore": "+{n} more running…", - "jobs.nextRun": "Next run: {time}", - "jobs.status.pending": "Pending", - "jobs.status.queued": "Scheduled", - "jobs.status.running": "Running", - "jobs.status.succeeded": "Succeeded", - "jobs.status.failed": "Failed", - "jobs.status.cancelled": "Cancelled", - "jobs.status.cancel_requested": "Cancel requested", - "jobs.loadFail": "Could not load jobs", - "jobs.empty": "No jobs yet", - "jobs.empty.active": "No running or ready-to-claim jobs", - "jobs.empty.recurring": "No scheduled / recurring jobs yet", - "jobs.empty.history": "No history yet (succeeded / failed / cancelled)", - "jobs.tabsAria": "Job categories", - "jobs.tab.active": "Active", - "jobs.tab.recurring": "Scheduled", - "jobs.tab.history": "History", - "jobs.recurringHint": "Future schedules (e.g. token renew in ~30 days). When due, they appear under Active.", - "jobs.total": "{n} total", - "jobs.showing": " · showing first {n}", - "jobs.detail": "Details", - "jobs.loadMore": "Load more ({n} more)", - "jobs.notFound": "Job not found", - "jobs.detailLoadFail": "Could not load this job", - "jobs.progress": "Progress {n}%", - "jobs.updated": "Updated {time}", - "jobs.backList": "Back to list", - "jobs.backCompose": "Back to compose", - "jobs.mimicApplyCompose": "Apply to compose", - "jobs.mimicApplyHint": "Mimic finished. Use the button below to return to single-post compose with the draft filled in.", - "jobs.mimicNoResult": "No mimic result found — run mimic again", - "jobs.mimicApplyFail": "Could not apply result", - "jobs.delete": "Delete", - "jobs.deleteConfirm": "Delete “{name}”? This cannot be undone.", - "jobs.deleted": "Job deleted", - "jobs.deleteFail": "Could not delete job", - "jobs.deleteRunningHint": "Running jobs cannot be deleted. Wait until they finish.", - "jobs.retentionHint": "Terminal jobs are auto-removed after about 2 days", - - "plans.title": "Change plan", - "plans.current": "Current plan", - "plans.perMonth": "/mo", - "plans.monthlyCredits": "{n} credits / month", - "plans.usageLink": "Usage", - "plans.inUse": "Active", - "plans.recommended": "Recommended", - "plans.creditsPerMonth": "{n} credits / month", - "plans.manage": "Manage subscription", - "plans.loadFail": "Could not load your subscription", - - "plan.cta.current": "Current plan", - "plan.cta.upgrade": "Upgrade", - "plan.cta.downgrade": "Downgrade", - "plan.cta.switch": "Switch", - - "plan.free.headline": "Enough to try the full product", - "plan.free.bullet1": "{n} credits / month (~2–3 weeks light use)", - "plan.free.bullet2": "Full product: Studio, Patrol, Outbox, images", - "plan.free.bullet3": "Feel the value, then upgrade to Starter", - "plan.free.bullet4": "When platform is busy, add your own key", - "plan.free.right1": "Full access to accounts, Studio, Patrol, Outbox, jobs, and inspiration.", - "plan.free.right2": "Enough credits for real copy, search, and a few images—not a hollow demo.", - "plan.free.right3": "After the cap, upgrade to Starter—or set BYOK so you don't use platform credits.", - "plan.free.quota1": "{n} credits allocated each month.", - "plan.free.note1": "Free requires no payment; paid subscribers manage cancellation in the billing portal.", - "plan.free.note2": "After cancellation, the plan changes on the date shown in the billing portal.", - - "plan.starter.headline": "Small teams posting and patrolling daily", - "plan.starter.bullet1": "{n} credits / month (about 5× Free)", - "plan.starter.bullet2": "Steady posting, replies, and patrol", - "plan.starter.bullet3": "Fits a 1–3 person cadence · main paid tier", - "plan.starter.bullet4": "Takes effect right after payment", - "plan.starter.right1": "After payment this account becomes Starter; the month uses the new quota.", - "plan.starter.right2": "Credits support regular posts, reply drafts, and scheduled patrol.", - "plan.starter.right3": "Same features as Free; you buy more headroom. Best starting paid plan.", - "plan.starter.quota1": "{n} credits / month · {price}.", - "plan.starter.note1": "Plan changes only after successful payment.", - "plan.starter.note2": "Resets on calendar month; unused credits do not roll over.", - - "plan.pro.headline": "Multi-account, heavy AI and research", - "plan.pro.bullet1": "{n} credits / month (about 3× Starter)", - "plan.pro.bullet2": "High-volume copy, research, and images · heavy ceiling", - "plan.pro.bullet3": "Built for agencies and multi-brand work", - "plan.pro.bullet4": "Takes effect right after payment", - "plan.pro.right1": "After payment this account becomes Pro; the month uses Pro quota.", - "plan.pro.right2": "Fits multi-account replies and deep research with less risk of running out mid-month.", - "plan.pro.right3": "Same features; you buy capacity. Beyond this, use BYOK—still works when platform is limited.", - "plan.pro.quota1": "{n} credits / month · {price}.", - "plan.pro.note1": "Failed payment does not change your plan.", - "plan.pro.note2": "After payment you can find receipts in billing history.", - - "plan.quota2": "Per-feature credits: copy {copy} (~{copyCalls} uses), research {research} (~{researchCalls}), search {search} (~{searchCalls}), image {image} (~{imageCalls} images).", - "plan.softCapsLine": "Credits: copy {copy} · research {research} · search {search} · image {image}", - "plan.approxCallsLine": "About {copyCalls} copy · {researchCalls} research · {searchCalls} search · {imageCalls} images", - - "checkout.title": "Confirm plan", - "checkout.pickFirst": "Please choose a plan first.", - "checkout.viewPlans": "View plans", - "checkout.fail": "Could not complete", - "checkout.confirmFree": "Confirm switch to Free", - "checkout.payAndAction": "{action} and pay {price}", - "checkout.subscribe": "Subscription", - "checkout.perMonth": "/mo", - "checkout.monthlyCredits": "{n} credits per month", - "checkout.youGet": "What you get", - "checkout.quota": "Quota", - "checkout.notes": "Notes", - "checkout.amountDue": "Amount due", - "checkout.billedMonthly": "{name} · billed monthly", - "checkout.already": "Already on this plan", - "checkout.processing": "Processing…", - "checkout.currentPlan": "Current plan", - "checkout.pickOther": "Choose another plan", - "checkout.cancel": "Cancel", - "checkout.invalidUrl": "The billing service returned an unsafe URL. No redirect was made.", - "checkout.redirecting": "Taking you to Stripe's secure checkout…", - "checkout.redirectingPortal": "Taking you to the Stripe subscription portal…", - "checkout.redirectFailed": "Stripe could not be opened. Check your browser or network settings and try again.", - "checkout.networkError": "Could not connect to the billing service. Check your network and try again.", - "checkout.unavailable": "Billing is not enabled or is temporarily unavailable. Please try again later.", - "checkout.sessionExpired": "Your session has expired. Sign in again and retry.", - "checkout.portalUnavailable": "There is no Stripe subscription to manage yet. Choose a paid plan first.", - "checkout.verifying": "Confirming payment and plan activation…", - "checkout.pollFail": "Could not check the payment status. Please retry.", - "checkout.missingId": "The checkout ID is missing, so the payment result cannot be verified.", - "checkout.terminalFail": "Checkout did not complete ({status}). Choose a plan and try again.", - "checkout.timeout": "Payment may still be processing, but the plan was not activated within 30 seconds. Retry the status check; do not pay again.", - "checkout.retry": "Retry status check", - "checkout.canceledTitle": "Checkout canceled", - "checkout.canceledBody": "Your plan was not changed and no payment action was performed.", - "checkout.manageInstead": "Manage this change in the billing portal to avoid a duplicate subscription.", - - "usage.widget.titleUsed": "{name} · used {used}/{cap} credits", - "usage.widget.titleUnlimited": "{name} · unlimited", - "usage.widget.ariaUsed": "Used {used} of {cap} credits", - "usage.widget.dialog": "Plan and usage", - "usage.widget.currentPlan": "Current plan", - "usage.widget.unlimited": "Unlimited", - "usage.widget.perMonth": "/mo", - "usage.widget.monthUsage": "This month", - "usage.widget.remaining": "{n} credits left", - "usage.widget.leftShort": "{n} left", - "usage.widget.usedOfCap": "{used}/{cap}", - "usage.widget.overShort": "Over", - "usage.widget.upgradeShort": "Upgrade", - "usage.widget.upgrade": "Upgrade", - "usage.widget.includes": "This plan includes", - "usage.widget.nudge": "You're running low this month. Upgrade for more credits right away.", - "usage.widget.changePlan": "Change plan", - "usage.widget.usageDetail": "Usage details", - - "usage.meter.ai_copy": "AI copy", - "usage.meter.ai_research": "AI research", - "usage.meter.web_search": "Search", - "usage.meter.ai_image": "AI image", - "usage.meter.barAria": "{label} {credits}/{cap} credits ({count} runs)", - "usage.ledger.costAria": "Used {n} credits", - "usage.event.keyMode.platform": "Platform credits", - "usage.event.keyMode.byok": "Your own key", - "usage.event.cost.credits": "−{n}", - "usage.event.cost.byok": "BYOK", - "usage.event.cost.byokAria": "Own key — no platform credits charged", - "usage.event.label.unknown": "Usage", - "usage.event.label.genericAi": "AI call", - "usage.event.label.personaAnalyzeText": "Persona analyze · text", - "usage.event.label.personaAnalyzeAccount": "Persona analyze · account", - "usage.event.label.composeMimic": "Mimic post", - "usage.event.label.composeViral": "Viral analysis", - "usage.event.label.personaPreview": "Persona preview", - "usage.event.label.ownPostReply": "Own post · reply draft", - "usage.event.label.mentionReply": "Mention · reply draft", - "usage.event.label.inspireChat": "Inspire chat", - "usage.event.label.researchSearch": "Research search", - "usage.event.label.generateImage": "Generate image", - "usage.event.label.search": "Web search", - "usage.event.label.aiComplete": "AI complete", - "usage.event.source.personaAnalyzeText": "Persona analyze · text", - "usage.event.source.personaAnalyzeAccount": "Persona analyze · account", - "usage.event.source.composeMimic": "Mimic post", - "usage.event.source.composeViral": "Viral analysis", - "usage.event.source.personaPreview": "Persona preview", - "usage.event.source.ownPostAnalyze": "Own post · structure analysis", - "usage.event.source.ownPostReply": "Own post · reply draft", - "usage.event.source.mentionReply": "Mention · reply draft", - "usage.event.source.inspireChat": "Inspire chat", - "usage.event.source.researchSearch": "Research search", - "usage.event.source.generateImage": "Generate image", - "usage.event.source.proxySearch": "Web search", - "usage.event.source.proxyAi": "AI complete", - - "usage.chart.period": "Period", - "usage.chart.allocated": "Allocated", - "usage.chart.consumed": "Used", - "usage.chart.pctTitle": "Usage as share of allocation", - "usage.chart.aria": "Allocation and usage", - "usage.chart.colAria": "{label}: allocated {purchased}, used {consumed}", - - "settings.provider": "Provider", - "settings.model": "Model", - "settings.aiUnifiedHint": "Copy, research, and expand all share one provider and model.", - "settings.fetchModels": "Fetch models", - "settings.fetchingModels": "Loading…", - "settings.apiKey": "API key", - "settings.configured": "Configured", - "settings.notConfigured": "Not set", - "settings.platformKeyOk": "Platform key available (optional override)", - "settings.modelsHint": "Models", - "settings.modelsCached": "Models from cache (~5 min)", - "settings.modelsLoaded": "Loaded models for {provider}", - "settings.aiSaved": "AI settings saved", - "settings.searchSaved": "Search settings saved", - "settings.clearAiKey": "Clear personal AI key", - "settings.aiKeyCleared": "Personal AI key cleared", - "settings.clearExaKey": "Clear Exa key", - "settings.exaKeyCleared": "Exa key cleared", - "settings.searchProvider": "Search provider", - "settings.expand": "Expand strategy", - "settings.exaKey": "Exa API key", - "settings.devMode": "Test patrol (local session)", - "settings.devModeHint": "When on, test patrol can use a synced Chrome sign-in. Live publish/replies still use the official API.", - "settings.ext.title": "Chrome extension", - "settings.ext.desc": "Install this extension (v1.2.0+) to sync a Threads sign-in from Chrome for test patrol.", - "settings.ext.step1": "Download and unzip to get the haixun-threads-sync folder", - "settings.ext.step2": "Open chrome://extensions and enable Developer mode", - "settings.ext.step3": "Load unpacked (or click Reload if already installed)", - "settings.ext.step4": "Set this site’s URL in extension options (must match address bar), then refresh Lapras", - "settings.ext.download": "Download extension (ZIP)", - "settings.ext.sessionTitle": "Chrome session (test patrol)", - "settings.ext.sessionHint": "Sync sign-in from a logged-in Threads tab for test patrol. Live publish still uses the official API.", - "settings.ext.pageOrigin": "This tab: {origin}", - "settings.ext.detected": "Extension detected", - "settings.ext.notReady": "Extension not detected", - "settings.ext.synced": "Session synced", - "settings.ext.notSynced": "Session not synced", - "settings.ext.syncBtn": "Sync session from Chrome", - "settings.ext.recheck": "Recheck", - "settings.ext.syncOk": "Chrome session synced for test patrol", - "settings.ext.syncFail": "Chrome session sync failed", - "settings.ext.needLogin": "Please sign in to Lapras before syncing.", - "settings.ext.notDetected": "Chrome extension not found on {origin}. Use v1.2.1+: reload in chrome://extensions → set Options Server URL to the same origin and Save → F5 this tab.", - "settings.ext.reloadHint": "After install/update, reload the extension and press F5 on this page.", - "settings.ext.detectSteps": "Installed but not detected? ① chrome://extensions → enable and Reload (v1.2.1+) ② Extension Options → set Server URL to {origin} and Save (Allow) ③ Hard-refresh this tab (F5). Do not mix localhost and 127.0.0.1.", - - "forgot.fail": "Request failed", - "forgot.mockHint": "In production this goes to email; here is a direct link:", - "forgot.checkInbox": "Please check your inbox (and spam).", - - "reset.mismatch": "Passwords do not match", - "reset.fail": "Reset failed", - "reset.cardTitle": "Reset password", - "reset.redirecting": "Redirecting to sign in…", - "reset.loginNow": "Sign in now", - "reset.passwordPh": "Password must be at least 12 characters with upper, lower, digit, and symbol", - "reset.forgotLink": "Request a new reset", - - "verify.sendFail": "Could not send", - "verify.fail": "Verification failed", - "verify.success": "Email verified. You can use Lapras now.", - "verify.codePh": "6-digit code", - "verify.currentAccount": "Account: {email}", - - "login.brandTitle": "巡樓 · Lapras", - - "home.navLabel": "Public navigation", - "public.localeLabel": "Language", - "home.heroTitle": "Find clients on Threads. Follow through to a win.", - "home.heroLead": "Daily demand lists, replies, and follow-ups on one desk.", - "home.outcomesTitle": "What you get", - "home.outcome.find.title": "People looking for you", - "home.outcome.find.body": "Keyword watches refresh daily; run a manual patrol when you need it.", - "home.outcome.reply.title": "Replies that fit", - "home.outcome.reply.body": "Drafts follow your services and tone; forbidden words stay out.", - "home.outcome.close.title": "Follow-ups that close", - "home.outcome.close.body": "One contact per person, with stages and reminders.", - "home.productTitle": "What you’ll use", - "home.productLead": "Demand, patrol, replies, CRM, and send — one desk.", - "home.preview.radar.title": "Today’s demand", - "home.preview.radar.caption": "Keyword watches refresh a daily list of needs", - "home.preview.scout.title": "Patrol", - "home.preview.scout.caption": "Run a manual sweep when you want hits now", - "home.preview.studio.title": "Replies & Studio", - "home.preview.studio.caption": "Drafts match your services and tone", - "home.preview.crm.title": "Pipeline", - "home.preview.crm.caption": "One contact card per person — stages and follow-ups", - "home.preview.outbox.title": "Outbox", - "home.preview.outbox.caption": "Scheduled, ready, and sent in one queue", - "home.preview.mock.radar.badge": "Today · high match", - "home.preview.mock.radar.meta": "3 new needs", - "home.preview.mock.radar.row1": "Taipei · moving quote", - "home.preview.mock.radar.row2": "Looking for home cleaning", - "home.preview.mock.radar.row3": "Freelance design · budget ready", - "home.preview.mock.scout.badge": "Keyword scan", - "home.preview.mock.scout.hit": "Anyone recommend an accountant?", - "home.preview.mock.scout.snippet": "Need someone for small-company tax…", - "home.preview.mock.scout.hit2": "Has anyone tried that SaaS?", - "home.preview.mock.studio.tab1": "Inspire", - "home.preview.mock.studio.tab2": "Reply", - "home.preview.mock.studio.tab3": "Schedule", - "home.preview.mock.studio.draft1": "Hi — saw you’re looking for movers.", - "home.preview.mock.studio.draft2": "We cover metro Taipei; happy to estimate first.", - "home.preview.mock.studio.draft3": "(From your service profile & forbidden words)", - "home.preview.mock.crm.col1": "New", - "home.preview.mock.crm.col2": "Talking", - "home.preview.mock.crm.col3": "Won", - "home.preview.mock.crm.foot": "Due follow-ups & reminders", - "home.preview.mock.outbox.r1": "Reply · tonight 20:00", - "home.preview.mock.outbox.r2": "Post · needs review", - "home.preview.mock.outbox.r3": "Sent · syncing metrics", - "home.pricingTitle": "Plans", - "home.pricingLead": "Same features; monthly credits differ.", - "home.pricingHoverHint": "Hover the credit line to see what each plan covers.", - "home.ctaLogin": "Sign in", - "home.privacyLink": "Privacy", - "home.termsLink": "Terms", - "home.dataDeletionLink": "Data deletion", - "legal.footerNav": "Legal links", - - "privacy.navLabel": "Privacy policy navigation", - "privacy.title": "Privacy Policy", - "privacy.updated": "Last updated: 2026-07-31", - "privacy.intro": - "This Privacy Policy explains how Lapras (巡樓, “the Service”, “we”) collects, uses, stores, shares, and deletes your personal data and Meta/Threads platform data. It covers the Lapras web console, public tools, and processing related to Threads accounts you connect via OAuth. Using the Service means you acknowledge this policy.", - "privacy.section.overview.title": "1. Controller and scope", - "privacy.section.overview.body": - "The Lapras operator is the controller of personal data described here.\nThis policy covers: member accounts, workspace content, usage and payment identifiers, and platform data we obtain from Meta/Threads APIs after you authorize us.\nIf you join someone else’s workspace, that owner may have extra rules; they do not replace this notice for platform- and member-level data.\nThis is our own policy, not Meta’s, Threads’, or Instagram’s. See Meta’s Privacy Center for how Meta processes data in its products.", - "privacy.section.collect.title": "2. What information we collect", - "privacy.section.collect.body": - "(A) Data you provide\n• Account: email, display name, password hash (never plaintext passwords), role, email verification status.\n• Content you enter: personas, brands, patrol intents/keywords, drafts, reply copy, Outbox items, workspace settings, and uploaded media if any.\n• Support/privacy requests: emails and request details you send us.\n\n(B) Data collected automatically\n• Login sessions, IP, basic browser/device metadata (security, debugging, abuse prevention).\n• UI preferences (language, theme).\n• Feature usage and job logs (quotas, diagnostics).\n\n(C) Meta/Threads platform data (only after you connect)\n• Threads user identifiers (e.g. user id, username) and profile summary within granted scopes.\n• Posts, replies, media metadata, and API responses needed to read/send/insights within granted permissions.\n• Access tokens (and related refresh credentials) used only to call Threads APIs on your behalf; stored separately from member JWTs.\n\n(D) Billing (if enabled)\n• Plan tier, quota usage, payment transaction identifiers (we do not store full card numbers).\n\nWe do not require unrelated government ID for ordinary use.", - "privacy.section.use.title": "3. How we use data and why", - "privacy.section.use.body": - "We process data to:\n• Provide accounts, workspaces, permissions, and sign-in security.\n• Run core features: patrol/outreach, create and AI assist, Outbox schedule/send, account and post sync, insights and metering.\n• Call Meta/Threads APIs with your tokens (read authorized content, publish content you confirm, etc., subject to granted scopes).\n• Send verification codes, password resets, and service notices.\n• Prevent abuse, keep the Service reliable, debug and improve (de-identified where practical).\n• Comply with law or valid legal requests.\n\nWe do not sell Meta/Threads platform data to data brokers, and we do not use that data to build independent marketing profiles unrelated to providing the Service.", - "privacy.section.threads.title": "4. Threads / Meta platform data", - "privacy.section.threads.body": - "Connection: you authorize Lapras via Meta OAuth for Threads. We request and use only permissions needed for product features (e.g. basic profile, content publish, and other scopes shown in the authorization UI).\n\nUse limits: Platform Data from Meta is used only to provide, maintain, and improve Lapras features you use (connect accounts, sync posts, schedule/send replies or posts, show status, debug). We do not sell Platform Data or use it for advertising targeting unrelated to your authorization.\n\nStorage: Threads credentials are kept separate from member sign-in credentials; access is permission-controlled by workspace/account.\n\nYou may at any time:\n• Disconnect Threads inside Lapras; and/or\n• Revoke the app in Meta/Threads/Instagram settings.\nAfter disconnect we stop new API access and handle deletion/anonymization under Section 8.\n\nMeta’s own processing is governed by Meta’s policies; we do not control Meta’s systems.", - "privacy.section.share.title": "5. Sharing and processors", - "privacy.section.share.body": - "We may share data with or have it processed by:\n• Infrastructure: hosting, databases, object storage, email delivery, monitoring/logs (to run the Service).\n• AI/search providers: when you use inspire, analysis, or suggestion features (see Section 6).\n• Payment processors: when billing is enabled.\n• Legal: when required by law or valid authority.\n• Business transfers: merger/acquisition under applicable law and notice.\n\nProcessors may only process under our instructions with reasonable security. We do not sell personal data.", - "privacy.section.ai.title": "6. AI and automated processing", - "privacy.section.ai.body": - "Some features send prompts, post samples, structured notes, or summaries of authorized public content to AI/search providers for suggestions, style analysis, or retrieval.\nYou may use platform defaults or BYOK keys; with BYOK, requests go to your provider—read their terms too.\nWe do not use your content as public marketing or advertised training material without separate consent.", - "privacy.section.retention.title": "7. Retention", - "privacy.section.retention.body": - "• Account and workspace data: while the account is active.\n• Threads tokens: while connected; invalidated/removed after disconnect or deletion requests.\n• Job logs, usage, security records: for a reasonable period for operations, disputes, and law, then delete or anonymize.\n• Backups: may retain residual copies until rotation completes.\nSee Section 8 and the Data deletion page for how to request deletion.", - "privacy.section.deletion.title": "8. How to request deletion of your data", - "privacy.section.deletion.body": - "You can request deletion of personal data and platform data we hold about you as follows:\n\nOption 1 (recommended): Sign in to Lapras → disconnect Threads in account/settings, then use any in-product account/data deletion flow if available.\n\nOption 2: Email the Service operator from your registered email address. Subject: “Data Deletion Request”. Include:\n• Registered email\n• Threads username or in-app account id if known\n• Scope (entire account / Threads connection data only / specific workspace)\n\nOption 3: If you remove our app in Meta and request deletion, follow Meta’s flow. We also process platform deletion callbacks when provided; otherwise still use Option 2.\n\nFull steps are on the Data deletion page (/data-deletion).\nAfter identity verification we delete or anonymize within a reasonable period (typically 30 days, or sooner/later if law requires), except records we must retain by law.", - "privacy.section.rights.title": "9. Your rights", - "privacy.section.rights.body": - "Where applicable law allows, you may request access, correction, portability, deletion, restriction, or objection to certain processing. You may sign out, edit profile data, disconnect Threads, or stop using the Service. Contact us per Section 12; we may need to verify identity.", - "privacy.section.security.title": "10. Security", - "privacy.section.security.body": - "We use reasonable technical and organizational measures, including HTTPS, password hashing, permission isolation, and separating member credentials from Threads tokens. No system is perfectly secure; we notify of incidents as required by law or policy.", - "privacy.section.children.title": "11. Children", - "privacy.section.children.body": - "The Service is intended for creators and business operators, not children. We do not knowingly collect personal data from children under the applicable age (e.g. 13, or a higher age where required). Contact us under Section 12 if you believe we collected a child’s data; we will delete it promptly.", - "privacy.section.contact.title": "12. Contact us", - "privacy.section.contact.body": - "For privacy questions or access/correction/deletion requests, contact the Service operator using your registered email. Subject: “Privacy / Data Request”. Include your account email and details so we can verify identity.\nYou may also use in-product profile/settings after sign-in.", - "privacy.section.updates.title": "13. Updates", - "privacy.section.updates.body": - "We may update this policy for features, Meta platform rules, or law. We change the “Last updated” date; for material changes we may also notify in-product or by email. Continued use after an update means you acknowledge the revised policy to the extent permitted by law.", - "privacy.backHome": "Back to intro", - - "terms.navLabel": "Terms of service navigation", - "terms.title": "Terms of Service", - "terms.updated": "Last updated: 2026-07-31", - "terms.intro": - "Welcome to Lapras (巡樓). These Terms of Service govern your use of the web console, public tools, and related features. By using the Service you agree to these Terms. If you do not agree, do not use the Service.", - "terms.section.acceptance.title": "1. Acceptance", - "terms.section.acceptance.body": - "You must have legal capacity to contract and comply with applicable law. If you use the Service for an organization, you represent you have authority to bind that organization.", - "terms.section.service.title": "2. The Service", - "terms.section.service.body": - "Lapras provides Threads operations tooling, including account linking, patrol/outreach, create and send, personas/brands, usage and plans. Features may change. We may suspend or modify features for maintenance, security, or legal reasons.", - "terms.section.accounts.title": "3. Accounts and security", - "terms.section.accounts.body": - "Provide accurate information, protect credentials, and you are responsible for activity under your account. Notify us of unauthorized use. We may limit or suspend accounts for security, abuse, or violations.", - "terms.section.threads.title": "4. Threads / Meta connection", - "terms.section.threads.body": - "Some features require Meta OAuth to connect Threads. You must have the right to connect that account and must comply with Meta, Threads, and Instagram terms and community standards.\nYou are responsible for lawful, non-infringing content and for following platform rules. Platform enforcement, API denials, or third-party claims arising from your content or account use are your responsibility. Lapras is a tool and does not guarantee platform approval or performance outcomes.\nYou may disconnect or revoke Meta authorization at any time; some features will stop working after disconnect.", - "terms.section.content.title": "5. Your content", - "terms.section.content.body": - "You retain rights in content you upload or create. You grant us a non-exclusive, worldwide, royalty-free license to store, process, transmit, and display content as needed to provide the Service to you and authorized workspace members, including calling necessary third-party APIs. You warrant content is lawful and non-infringing.", - "terms.section.acceptable.title": "6. Acceptable use", - "terms.section.acceptable.body": - "You may not: abuse APIs or automate harm to others; spam, fraud, hate, or illegal content; bypass quotas or security; reverse engineer or access systems without authorization; use the Service in ways that violate Meta platform policies; or infringe others’ privacy or IP.", - "terms.section.billing.title": "7. Plans and usage", - "terms.section.billing.body": - "Paid plans, credits, and limits are as shown in product. Non-payment, overuse, or abuse may reduce or suspend features. Refunds follow the purchase terms and applicable law.", - "terms.section.disclaimer.title": "8. Disclaimers and liability", - "terms.section.disclaimer.body": - "The Service is provided “as is”. To the fullest extent permitted by law, we do not warrant uninterrupted or error-free operation, or continued availability of third platforms (including Threads/Meta/AI providers). We are not liable for indirect, incidental, or lost-profit damages except where liability cannot be limited. Our aggregate liability is capped at fees you paid for the Service in the twelve months before the claim (or zero if free), except where law forbids the cap.", - "terms.section.termination.title": "9. Termination", - "terms.section.termination.body": - "You may stop using the Service and request deletion under the Privacy Policy. We may suspend or terminate for breach, abuse, legal requirements, or if we discontinue the Service. Data handling after termination follows the Privacy Policy.", - "terms.section.contact.title": "10. Contact and governing law", - "terms.section.contact.body": - "Contact the operator via your registered email for terms questions. Governing law and venue are those of our primary place of operation unless mandatory law says otherwise. Privacy: /privacy. Data deletion: /data-deletion.", - - "deletion.navLabel": "Data deletion navigation", - "deletion.title": "User Data Deletion Instructions", - "deletion.updated": "Last updated: 2026-07-31", - "deletion.intro": - "This page explains how to ask Lapras (巡樓) to delete personal data and Threads platform data associated with you.", - "deletion.section.summary.title": "1. Summary", - "deletion.section.summary.body": - "If you stop using the Service, or remove our app in Meta and want us to delete data we hold, follow the steps below. After identity verification we delete or anonymize deletable data, except where law or legitimate retention requires keeping records.", - "deletion.section.steps.title": "2. How to request deletion", - "deletion.section.steps.body": - "Step 1: If you can still sign in, disconnect all Threads accounts in Lapras (settings/accounts).\nStep 2: Email the Service operator from your registered email address.\nStep 3: Subject line: “Data Deletion Request”.\nStep 4: Include:\n• Registered email (required)\n• Display name or member id if known\n• Threads username or in-app account id if you connected Threads\n• Scope: entire account / Threads connection + synced data only / a specific workspace\nStep 5: We confirm receipt, process the request, and email completion when possible.", - "deletion.section.threads.title": "3. Threads / Meta authorization", - "deletion.section.threads.body": - "Deleting Lapras data does not delete your posts or account on Threads/Instagram/Meta.\nAlso revoke our app in Meta/Threads settings to clear authorization on Meta’s side.\nIf the platform sends us a deletion callback, we process matching data; otherwise use the email process on this page.", - "deletion.section.scope.title": "4. What we delete", - "deletion.section.scope.body": - "Where reasonably possible we delete or anonymize:\n• Member personal data and credentials\n• Workspace content you created (personas, drafts, patrol, Outbox, etc., per request scope)\n• Threads connection identifiers, tokens, and sync caches\n• Non-essential job and analytics records\n\nWe may retain for a limited time or by law:\n• Transaction/billing records required by law\n• Security and anti-abuse logs (limited period)\n• Backup copies until rotation completes", - "deletion.section.timeline.title": "5. Timeline", - "deletion.section.timeline.body": - "We typically complete deletion or anonymization within 30 days after a verifiable request. If more time is needed (complex workspaces or legal review), we will communicate an estimate. Backup purge may finish afterward.", - "deletion.section.contact.title": "6. Contact", - "deletion.section.contact.body": - "Email the Service operator from your registered address with subject “Data Deletion Request”. Full privacy policy: /privacy. Terms: /terms.", - - "common.listSep": ", ", - "common.dash": "—", - - "scout.title": "Topic ideas", - "scout.topic.intro": "Enter a title, person, event, or work direction. Scout infers whether you want trending discussion, recommendations, or public work signals. Use Demand to manage customer opportunities.", - "scout.topic.termHint": "Up to 3 intent-aware queries are preselected. You can uncheck, edit, or add your own wording.", - "scout.topic.termsNeedShort": "{n} term(s) break Threads short-query rules — shorten them before searching.", - "scout.topic.noTerms": "No usable terms — try a clearer topic (e.g. Taipei market, nanny recs).", - "scout.topic.termsReadyPrimary": "Intent understood. Up to 3 high-confidence queries are selected, with {n} more available to adjust.", - "scout.topic.workshopHintSelect": "Queries run separately and are deduplicated. Up to 3 high-confidence queries are preselected for review.", - "scout.topic.primaryTerm": "Primary (recommended)", - "scout.topic.variantTerm": "Variant {n}", - "scout.topic.useTerm": "Include this term in search", - "scout.today": "Today's sortie", - "scout.purposeValue": "Pain-point replies", - "scout.purposeDemand": "Find demand pains", - "scout.purposeProvider": "Find solution providers", - "scout.purposeActivity": "Activity short replies", - "scout.goal": "Daily goal (posts)", - "scout.progress": "Progress {done}/{goal}", - "scout.intent": "What to find / reply to", - "scout.keyword": "Keywords", - "scout.intentPh": "e.g. seasonal scalp itch, truly fragrance-free, outlets on weekends", - "scout.keywordPh": "e.g. backend freelance engineer, Demon Slayer", - "scout.productOptional": "Product (optional)", - "scout.productRequired": "Product to solve (required)", - "scout.selectProduct": "Select a product", - "scout.noProduct": "No product", - "scout.brandFallback": "Brand", - "scout.placement": "Placement: {label}", - "scout.providerProduct": "Product: {label}", - "scout.painPart": " · pain “{pain}”", - "scout.noProductsBefore": "No products yet. Add some under", - "scout.noProductsAfter": ".", - "scout.start": "Start", - "scout.startMore": "Fetch more", - "scout.fetching": "Fetching…", - "scout.planKeywords": "Generate keywords", - "scout.planning": "Planning keywords…", - "scout.workshop": "Search keywords (editable)", - "scout.workshopHint": "Search runs only after you confirm. Each line is its own query — remove weak ones and add phrases that work.", - "scout.workshopEmpty": "Keep at least one keyword to search", - "scout.addTerm": "Add", - "scout.addTermPh": "Add another search phrase", - "scout.removeTerm": "Remove", - "scout.confirmScan": "Search with these", - "scout.startImmediate": "Start patrol now", - "scout.immediateHint": "Start now first prepares suggested phrases, then searches with up to 3 focused queries.", - "scout.openDailySchedule": "View daily schedule", - "scout.scheduleHint": "Daily automatic patrols are configured under Demand watches.", - "scout.replan": "Regenerate", - "scout.clearWorkshop": "Cancel", - "scout.termsReady": "{n} keywords ready — review before searching.", - "scout.runs": "Patrol batches", - "scout.runCount": "Batch ({n})", - "scout.runCreatedAt": "Created {time}", - "scout.runSelectAria": "Switch patrol batch", - "scout.runPending": "{n} pending · ", - "scout.runDone": "Cleared · ", - "scout.runTotal": " ({n} posts)", - "scout.runStatus.queued": "Queued", - "scout.runStatus.running": "Scanning", - "scout.runStatus.succeeded": "Complete", - "scout.runStatus.failed": "Failed", - "scout.runStatus.cancelled": "Cancelled", - "scout.shortfall": "{n} posts short", - "scout.shortfallReason.source_exhausted": "Sources exhausted", - "scout.shortfallReason.duplicate_exhausted": "Too many duplicates", - "scout.shortfallReason.relevance_exhausted": "Not enough relevance", - "scout.shortfallReason.source_unavailable": "Source unavailable", - "scout.shortfallReason.limit_reached": "Limit reached", - "scout.shortfallReason.unknown": "Insufficient source results", - "scout.refreshRuns": "Refresh batches", - "scout.refreshingRuns": "Refreshing…", - "scout.runsRefreshed": "Batches refreshed", - "scout.newRunReady": "A new patrol batch is ready. Your current reading position is unchanged; refresh batches to view it.", - "scout.deleteRun": "Delete this batch", - "scout.deleting": "Deleting…", - "scout.now": "Now this post", - "scout.emptyBatch": "Nothing pending in this batch. Want a daily auto demand list? Open Demand and add keyword watches.", - "scout.draft": "Reply draft", - "scout.draftPhActivity": "Short reply…", - "scout.draftPhValue": "Empathize → suggest…", - "scout.sendAccount": "Send from account", - "scout.noAccount": "No usable accounts", - "scout.personaForRegen": "Persona (for regen)", - "scout.notReady": " (not ready)", - "scout.skip": "Skip", - "scout.regen": "Regen", - "scout.send": "Send", - "scout.openThreadsReply": "Open Threads to reply", - "scout.markManualDone": "Replied, mark complete", - "scout.manualReplyHint": "Review the draft, open the original Threads post, and reply there. The draft will be copied when possible. Return here to mark it complete.", - "scout.openedAndCopied": "Threads opened and the draft was copied. Paste the reply, then return to mark it complete.", - "scout.openedManual": "Threads opened. Return here after replying to mark it complete.", - "scout.noPermalink": "This result has no Threads permalink to open.", - "scout.manualDone": "Manual reply marked complete · today {done}/{goal}", - "scout.manualDoneFail": "Failed to mark the manual reply complete", - "scout.resend": "Resend", - "scout.sending": "Sending…", - "scout.needAccountBefore": "Connect a usable account under", - "scout.needAccountAfter": "first.", - "scout.loadingKnowledge": "Preparing related knowledge…", - "scout.product": "Product", - "scout.queue": "Matches · {n}", - "scout.valueQueue": "Value replies · {n}", - "scout.providerQueue": "Solution providers · {n}", - "scout.demandQueue": "Demand pains · {n}", - "scout.activityQueue": "Activity short replies · {n}", - "scout.noMatchesInQueue": "No matches in this queue", - "scout.collapseQueue": "Collapse queue", - "scout.expandQueue": "Expand queue", - "scout.noOtherPending": "No other pending", - "scout.unnamedRun": "Unnamed batch", - "scout.thisRun": "this batch", - "scout.confirmDeleteRun": "Delete patrol batch “{label}”?\\nHits and related knowledge will be removed. This cannot be undone.", - "scout.deletedRun": "Deleted batch “{label}”", - "scout.deleteRunFail": "Failed to delete batch", - "scout.err.runBusy": "This batch is still scanning or already finished and cannot be deleted yet.", - "scout.err.notFound": "This batch or post no longer exists. Refresh the batches.", - "scout.err.sourceUnavailable": "The patrol source is unavailable right now. Try again later.", - "scout.needKeyword": "Enter keywords first", - "scout.needIntent": "Write what you're looking for", - "scout.productMissing": "Selected product is not in the list. Please reselect.", - "scout.providerSetupRequired": "Solution matching needs pain points and at least one tag or provider capability term. Complete them on the Brands page first.", - "scout.defaultLabel": "Patrol", - "scout.newRunActivity": "New batch “{label}” · {n} pending", - "scout.newRunValue": "New batch “{label}” · {n} posts · handle “Now this post”", - "scout.knowledgeReady": "“{label}” knowledge ready · {n} notes", - "scout.patrolFail": "This patrol failed", - "scout.loadFail": "Scout data could not be loaded. Refresh and try again.", - "scout.scanQueued": "Patrol job “{label}” queued. You’ll be notified when a new batch is ready.", - "scout.workerWaiting": "The patrol job is still waiting for a worker. Check that apps/backend worker is running.", - "scout.scanReady": "Patrol complete. Found {n} pending replies.", - "scout.queued": "Queued in @{who}'s Outbox for delivery · today {done}/{goal}", - "scout.scanJob": "Patrol job", - "scout.scanInProgress": "Scanning", - "scout.crawlerSessionRequired": "Test patrol needs a valid Chrome session. Sync it from a signed-in Threads tab in Settings.", - "scout.openSettings": "Open Settings", - "scout.source": "Source: Threads Keyword Search", - "scout.resultKeyword": "Keyword: {tag}", - "scout.classification": "Class: {classification}", - "scout.postedAt": "Posted: {time}", - "scout.postedUnknown": "Post time unknown", - "scout.scannedAt": "Scanned {time}", - "scout.createdAt": "Created {time}", - "scout.openPermalink": "Open original on Threads", - "scout.draftFail": "Draft failed", - "scout.skipped": "Skipped", - "scout.status.new": "To handle", - "scout.status.drafted": "Drafted", - "scout.status.queued": "Queued", - "scout.status.published": "Sent", - "scout.status.skipped": "Skipped", - "scout.noDraft": "No draft to send", - "scout.accountFallback": "account", - "scout.sent": "Sent (@{who}) · today {done}/{goal}", - "scout.sendFail": "Send failed", - "scout.confirmDeletePost": "Delete this hit?", - "scout.deletedPost": "Deleted this hit", - "scout.stanceActivity": "Short reply · activity", - "scout.stanceDemand": "Demand pain · replyable", - "scout.stanceProvider": "Solution matching · no product promotion", - "scout.demandHint": "These posts show people seeking help or comparing solutions. Read the original post before responding helpfully.", - "scout.providerHint": "These are solution-provider candidates. Review the original post and proof of capability before contacting them.", - "scout.stanceProduct": "Empathy · soft product", - "scout.stanceRelation": "Engage · build rapport", - - "brands.title": "Brands", - "brands.railAria": "Brand list", - "brands.railLabel": "Your brands", - "brands.add": "Add", - "brands.brandName": "Brand name", - "brands.brandNamePh": "e.g. your brand", - "brands.creating": "Creating…", - "brands.createBrand": "Create brand", - "brands.searchAria": "Search brands", - "brands.searchPh": "Search brands…", - "brands.empty": "No brands yet", - "brands.noMatch": "No matches", - "brands.selectAria": "Select brand", - "brands.pickOne": "Pick a brand", - "brands.inUseHint": "Active · used for patrol and studio", - "brands.inUse": "Active", - "brands.tabBrands": "Brands", - "brands.tabInfo": "Brand info", - "brands.tabProducts": "Products", - "brands.tabProductsN": "Products ({n})", - "brands.displayName": "Name", - "brands.brief": "Summary", - "brands.briefPh": "One line about this brand", - "brands.audience": "Audience", - "brands.audiencePh": "Who cares and why", - "brands.goals": "Goals", - "brands.goalsPh": "What you want on Threads", - "brands.saving": "Saving…", - "brands.deleteBrand": "Delete brand", - "brands.searchProductAria": "Search products", - "brands.searchProductPh": "Search products…", - "brands.addProduct": "Add product", - "brands.noProducts": "No products yet", - "brands.hasLink": "Has link", - "brands.painLabel": "Pains ", - "brands.editProduct": "Edit product", - "brands.newProduct": "New product", - "brands.importFromUrl": "Import from product URL", - "brands.fetching": "Fetching…", - "brands.fetch": "Fetch", - "brands.pains": "Pain points", - "brands.painsPh": "One per line", - "brands.tags": "Tags", - "brands.tagsPh": "Comma-separated", - "brands.intro": "Description", - "brands.providerCapabilities": "Capabilities / services that solve the pain", - "brands.providerCapabilitiesPh": "e.g. dermatology, allergen testing, sensitive-skin consultation", - "brands.providerExcludes": "Same-category exclusions", - "brands.providerExcludesPh": "e.g. shampoo, hair-care products", - "brands.link": "Link", - "brands.update": "Update", - "brands.createItem": "Add", - "brands.needName": "Enter a name", - "brands.created": "Created “{name}”", - "brands.createFail": "Create failed", - "brands.saved": "Saved", - "brands.saveFail": "Save failed", - "brands.confirmDelete": "Delete “{name}”?", - "brands.deleted": "Deleted", - "brands.deleteFail": "Delete failed", - "brands.fetchFail": "Fetch failed", - "brands.needLabelContext": "Name and description are required", - "brands.productUpdated": "Updated", - "brands.productAdded": "Added", - "brands.confirmDeleteProduct": "Delete this product?", - - "insights.title": "Account performance", - "insights.account": "Account", - "insights.noAccount": "No accounts", - "insights.syncing": "Syncing…", - "insights.syncPosts": "Sync posts", - "insights.myPosts": "My posts", - "insights.pickAccount": "Select an account", - "insights.goAccounts": "Accounts", - "insights.kpiMonth": "This month", - "insights.monthViews": "Views this month", - "insights.monthLikes": "Likes this month", - "insights.monthReplies": "Replies this month", - "insights.engRate": "Engagement", - "insights.vsPrev": "vs last month", - "insights.avgNear": "Recent avg {rate}", - "insights.trendTitle": "Trends & analysis · @{user}", - "insights.metricViews": "Views", - "insights.metricLikes": "Likes", - "insights.metricReplies": "Replies", - "insights.metricPosts": "Posts", - "insights.metricPostsFull": "Posts", - "insights.chartMetrics": "Chart metric", - "insights.barsAria": "Recent months {metric}; click a bar for analysis", - "insights.barsLabel": "{metric} · last {n} months", - "insights.clickBar": " · click bar for analysis", - "insights.pickMonthAria": "Select month", - "insights.barTitle": "{label}: {value}{est} · click for analysis", - "insights.est": " (est.)", - "insights.monthSuffix": "{m}", - "insights.sparkAria": "Trend line; click a node to select month", - "insights.analysisOf": "{label} analysis", - "insights.producedAt": "Generated {time}", - "insights.hasEstimate": " · includes estimates", - "insights.viewsVsPrev": " · views vs prev {delta}", - "insights.statPosts": "Posts", - "insights.statViews": "Views", - "insights.statLikes": "Likes", - "insights.statReplies": "Replies", - "insights.conclusions": "Takeaways", - "insights.recommendations": "Recommendations", - "insights.highlights": "Month highlights", - "insights.findTopics": "Find topics", - "insights.goScout": "Go patrol", - "insights.selectMonth": "Select a month", - "insights.topPosts": "Top posts", - "insights.noPosts": "No posts yet", - "insights.postStats": "Views {views} · likes {likes} · replies {replies}", - "insights.openThreads": "Open Threads", - "insights.zeroPct": "0%", - "insights.panelHint": "Aggregates metrics from synced “My posts”", - "insights.lastSynced": "Last sync {time}", - "insights.neverSynced": "Not synced yet", - "insights.emptyTitle": "No post data yet", - "insights.emptyDesc": "Tap “Sync posts” to pull your Threads posts and insights, then review charts and analysis.", - "insights.syncDone": "Synced {n} posts · insights updated", - "insights.syncFail": "Sync failed", - "insights.loadFail": "Failed to load posts", - "insights.zeroViewsHint": "Posts found but views are mostly 0 — Insights scope may be missing, or stats not ready. Try sync again.", - "insights.postsInMonth": "{n} posts", - "insights.kpiForMonth": "{label} metrics", - "insights.topPostsOfMonth": "{label} · top posts", - "insights.noPostsInMonth": "No posts in {label}", - "insights.pastMonthEmptyHint": "No synced posts for this month (past months are fetched once). Sync manually or pick another month.", - "insights.pastBackfillDone": "Backfilled {n} posts (past months fetch once)", - "insights.autoRefreshDone": "Current month refreshed ({n} posts)", - "insights.noDataNoAnalysis": "No posts in {label} — no analysis.", - "insights.noAnalysisYet": "No analysis for {label} yet (need synced posts).", - "insights.thisMonth": "This month", - "insights.narrative.summary": "{when} summary: {posts} posts, {views} views, {likes} likes, {replies} replies (from synced posts).", - "insights.narrative.viewsDelta": "Views {delta} vs last month ({prev} → {curr}).", - "insights.narrative.viewsFlat": "Views roughly flat vs last month ({delta}).", - "insights.narrative.repliesUp": "Replies {delta}; conversation is heating up.", - "insights.narrative.repliesDelta": "Replies {delta}.", - "insights.narrative.engRate": "Engagement about {pct}% (likes+replies+reposts+quotes+shares / views).", - "insights.narrative.engLow": "Engagement is low: try ending with a specific question.", - "insights.narrative.engGood": "Engagement looks solid: reuse a high-performing structure for 1–2 more posts.", - "insights.narrative.zeroViews": "This month has likes/replies but 0 views (Insights may still be pending or missing permission).", - "insights.narrative.highlight": "One stronger post: {snippet}", - "insights.narrative.smallSample": "Few posts this month — treat month-over-month as a hint only.", - - "plays.tabOwn": "My posts", - "plays.tabLink": "Threads link", - "plays.noPosts": "No posts yet", - "plays.targetPost": "Target post", - "plays.likesSuffix": " ({n} likes)", - "plays.linkCard": "Paste Threads link", - "plays.postLink": "Post URL", - "plays.resolving": "Resolving…", - "plays.resolve": "Resolve link", - "plays.resolveHint": "After resolve, schedule your accounts to reply under that post.", - "plays.targetOwn": "Target post (yours)", - "plays.openThreads": "Open Threads", - "plays.external": "External post", - "plays.addScheme": "New scheme", - "plays.schemeCount": "{n} schemes for this target", - "plays.noSchemes": "No schemes yet", - "plays.replyCount": "{n} replies", - "plays.editTitle": "Edit: {title}", - "plays.schemeName": "Scheme name", - "plays.schemeNamePh": "e.g. Scheme A · soft engage", - "plays.speakersOwn": "Accounts (post owner always included)", - "plays.speakers": "Accounts", - "plays.postOwner": " (post owner)", - "plays.noAccounts": "No usable accounts. Connect Threads first.", - "plays.interval": "Interval (min)", - "plays.applyInterval": "Apply interval", - "plays.aiEmpty": "AI fill empty", - "plays.aiBusy": "Generating…", - "plays.aiFail": "AI generate failed", - "plays.aiStepDone": "Step filled — edit as needed", - "plays.aiNoneFilled": "No empty steps to fill (or persona not ready)", - "plays.needPersonaForStep": "Pick a ready persona on this step first", - "plays.aiEmptyResult": "AI returned empty — retry or pick a faster model", - "plays.saveBeforeAi": "Save the play first, then generate the full script (needs play id)", - "plays.scriptJobQueued": "Full-script job queued — you can leave; steps fill when done", - "plays.scriptJobDone": "Script generated — steps filled (edit as needed)", - "plays.scriptJobDoneReload": "Script done — reopen the play to see steps", - "plays.replies": "Replies ({n})", - "plays.stepN": "Reply {n}", - "plays.who": "Who", - "plays.personaOpt": "Persona (optional)", - "plays.brandOpt": "Brand (optional)", - "plays.reply": "Reply", - "plays.attach": "Images", - "plays.addOne": "Add one", - "plays.saving": "Saving…", - "plays.saveScheme": "Save scheme", - "plays.submitting": "Submitting…", - "plays.submitOutbox": "Submit to Outbox", - "plays.closeEdit": "Close editor", - "plays.noTarget": "No target post yet", - "plays.resolved": "Link resolved", - "plays.resolveFail": "Resolve failed", - "plays.filled": "Generated {n}", - "plays.needTarget": "Select a target post first", - "plays.saved": "Scheme saved", - "plays.saveFail": "Save failed", - "plays.submitted": "Sent to Outbox", - "plays.submitFail": "Submit failed", - "plays.confirmDelete": "Delete this scheme?", - "plays.accountFallback": "account", - - "inspire.loading": "Loading…", - "inspire.trendsAria": "Topic ideas", - "inspire.trendsLabel": "Topic ideas", - "inspire.trendsHint": "Web-sourced ideas · costs a search credit", - "inspire.trendsSeed": "sample", - "inspire.topicSeed": "I want a Threads post about “{topic}”. Help me with openings and angles.", - "inspire.trendsEmpty": "Tap “Find ideas” to search (uses credits)", - "inspire.refreshConfirm": "Finding ideas costs 1 search credit. Continue?", - "inspire.refreshOk": "Updated {n} topic ideas", - "inspire.refreshFail": "Could not find ideas (quota or search failed)", - "inspire.refresh": "Find ideas", - "inspire.clearChat": "New chat", - "inspire.clearedNewSession": "New chat started", - "inspire.sessionsAria": "Inspiration chats", - "inspire.session": "Chat", - "inspire.sessionNew": "New chat", - "inspire.newSession": "+ New", - "inspire.newSessionOk": "New chat started (previous kept in list)", - "inspire.deleteSession": "Delete current chat", - "inspire.deleteSessionShort": "Delete", - "inspire.confirmDeleteSession": "Delete this chat permanently?", - "inspire.deletedSession": "Deleted — switched to another chat", - "inspire.pinAsElement": "Apply as element", - "inspire.you": "You", - "inspire.ai": "AI", - "inspire.system": "System", - "inspire.useDraft": "Use this draft", - "inspire.openPlay": "Open play", - "inspire.thinking": "Thinking…", - "inspire.stop": "Stop generating", - "inspire.stopped": "Generation stopped", - "inspire.pinnedAria": "This-round references (for the AI)", - "inspire.pinned": "References", - "inspire.pinnedCount": "· {n}", - "inspire.pinsLocalShort": "this session", - "inspire.pinsSessionLocal": "References apply to this session only and will be included with your next message.", - "inspire.pickRight": "Pick on the right = pin for AI; click name to insert", - "inspire.unpinTitle": "Remove reference", - "inspire.insertPinTitle": "Insert into input", - "inspire.insertBrand": "Talk about “{name}”", - "inspire.insertedPin": "Inserted “{name}” into the input", - "inspire.flowStep1": "Prep", - "inspire.flowStep2": "Ideate", - "inspire.flowStep3": "Persona draft", - "inspire.emptyTitle": "Ideate first, then draft in persona", - "inspire.emptyDesc": "Pick a path. You don’t need every control.", - "inspire.entryTopic": "Start from a topic", - "inspire.entryPaste": "I have a draft — rewrite in persona", - "inspire.startTopicHint": "Type a topic below and press Enter", - "inspire.pasteDraftHint": "Paste your draft into the box, then rewrite", - "inspire.needMaterialOrPaste": "No chat material yet — paste text to rewrite", - "inspire.flowOneLiner": "Talk it through, search when needed, then turn the conversation into a post.", - "inspire.showTopics": "Topics", - "inspire.hideTopics": "Hide topics", - "inspire.showLibrary": "Library", - "inspire.hideLibrary": "Hide library", - "inspire.showAdvanced": "Advanced", - "inspire.hideAdvanced": "Hide advanced", - "inspire.readyToWrite": "{n} turns in — ready to draft", - "inspire.inputAria": "Talk to AI", - "inspire.inputPh": "What to explore? Enter to send · Shift+Enter newline", - "inspire.previewTitle": "Full payload that will be sent", - "inspire.previewPrompt": "Full prompt (persona / brands / pins / chat — same as AI)", - "inspire.previewSections": "Included sections", - "inspire.previewPinnedCount": "{n} pinned elements", - "inspire.previewNoPins": "No pinned elements yet", - "inspire.runes": "chars", - "inspire.copyAll": "Copy all", - "inspire.copied": "Full prompt copied", - "inspire.copyFail": "Copy failed", - "inspire.rawPrompt": "Raw text sent to AI", - "inspire.verifyHow": "To verify: open ? for fingerprint → send without changes → status says match.", - "inspire.verifyHowShort": "Type → ? (previews Send path) → send unchanged → should match. Generate uses different mode (fingerprint will differ).", - "inspire.previewModeNote": "Assembled for mode={mode} (same as sending with that mode).", - "inspire.lastSentFp": "Last sent fingerprint", - "inspire.matchOk": "Matches last send ✓", - "inspire.matchBad": "Differs from preview (input/pins/persona/mode)", - "inspire.matchBadShort": "≠ last sent {sent}", - "inspire.fpMatch": "Sent fingerprint {fp} matches preview", - "inspire.fpMismatch": "Different: preview {preview} ≠ sent {sent} (text/mode changed?)", - "inspire.fpSent": "Sent fingerprint {fp}", - "inspire.viewSent": "View sent prompt", - "inspire.viewSentShort": "Sent", - "inspire.sentPrompt": "Prompt actually sent", - "inspire.sentPromptNote": "Exact prompt the backend sent to the AI (from stream done).", - "inspire.send": "Send", - "inspire.generate": "Turn into post", - "inspire.generateHint": "Turn the whole conversation into a publish-ready post using the selected persona and strong engagement principles", - "inspire.webSearch": "Search web", - "inspire.webSearchOn": "Web search: on", - "inspire.webSearchHint": "The next message will search Exa before AI responds", - "inspire.needConversation": "Share one thought first, then turn it into a post.", - "inspire.needReadyPersona": "Select a persona that has finished analysis first.", - "inspire.generating": "Rewriting…", - "inspire.generateOk": "Draft rewritten in persona", - "inspire.materialTitle": "Lock content to write", - "inspire.materialHint": "Generate only rewrites this block (editable). Chat is for ideation; this is for the final voice.", - "inspire.materialLabel": "Content to rewrite", - "inspire.materialPh": "Key points, angle, what you want to say…", - "inspire.rewriteNotes": "Rewrite notes (optional)", - "inspire.rewriteNotesPh": "e.g. shorter, more casual, end with a question", - "inspire.rewriteDefault": "Rewrite as a Threads post in persona voice", - "inspire.confirmRewrite": "Rewrite in persona", - "inspire.needMaterial": "Chat a bit first, or paste text to rewrite", - "inspire.library": "Element library", - "inspire.addNew": "+ Add", - "inspire.kind": "Type", - "inspire.kindRole": "Role prompt", - "inspire.kindSnippet": "Snippet", - "inspire.kindTrendNote": "Trend note", - "inspire.kindBrand": "Brand", - "inspire.kindTrend": "Trend", - "inspire.name": "Name", - "inspire.namePh": "e.g. pro Threads writer", - "inspire.body": "Content (goes into prompt)", - "inspire.bodyPh": "You are a…", - "inspire.saveElement": "Save to library", - "inspire.citeBrand": "Cite brand", - "inspire.applied": "Applied", - "inspire.clickApply": "Click to apply", - "inspire.noBrands": "No brands yet", - "inspire.appliedToggle": "Applied · click again to remove", - "inspire.deleteAria": "Delete", - "inspire.needInput": "Type what you want to explore", - "inspire.fail": "Failed", - "inspire.wantWrite": "Want to write about {label}: {summary}", - "inspire.trendBody": "Topic: {label}. {summary}", - "inspire.pinnedTrend": "Applied trend {label}", - "inspire.needTitleBody": "Name and content required", - "inspire.added": "Added to library", - "inspire.addFail": "Add failed", - "inspire.confirmRemove": "Remove this from the library?", - "inspire.confirmClear": "Start a new chat? (previous chats stay in the list)", - "inspire.genMessage": "Rewrite as a Threads post in persona voice", - - "persona.add": "New persona", - "persona.empty": "No personas yet", - "persona.emptyDesc": "Add one and analyze it for drafting.", - "persona.statusReady": "ready", - "persona.statusAnalyzing": "analyzing", - "persona.statusPending": "pending", - "persona.default": "Default", - "persona.backList": "← Personas", - "persona.tabOverview": "Overview", - "persona.tabAnalyze": "Analyze", - "persona.tabFingerprint": "Fingerprint", - "persona.tabPreview": "Preview", - "persona.name": "Name", - "persona.brief": "Brief", - "persona.briefPh": "Who, for whom, core message…", - "persona.avoid": "Guardrails · banned words (comma-separated)", - "persona.guardChars": "{n} chars", - "persona.banAi": " · ban AI tone", - "persona.notReadySuffix": " · not ready", - "persona.setDefault": "Set as default", - "persona.modeAccount": "Public account", - "persona.modeText": "Paste text", - "persona.username": "Threads username", - "persona.fromBound": "From linked account", - "persona.select": "Select…", - "persona.crawlAnalyze": "Analyze public posts", - "persona.crawlBusy": "Analyzing…", - "persona.refText": "Reference text (--- separates posts)", - "persona.refTextPh": "First post…\n\n---\n\nSecond post…", - "persona.sourceLabel": "Source note (optional)", - "persona.sourcePh": "My old posts", - "persona.analyzeText": "Analyze from text", - "persona.analyzeBusy": "Analyzing…", - "persona.sampleMeta": "Samples {n}", - "persona.sourceManual": "Pasted", - "persona.analyzeHint": "After analysis, 8D summaries appear here.", - "persona.fingerprintHint": "Main voice for drafting. Edit catchphrases, rhythm, bans; Studio/replies use this.", - "persona.fingerprint": "Language fingerprint", - "persona.fingerprintPh": "Filled after analysis…", - "persona.saveFingerprint": "Save fingerprint", - "persona.tryGen": "Preview root + reply", - "persona.previewHint": "Write a sample root post and reply in this fingerprint style. Uses a live news headline as inspiration (rewritten in-character, not a news dump).", - "persona.previewRunning": "Generating…", - "persona.previewDone": "Preview done · topic: {topic} ({source})", - "persona.previewFail": "Preview failed — try again", - "persona.previewTopicLabel": "Topic seed: {topic} · {source}", - "persona.topicNews": "live news", - "persona.topicManual": "manual", - "persona.topicFallback": "everyday seed", - "persona.notReadyMsg": "Persona not ready", - "persona.rootPost": "Root post", - "persona.reply": "Reply", - "persona.hidePrompt": "Hide prompt block", - "persona.showPrompt": "Show injected prompt", - "persona.promptBlock": "prompt block (post)", - "persona.pickOne": "Pick a persona", - "persona.pickDesc": "Or add one to start analyzing.", - "persona.created": "Created — finish account analysis or paste text under Analyze", - "persona.saved": "Saved", - "persona.textDone": "Text analysis done · {n} segments → ready", - "persona.analyzeFail": "Analysis failed", - "persona.reading": "Reading public posts…", - "persona.accountDone": "@{user} · {n} posts → ready", - "persona.jobQueued": "Queued as a background job · you can leave; results save automatically", - "persona.jobQueuedCrawl": "Analyze job queued · you can leave; persona updates when done", - "persona.jobRunning": "Analyzing in background… will refresh when ready (see Jobs for progress)", - "persona.jobDone": "Background analysis finished · fingerprint saved", - "persona.jobFailed": "Background analysis failed — check Jobs for the error or retry", - "persona.loadFail": "Could not load personas", - "persona.openJob": "Open job details", - "persona.setDefaultMsg": "“{name}” set as default", - "persona.confirmDelete": "Delete persona “{name}”?", - "persona.deleted": "Persona deleted", - "persona.needReady": "Finish analysis first (ready)", - "persona.dim.d1Tone": "D1 Tone", - "persona.dim.d2Structure": "D2 Structure", - "persona.dim.d3Interaction": "D3 Interaction", - "persona.dim.d4Topics": "D4 Topics", - "persona.dim.d5Rhythm": "D5 Rhythm", - "persona.dim.d6Visual": "D6 Visual", - "persona.dim.d7Conversion": "D7 Conversion", - "persona.dim.d8Risk": "D8 Risk", - - "admin.users.loadFail": "Load failed", - "admin.users.created": "Added islander “{name}” · copy the password below", - "admin.users.createFail": "Create failed", - "admin.users.unlimitedOn": "“{name}” set to unlimited (usage still counted)", - "admin.users.unlimitedOff": "“{name}” back to plan limits", - "admin.users.updateFail": "Update failed", - "admin.users.planSet": "“{name}” plan → {plan}", - "admin.users.confirmSuspend": "Suspend “{name}”?\\nThey will not be able to sign in.", - "admin.users.confirmUnsuspend": "Restore “{name}”?\\nThey can sign in again.", - "admin.users.didSuspend": "Suspended “{name}”", - "admin.users.didUnsuspend": "Restored “{name}”", - "admin.users.suspendFail": "Suspend failed", - "admin.users.unsuspendFail": "Restore failed", - "admin.users.markedVerified": "Marked {name} as email verified", - "admin.users.markedUnverified": "Marked {name} as unverified", - "admin.users.rolesUpdated": "Updated roles for {name}: {roles}", - "admin.users.rolesFail": "Role update failed", - "admin.users.confirmReset": "Reset password for “{name}”?\\nTemp password stays visible until you close it (survives refresh).", - "admin.users.resetDone": "Reset password for {name} (shown below — copy then close)", - "admin.users.resetFail": "Reset failed", - "admin.users.copied": "Copied to clipboard", - "admin.users.copyFail": "Copy failed — select the password manually", - "admin.users.confirmDismissTemp": "After close, this temp password won't show again (copy first if needed). Close?", - "admin.users.tempPwNew": "New islander temp password", - "admin.users.tempPw": "Temp password", - "admin.users.tempPwPersist": "(stays visible · survives refresh)", - "admin.users.copyPw": "Copy password", - "admin.users.close": "Close", - "admin.users.createTitle": "Add islander", - "admin.users.memberName": "Display name", - "admin.users.displayNamePh": "Display name", - "admin.users.email": "Email", - "admin.users.initPassword": "Initial password (optional)", - "admin.users.initPasswordPh": "Leave blank to auto-generate; if set, must meet policy", - "admin.users.markVerifiedCheck": "Mark email verified (usable immediately)", - "admin.users.alsoAdmin": "Also make admin", - "admin.users.creating": "Creating…", - "admin.users.createSubmit": "Create islander", - "admin.users.clear": "Clear", - "admin.users.searchActive": "Search “{query}” · matches name, email, uid", - "admin.users.noMatch": "No matches", - "admin.users.none": "No islanders yet", - "admin.users.you": "You", - "admin.users.status": "Status", - "admin.users.role": "Roles", - "admin.users.emailVerify": "Email verification", - "admin.users.bio": "Bio", - "admin.users.timezone": "Timezone", - "admin.users.notifyEmail": "Email notifications", - "admin.users.on": "On", - "admin.users.off": "Off", - "admin.users.createdAt": "Created", - "admin.users.updatedAt": "Updated", - "admin.users.accountStatus": "Account status", - "admin.users.updating": "Updating…", - "admin.users.usageTitle": "Usage & plan", - "admin.users.usageLiveSkip": "Plan/quota are Usage domain — not on live backend yet (M3). Editable in mock only.", - "admin.users.plan": "Plan", - "admin.users.planOption": "{name} ({credits} credits / mo)", - "admin.users.unlimited": "Unlimited", - "admin.users.byPlan": "By plan", - "admin.users.setUnlimited": "Set unlimited", - "admin.users.setLimited": "Enforce plan limits", - "admin.users.unlimitedHint": "Unlimited: can keep using past plan cap; AI/Search counts and credits still track.", - "admin.users.loadingUsage": "Loading usage prefs…", - "admin.users.assignRoles": "Assign roles", - "admin.users.memberBase": "{role} (base, always on)", - "admin.users.adminDesc": "{role} — manage islanders and system", - "admin.users.saving": "Saving…", - "admin.users.saveRoles": "Save roles", - "admin.users.markUnverifiedBtn": "Mark unverified", - "admin.users.markVerifiedBtn": "Mark verified", - "admin.users.resetting": "Resetting…", - "admin.users.resetTemp": "Reset password (temp)", - "admin.users.customPw": "Or set a password (optional)", - "admin.users.customPwPh": "Password must be at least 12 characters with upper, lower, digit, and symbol", - "admin.users.resetWithCustom": "Reset with this password", - - "usage.tabMine": "My usage", - "usage.tabTenant": "All usage", - "usage.currentPlan": "Current plan", - "usage.planMeta": "/ mo · {n} credits monthly", - "usage.changePlan": "Change plan", - "usage.upgradePlan": "Upgrade plan", - "usage.outcome.title": "This month's outcomes", - "usage.outcome.summary": "Reach {reach} · Conversations {conversations} · Conversions {conversions}", - "usage.outcome.amount": " (~${amount})", - "usage.outcome.emptyHint": "No attributable outcomes this month yet — try patrol or posting.", - "usage.warn.unlimitedOver": "Used {used} credits this month (plan {cap}; limits off — you can continue).", - "usage.warn.exhausted": "Monthly credits are used up. Upgrade or wait for next month.", - "usage.warn.high": "You've used {pct}% of monthly credits.", - "usage.warn.meterNear": "{label} is near its cap ({credits}/{cap} credits).", - "usage.usedThisMonth": "Used this month", - "usage.remainLabel": "Remaining", - "usage.ledgerToggle": "Usage log", - "usage.collapse": "Collapse", - "usage.eventsCount": "{n} events", - "usage.granularity": "Granularity", - "usage.day": "Day", - "usage.monthUnit": "Month", - "usage.year": "Year", - "usage.from": "From", - "usage.to": "To", - "usage.callCounts": "Call counts", - "usage.noMembers": "No members yet", - "usage.planAria": "{name} plan", - "usage.unlimitedTitle": "Unlimited", - "usage.setLimited": "Enforce limits", - "usage.setUnlimited": "Set unlimited", - "usage.limitShort": "Cap", - "usage.subscribed": "Subscribed to {name}", - "usage.unlimitedSet": "Set unlimited", - "usage.limitedSet": "Limits enforced", - "usage.planUpdated": "Plan updated to {name}", - "usage.fail": "Failed", - - "currency.TWD": "New Taiwan Dollar (TWD)", - "currency.USD": "US Dollar (USD)", - "currency.JPY": "Japanese Yen (JPY)", - "currency.EUR": "Euro (EUR)", - "currency.HKD": "Hong Kong Dollar (HKD)", - - "locale.zh-TW": "繁體中文", - "locale.en": "English", - - "pager.nav": "Pagination", - "pager.pageSize": "Items per page", - "pager.perPage": "{n}/page", - "pager.prev": "Previous", - "pager.next": "Next", - - "plays.defaultTitle": "New play", - "plays.topicOnPost": "On: {snippet}", - "plays.topicOnExternal": "On: {label} · {snippet}", - "plays.externalFallback": "External post", - - "persona.newName": "New persona", - "persona.previewTopic": "Looking for a café I can sit in for hours", - "persona.previewReplySample": "The one in Da’an is fine but crowded", - - "inspire.playTitle": "Inspired thread", - - "play.err.needLead": "Pick a lead account", - "play.err.needRoot": "Add at least one root post", - "play.err.firstMustRoot": "The first step must be the root post", - "play.err.rootMustLead": "Root post must use the lead account", - "play.err.rootEmpty": "Root post text cannot be empty", - "play.err.replyAccount": "Replies can only use lead or selected cast accounts", - "play.err.replyEmpty": "Reply text cannot be empty", - "play.err.needTarget": "Pick one of your posts, or paste a Threads link", - "play.err.needReplies": "Add at least one reply", - "play.err.needReplyAccounts": "Pick at least one reply account", - "play.err.stepAccount": "Every reply needs a usable account", - "play.err.stepEmpty": "Reply text cannot be empty", - "play.err.notFound": "Play not found", - - "time.justNow": "Just now", - "time.minAgo": "{n}m ago", - "time.hourAgo": "{n}h ago", - "time.dayAgo": "{n}d ago", - "time.min": "{n} min", - "time.hour": "{n} hr", - "time.day": "{n} day", - "time.expired": "Expired {span}", - "time.remaining": "{span} left", - "time.sessionUnknown": "Session not recorded", - "time.sessionExpired": "Session expired · {absolute}", - "time.sessionSoon": "Session expiring · {relative} ({absolute})", - "time.sessionOk": "Session OK · {relative} ({absolute})", - - "policy.title": "Opportunity and reply policy", - "policy.subtitle": "Set service scope, forbidden words, cases, FAQs, and tone. Qualification and generated replies share this policy.", - "radar.profile.title": "Service profile", - "radar.profile.subtitle": - "The radar uses this to judge whether an opportunity is worth answering, and replies quote these prices and tone.", - "radar.profile.firstTimeHint": - "Fill this in before turning on a radar watch. The more specific it is, the better the scoring and replies.", - "radar.profile.updatedAt": "Last updated: {at}", - "radar.profile.saved": "Service profile saved", - "radar.profile.services": "Services and pricing", - "radar.profile.servicesHint": - "At least one. Leave prices empty to mean negotiable; if filled they show up in replies.", - "radar.profile.serviceName": "Service name", - "radar.profile.serviceNamePh": "e.g. Interior measuring and layout", - "radar.profile.priceMin": "Price from", - "radar.profile.priceMax": "Price to", - "radar.profile.addService": "+ Add service", - "radar.profile.areas": "Service areas", - "radar.profile.areasHint": - "Pick the cities you can actually serve; mismatched leads score lower. Tick remote below if location does not matter.", - "radar.profile.remoteOk": "Remote work is fine (any location)", - "radar.profile.forbidden": "Forbidden words", - "radar.profile.forbiddenHint": "One per line. These never appear in any generated reply.", - "radar.profile.forbiddenPh": "guaranteed\ncheapest\nnumber one", - "radar.profile.cases": "Case studies", - "radar.profile.casesHint": "Optional. Replies cite these when proof helps; if empty they cite nothing.", - "radar.profile.caseTitle": "Case title", - "radar.profile.caseSummary": "One-line summary", - "radar.profile.addCase": "+ Add case", - "radar.profile.faq": "FAQ", - "radar.profile.faqHint": "Optional. Replies follow these answers when a similar question comes up.", - "radar.profile.faqQuestion": "Question", - "radar.profile.faqAnswer": "Answer", - "radar.profile.addFaq": "+ Add FAQ", - "radar.profile.availability": "Availability", - "radar.profile.availabilityHint": "e.g. Can start within two weeks, weekends only", - "radar.profile.toneNote": "Tone note", - "radar.profile.toneNoteHint": "e.g. Be direct, skip pleasantries, no exclamation marks", - - "radar.watches.title": "Demand watches", - "radar.watches.subtitle": - "Subscribe to keywords for a daily auto list. Unlike Patrol, you don’t re-run a manual scan each time.", - "radar.watches.needProfile": "Fill in the service profile before enabling demand watches", - "radar.watches.needProfileHint": - "Scoring needs your service profile; without it every judgment is a guess.", - "radar.watches.goProfile": "Go to service profile", - "radar.watches.quota": "Active {used} / {max}", - "radar.watches.quotaFull": "Plan limit reached — pause or archive one to add another", - "radar.watches.add": "+ New watch", - "radar.watches.newTitle": "New demand watch", - "radar.watches.editTitle": "Edit demand watch", - "radar.watches.requiredHint": "Required field", - "radar.watches.terms": "Terms", - "radar.watches.termsHint": "One short query per line. Max 2 tokens, CJK 2–4 chars each — long sentences are shortened on save so Threads can search them.", - "radar.watches.threadsWarn": "Some terms break Threads short-query rules: max 2 words, CJK 2–4 chars each, ≤12 chars total, no punctuation/#/emoji. Save shortens them into searchable queries.", - "radar.watches.threadsRequired": "These terms cannot be shortened into Threads-searchable queries. Use at most 2 tokens, CJK 2–4 chars each.", - "radar.watches.termsPh": "interior design\nfind designer", - "radar.watches.excludeTerms": "Exclude terms", - "radar.watches.excludeHint": "A hit here skips the post entirely, e.g. job ads or giveaways.", - "radar.watches.excludePh": "hiring\ngiveaway", - "radar.watches.regionsHint": - "Leave regions empty to reuse the service profile areas; only narrow it down for this watch.", - "radar.watches.regionsFromProfile": "Regions from service profile", - "radar.watches.enableNow": "Activate right away (uses an active slot)", - "radar.watches.filterStatus": "Status", - "radar.watches.statusAll": "All", - "radar.watches.status.active": "Active", - "radar.watches.status.paused": "Paused", - "radar.watches.status.archived": "Archived", - "radar.watches.pause": "Pause", - "radar.watches.resume": "Resume", - "radar.watches.archive": "Archive", - "radar.watches.confirmArchive": "Archived watches stop sweeping and cannot be restored. Continue?", - "radar.watches.deleteArchived": "Delete archived watch", - "radar.watches.deletingArchived": "Deleting…", - "radar.watches.confirmDeleteArchived": "Permanently delete this archived watch setting? Existing opportunities, sweeps, and statistics stay, but this setting will no longer appear in the list.", - "radar.watches.deletedArchived": "Archived watch setting deleted", - "radar.watches.created": "Watch created", - "radar.watches.createdFirstSweep": "Watch created — first sweep queued. Check today's demand in a few minutes (it runs automatically every day after this).", - "radar.watches.updated": "Watch updated", - "radar.watches.paused": "Watch paused", - "radar.watches.resumed": "Watch resumed", - "radar.watches.archived": "Watch archived", - "radar.watches.lastSwept": "Last sweep: {at}", - "radar.watches.neverSwept": "Never swept", - "radar.watches.empty": "No demand watches yet", - "radar.watches.emptyHint": - "Add short buyer phrases (e.g. “find designer”); the system sweeps daily. For a one-off pain/topic sortie, use Patrol.", - "radar.watches.emptyFiltered": "No watches in this status", - "radar.watches.scheduleTitle": "Daily patrol: 06:00 Taipei (22:00 UTC)", - "radar.watches.scheduleHint": "Active watches run once a day. Use Run now for an extra pass. Turning off Run now does not stop the daily patrol.", - "radar.watches.openToday": "Back to findings", - "radar.watches.sweepNow": "Run now", - "radar.watches.sweepQueued": "Demand sweep queued", - "radar.watches.sweepStarted": "Demand sweep started (job {job}…)", - - "today.radar.title": "Today's demand", - "today.radar.total": "Found", - "today.radar.high": "High", - "today.radar.mid": "Mid", - "today.radar.low": "Low", - "today.radar.open": "Open today's demand", - "today.radar.empty": "No demand yet. Add keyword watches for a daily auto list (different from Patrol’s manual scan).", - "today.radar.goProfile": "Fill service profile", - "today.radar.goWatches": "Add keyword watches", - - "firstRun.title": "Connect a Threads account first", - "firstRun.subtitle": "After you connect, the rest of the app unlocks. Tap the button to open Accounts.", - "firstRun.skip": "Skip — I'll look around", - "firstRun.go": "Connect", - "firstRun.progress": "Step {current} of {total}", - "firstRun.done": "Done", - "firstRun.aria": "First-run setup", - "firstRun.step.crew": "Connect a Threads account", - "firstRun.step.crewHint": "Needed later if you want to send a reply.", - "firstRun.step.brands": "Set brand and product", - "firstRun.step.brandsHint": "Fill audience, pains, and product capabilities.", - "firstRun.step.watch": "Create a daily patrol", - "firstRun.step.watchHint": "Pick the product, adopt suggested keywords, and save.", - "firstRun.step.radar": "Review the first demand", - "firstRun.step.radarHint": "No need to wait until tomorrow — use Explore now.", - "firstRun.status.pending": "Onboarding", - "firstRun.status.skipped": "Onboarding skipped", - "firstRun.status.completed": "Onboarding done", - - "radar.suggest.title": "Term suggestions", - "radar.suggest.hint": - "Terms drawn from your service profile. Adopt them one by one or all at once; you still need to save to create the watch.", - "radar.suggest.ask": "Get suggestions", - "radar.suggest.again": "Suggest more", - "radar.suggest.asking": "Thinking…", - "radar.suggest.adopt": "Adopt", - "radar.suggest.adopted": "Adopted", - "radar.suggest.adoptAll": "Adopt all", - "radar.suggest.include": "Term", - "radar.suggest.exclude": "Exclude", - "radar.suggest.none": "Nothing usable came back. Make the service profile more specific and try again.", - - "radar.today.title": "Today's demand", - "radar.today.subtitle": "Daily auto list from your watches (not Patrol’s one-off scan).", - "radar.today.link.watches": "Demand watches", - "radar.today.link.crm": "CRM board", - "radar.today.stats.total": "Found today", - "radar.today.stats.high": "High intent", - "radar.today.stats.mid": "Mid intent", - "radar.today.stats.low": "Low intent", - "radar.today.truncated": "Daily cap reached; {n} lower-intent leads were not listed", - "radar.today.lastSwept": "Last sweep: {at}", - "radar.today.band.high": "High", - "radar.today.band.mid": "Mid", - "radar.today.band.low": "Low", - "radar.today.status.accepted": "Added to CRM", - "radar.today.status.dismissed": "Dismissed", - "radar.today.status.qualified": "Open", - "radar.today.status.rejected": "Rejected", - "radar.today.status.judging": "Judging", - "radar.today.regionUnknown": "Region unknown", - "radar.today.group.high": "High intent", - "radar.today.group.mid": "Mid intent", - "radar.today.group.low": "Low intent", - "radar.today.group.empty": "None in this band", - "radar.today.group.expand": "Expand", - "radar.today.group.collapse": "Collapse", - "radar.today.action.open": "Original", - "radar.today.action.accept": "Add to CRM", - "radar.today.action.dismiss": "Dismiss", - "radar.today.action.reply": "Generate reply", - "radar.today.action.hideReply": "Hide reply", - "radar.today.action.reasons": "Why this score", - "radar.today.action.hideReasons": "Hide reasons", - "radar.today.action.override": "Override band", - "radar.today.reply.hint": "Pick a variant. DM is copy-only — never auto-sent.", - "radar.today.reply.copy": "Copy draft", - "radar.today.reply.variant.public_comment": "Public comment", - "radar.today.reply.variant.dm": "DM", - "radar.today.reply.variant.no_sales": "No-sales", - "radar.today.reply.variant.professional": "Professional", - "radar.today.reply.variant.humorous": "Light", - "radar.today.dim.authenticity": "Authenticity", - "radar.today.dim.intent": "Intent", - "radar.today.dim.region": "Region", - "radar.today.dim.freshness": "Freshness", - "radar.today.dim.fit": "Fit", - "radar.today.empty.title": "No demand for today yet", - "radar.today.empty.fallback": "Check back later, or review demand watches and the service profile.", - "radar.today.empty.goProfile": "Fill service profile", - "radar.today.empty.goWatches": "Add keyword watches", - "radar.today.empty.goAll": "View all results", - "radar.today.empty.reason.no_profile": "No service profile yet — fit cannot be scored.", - "radar.today.empty.reason.no_watch": "No demand watches yet; create keywords for daily auto sweeps (not Patrol’s manual scan).", - "radar.today.empty.reason.all_watches_paused": "All watches are paused. Resume one to keep daily sweeps.", - "radar.today.empty.reason.not_swept_yet": "Daily patrol hasn’t finished yet; you can also hit Run now on Demand.", - "radar.today.empty.reason.sweep_failed": "This auto sweep failed — check demand watches and retry.", - "radar.today.empty.reason.no_hit": "Swept but no matching demand. Loosen terms or exclusions.", - "radar.today.msg.accepted": "Added to CRM", - "radar.today.msg.dismissed": "Dismissed", - "radar.today.msg.replyReady": "Reply draft ready", - "radar.today.msg.overridden": "Band updated", - "radar.today.msg.copied": "Copied to clipboard", - "radar.today.msg.copyFail": "Could not copy — select the text manually", - "radar.today.msg.marked": "Marked as sent/copied", - "radar.today.msg.sent": "Sent — check progress in the outbox queue", - "radar.today.msg.needReply": "Generate a reply draft first", - "radar.today.sendAccount": "Send from", - "radar.today.reply.markCopy": "Mark as copied & sent", - "radar.today.reply.markOutbox": "Send now (Outbox)", - "radar.today.reply.needAccount": "Connect a Threads account first to send", - "radar.today.reply.used": "Already marked used", - "radar.today.reply.usedOutbox": "Sent (check the outbox queue)", - - "radar.reconnectSearch": "Reconnect search source", - "radar.patrol.searchFallback": "If search is temporarily unavailable, a fallback source is used. Credits are charged only on a successful result.", - "radar.empty.sweepFailedHint": "Patrol failed. Run it again, or switch to last 7 days / all.", - "radar.inbox.title": "Demand", - "radar.inbox.patrolAria": "Patrol status", - "radar.inbox.scheduledOn": "Daily patrol: on", - "radar.inbox.scheduledOff": "Daily patrol: off", - "radar.inbox.scheduleHint": "Runs every day at 06:00 Taipei time. Turning off Run now does not stop the daily patrol.", - "radar.inbox.lastSweep": "Last patrol: {time}", - "radar.inbox.neverSwept": "Not patrolled yet", - "radar.inbox.activeWatches": "{n} watches on", - "radar.inbox.allPaused": "All watches are paused. Run now also needs at least one on.", - "radar.inbox.noWatches": "No product or keywords to patrol yet", - "radar.inbox.sweepNow": "Run now", - "radar.inbox.sweeping": "Patrolling…", - "radar.inbox.sweepAgain": "Run again", - "radar.inbox.setupWatches": "Set up patrol", - "radar.inbox.introTitle": "Pain points land here", - "radar.inbox.introBody": "Read why it was recommended, then keep or discard. Adding to CRM is optional.", - "radar.inbox.resultsAria": "Demand results", - "radar.inbox.tabsAria": "Result state", - "radar.inbox.tab.pending": "New", - "radar.inbox.tab.completed": "Reviewed", - "radar.inbox.tab.removed": "Discarded", - "radar.inbox.total": "{n} total", - "radar.inbox.clearFilters": "Clear filters", - "radar.inbox.defaultToday": "Showing what was found today by default", - "radar.inbox.timeScope": "Time range", - "radar.inbox.time.today": "Today", - "radar.inbox.time.7d": "Last 7 days", - "radar.inbox.time.all": "All", - "radar.inbox.sort": "Sort by", - "radar.inbox.sort.recommended": "Best product fit", - "radar.inbox.sort.newest": "Newest posts", - "radar.inbox.sort.oldest": "Oldest posts", - "radar.inbox.sort.productFit": "Closest product match", - "radar.inbox.sort.demandIntent": "Clearest demand", - "radar.inbox.moreFilters": "More filters", - "radar.inbox.moreFiltersN": "More filters ({n})", - "radar.inbox.hideFilters": "Hide extra filters", - "radar.inbox.moreFiltersAria": "More filters", - "radar.inbox.brand": "Brand", - "radar.inbox.allBrands": "All brands", - "radar.inbox.product": "Product", - "radar.inbox.allProducts": "All products", - "radar.inbox.band": "Intent", - "radar.inbox.allBands": "All intent", - "radar.inbox.band.high": "High", - "radar.inbox.band.mid": "Medium", - "radar.inbox.band.low": "Low", - "radar.inbox.match": "Product match", - "radar.inbox.allStates": "All states", - "radar.inbox.state.eligible": "Follow up", - "radar.inbox.state.weak": "Weak fit", - "radar.inbox.state.excluded": "Excluded", - "radar.inbox.state.generic": "No product", - "radar.inbox.state.stale": "Older than 14 days", - "radar.inbox.loading": "Preparing patrol results…", - "radar.inbox.prevPage": "Previous", - "radar.inbox.nextPage": "Next", - "radar.inbox.pageOf": "Page {page} of {pages}", - "radar.inbox.goCrm": "Open CRM", - "radar.inbox.see7d": "See last 7 days", - "radar.inbox.empty.filteredPending": "No results for this filter", - "radar.inbox.empty.filteredCompleted": "No reviewed results yet", - "radar.inbox.empty.filteredRemoved": "No discarded results yet", - "radar.inbox.empty.filteredHint": "Clear filters or change the time range. Fresh patrol results may also be under Last 7 days or All.", - "radar.inbox.empty.noCompleted": "Nothing reviewed yet", - "radar.inbox.empty.noCompletedHint": "Switch back to New to keep going through pain points.", - "radar.inbox.empty.noRemoved": "Nothing discarded yet", - "radar.inbox.empty.noRemovedHint": "Switch back to New to keep going through pain points.", - "radar.inbox.empty.noWatchesTitle": "No patrol set up", - "radar.inbox.empty.noWatchesHint": "Pick a product and the keywords customers search. Then you can run now; daily patrol will follow.", - "radar.inbox.empty.pausedTitle": "Daily patrol is off", - "radar.inbox.empty.pausedHint": "Run now and daily patrol both stay on this page. Turn at least one watch back on to use either.", - "radar.inbox.empty.openSchedule": "Turn on daily patrol", - "radar.inbox.empty.neverTitle": "Not patrolled yet", - "radar.inbox.empty.neverHint": "Daily patrol is on. You can also press Run now. This is not an empty inbox — the first round has not finished.", - "radar.inbox.empty.failedTitle": "The last patrol did not finish", - "radar.inbox.empty.noHitsTitle": "Search found no posts", - "radar.inbox.empty.noHitsHint": "It was empty before scoring: keywords too long, too product-named, or Threads returned nothing. Use 2–4 character pain words customers type.", - "radar.inbox.empty.editTerms": "Edit keywords", - "radar.inbox.empty.noFitTitle": "Patrol ran, but no matching pain points", - "radar.inbox.empty.noFitStats": "Search hits {hits}, judged {judged}, created {created}. Older posts may show under Last 7 days or All.", - "radar.inbox.empty.noFitHint": "New posts or product-fit demand will appear here. You can also check Last 7 days or All, or change keywords.", - "radar.inbox.msg.kept": "Kept. No CRM contact was created.", - "radar.inbox.msg.removed": "Discarded. You can restore it from Discarded.", - "radar.inbox.msg.restored": "Restored.", - "radar.inbox.msg.accepted": "Added to CRM. This step is optional — go there later if you want to follow up.", - "radar.inbox.msg.widened7d": "Not every patrol result was posted today. Switched to last 7 days ({n}). Job judge/create counts include rematches, so they may not all be new cards.", - "radar.inbox.msg.widenedAll": "No pending results in the last 7 days. Switched to all ({n}).", - "radar.inbox.msg.alreadyReviewed": "The {n} judged this round are already in Reviewed, so New is empty. Job numbers include rematched older posts.", - "radar.inbox.msg.waitingWorker": "Run now queued · waiting for a worker", - "radar.inbox.err.noActive": "No daily patrol is on. Set product keywords first, or resume a watch.", - "radar.inbox.err.noJob": "No patrol job was queued.", - "radar.inbox.msg.running": "Patrol is running… pain points appear here when it finishes.", - "radar.inbox.err.failed": "Patrol failed.", - "radar.inbox.err.cancelled": "Patrol was cancelled, so it is not marked complete. You can run it again.", - "radar.inbox.msg.queuedN": "Queued {n} patrols still running in the background. You can leave this page; results will stay here.", - "radar.inbox.msg.doneWithSummary": "{summary} Posts not from today also stay in the list.", - "radar.inbox.msg.done": "This patrol finished. Matching pain points are listed below.", - "radar.card.priority.high": "Follow first", - "radar.card.priority.review": "Worth a look", - "radar.card.priority.low": "Low priority", - "radar.card.fitProduct": "Fit · {label}", - "radar.card.noProduct": "No product match", - "radar.card.intent": "Intent {n}", - "radar.card.why": "Why recommended: ", - "radar.card.unknownAuthor": "Unknown author", - "radar.card.openOriginal": "View original on Threads", - "radar.card.actionsAria": "Opportunity actions", - "radar.card.keep": "Keep", - "radar.card.discard": "Discard", - "radar.card.whyBtn": "Why recommended", - "radar.card.accept": "Add to CRM (optional)", - "radar.card.busy": "Working…", - "radar.card.restore": "Restore", - "radar.card.accepted": "In CRM", - "radar.card.done": "Handled", - "radar.card.removeAria": "Mark as not a fit", - "radar.card.removeTitle": "Why discard?", - "radar.card.removeHint": "After you pick a reason it leaves New, and later patrols will not surface the same post. This does not use credits.", - "radar.card.reason": "Reason", - "radar.card.reason.pain_mismatch": "Does not match product pain", - "radar.card.reason.provider_or_ad": "Vendor / ad post", - "radar.card.reason.stale": "Demand is stale", - "radar.card.reason.already_solved": "Already solved", - "radar.card.reason.duplicate": "Duplicate", - "radar.card.reason.other": "Other", - "radar.card.note": "Notes", - "radar.card.duplicateHint": "In the detail drawer you can pick which original to keep.", - "radar.card.confirmRemove": "Discard", - "radar.drawer.aria": "Opportunity detail", - "radar.drawer.title": "Opportunity detail", - "radar.drawer.noProduct": "No product", - "radar.drawer.close": "Close", - "radar.drawer.closeAria": "Close opportunity detail", - "radar.drawer.intent": "Intent {n}", - "radar.drawer.priority": "Priority {n}", - "radar.drawer.evidence": "Demand evidence", - "radar.drawer.matches": "Product match and risks", - "radar.drawer.generic": "No product assigned. This stays as generic demand.", - "radar.drawer.judge": "Original judgment", - "radar.drawer.openOriginal": "Open original on Threads", - "radar.drawer.hint": "Read the pain and product reasons first. Keep or discard. Add to CRM only if you want to follow this person.", - "radar.sweep.aria": "Patrol funnel", - "radar.sweep.title": "Where this patrol stopped", - "radar.sweep.hint": "Merged matches are not counted as new opportunities.", - "radar.sweep.status.complete": "Done", - "radar.sweep.status.partial_budget": "Paused on budget", - "radar.sweep.status.blocked_budget": "Out of credits", - "radar.sweep.status.failed": "Failed", - "radar.sweep.hits": "Hits", - "radar.sweep.deduped": "Deduped", - "radar.sweep.prefilterPass": "Prefilter pass", - "radar.sweep.prefilterReject": "Prefilter out", - "radar.sweep.cached": "Cached judge", - "radar.sweep.aiJudge": "AI judge", - "radar.sweep.created": "New opportunities", - "radar.sweep.deferred": "Budget deferred", - "radar.sweep.credits": "Credits: search {search} · demand map {map} · judge {judge}", - "radar.sweep.total": "Total {n}", - "radar.sweep.budgetHint": "Unused candidates stay for the next run. Already-judged items are not charged again.", - "radar.cost.aria": "Credit preview", - "radar.cost.title": "Confirm credits before running", - "radar.cost.hint": "The preview itself is free. Credits are used only when the provider returns a result.", - "radar.cost.byok": "BYOK · 0 platform credits", - "radar.cost.platform": "Platform credits", - "radar.cost.fixed": "Fixed", - "radar.cost.range": "Estimated range", - "radar.cost.calls": "Search calls", - "radar.cost.remaining": "Remaining", - "radar.cost.until": "Preview until {time}", - "radar.cost.ceiling": "Max credits for this run", - "radar.cost.ceilingHint": "At least {min}, at most {max}", - "radar.cost.invalid": "The ceiling must be between the fixed cost and the estimated max.", - "radar.cost.confirm": "Confirm and run", - "radar.cost.starting": "Starting…", - "radar.readiness.title": "Product context completeness", - "radar.readiness.audience": "Audience", - "radar.readiness.context": "Context", - "radar.readiness.pain": "Pain", - "radar.readiness.capability": "Capability terms", - "radar.readiness.ok": "Filled", - "radar.readiness.todo": "Missing", - "radar.readiness.hint": "Thin context lowers fit confidence, but you can still create a product radar.", - "radar.match.why": "Why it fits", - "radar.match.hide": "Hide evidence", - "radar.match.basis": "Product basis: {text}", - "radar.match.risks": "Risks: {text}", - "radar.today.empty.goBrands": "Set brand and product", - "radar.today.introTitle": "Start with people worth following up, then decide how to reply", - "radar.today.introBody": "We match Threads posts to your product pains, merge duplicates, and rank by demand score.", - "radar.today.navAria": "Demand radar navigation", - "radar.today.manageWatches": "Manage daily patrol", - "radar.today.viewAll": "View all results", - "radar.today.filterAria": "Filter today's demand", - "radar.today.filterTitle": "Filter today's demand", - "radar.today.filterHint": "Start with everything; narrow by brand or product when the list gets long.", - "radar.today.fit": "Product fit", - "radar.today.allFit": "All fit levels", - "radar.today.fit.strong": "Strong fit", - "radar.today.fit.possible": "Possible", - "radar.today.fit.weak": "Weak fit", - "radar.today.needMore": "Not seeing a post you want?", - "radar.today.setupAria": "First-time demand radar setup", - "radar.today.setupTitle": "First time here — three steps", - "radar.today.setupHint": "After this, daily patrol runs automatically.", - "radar.today.setupStep": "Step {n} of 3", - "radar.today.setup.1.title": "Set brand and product", - "radar.today.setup.1.body": "Fill audience, pains, and product capabilities.", - "radar.today.setup.1.cta": "Go to settings", - "radar.today.setup.2.title": "Create a daily patrol", - "radar.today.setup.2.body": "Pick a product, then adopt suggested keywords.", - "radar.today.setup.2.cta": "Create patrol", - "radar.today.setup.3.title": "Come back and work the list", - "radar.today.setup.3.body": "Start with high scores, then review product evidence.", - "radar.today.productEyebrow": "Recommended product", - "radar.today.noPrimary": "No primary product yet", - "radar.today.fitScore": "Fit {n}", - "radar.today.overridden": "Manually set", - "radar.today.hideEvidence": "Hide product evidence", - "radar.today.showMatches": "View {n} product matches", - "radar.today.genericJudge": "No product set (generic demand scoring)", - "radar.today.msg.primarySet": "Primary product saved. Later high-score matches will not overwrite this.", - - "radar.primary.empty": "No product matches yet", - "radar.primary.label": "Primary product", - "radar.primary.placeholder": "Choose a product", - "radar.primary.needsReason": " · reason required", - "radar.primary.reasonAria": "Primary-product reason", - "radar.primary.reasonPh": "Optional: why this product this time", - "radar.primary.submit": "Set primary", - "radar.primary.defaultReason": "Chosen from product evidence", - - "radar.watches.needBrandProduct": "Pick a brand and product before enabling a product radar.", - "radar.watches.needDemandMap": "Finish the product demand map before starting a product patrol.", - "radar.watches.needBrandProductShort": "Pick a brand and product first.", - "radar.watches.assigned": "Product attached. You cannot swap the product on this watch later.", - "radar.watches.assigning": "Attaching…", - "radar.watches.assign": "Attach this product", - "radar.watches.productGone": "Product unavailable — create a new watch", - "radar.watches.pickBrand": "Pick a brand first", - "radar.watches.pickProduct": "Pick a product under this brand", - - "radar.explore.needProduct": "Pick a brand and product before exploring, so generic hits are not treated as product demand.", - "radar.explore.pickBrand": "Pick a brand", - "radar.explore.pickProduct": "Pick a product", - "radar.explore.productStats": "New demand {created} · merged matches {merged} · scored {matched}", - - "radar.import.needProduct": "Pick a brand and product before importing.", - - "radar.suggest.basis": "Based on: {text}", - - "radar.demand.title": "Product demand map", - "radar.demand.hint": "Confirm real product pains first, then use them to narrow patrol results. Source stays next to each term.", - "radar.demand.aria": "{label} demand map", - "radar.demand.ready": "Ready", - "radar.demand.incomplete": "Needs more", - "radar.demand.version": "Version {n}", - "radar.demand.loading": "Building the demand map…", - "radar.demand.pain": "User pains", - "radar.demand.painHint": "Problems the product solves, e.g. leaks or messy collaboration", - "radar.demand.scenario": "Use scenarios", - "radar.demand.scenarioHint": "How people describe the situation", - "radar.demand.outcome": "Desired outcomes", - "radar.demand.outcomeHint": "The result or improvement they want", - "radar.demand.solution": "Solution signals", - "radar.demand.solutionHint": "Words that show you can help", - "radar.demand.exclusion": "Exclusion signals", - "radar.demand.exclusionHint": "Hiring, ads, and other posts that should not become demand", - "radar.demand.custom": "Custom notes", - "radar.demand.customHint": "Custom notes do not overwrite product data; later product updates still keep the source.", - "radar.demand.save": "Save demand map", - "radar.demand.saving": "Saving…", - "radar.demand.origin.product": "Product", - "radar.demand.origin.ai": "AI suggestion", - "radar.demand.origin.user": "Manual", - "radar.demand.customBasis": "Manual note", - - "radar.query.aria": "Query plan preview", - "radar.query.title": "These short terms will be searched", - "radar.query.hint": "At most two words per group, CJK 2–4 characters each, so Threads can find them. Product names are only supporting context.", - "radar.query.meta": "Input {input} · map v{map}", - "radar.query.group": "Query group {n}", - "radar.query.basis": "Based on: {text}", - "radar.query.exclude": "Exclude: {text}", - "radar.query.adopt": "Use these queries", - "radar.query.empty": "The demand map does not have enough searchable pains or scenarios to build query groups yet.", - "radar.query.defaultBasis": "Product pain", - - "brands.loadFail": "Could not load brands. Try again later.", - "brands.deleteImpact": "This will pause {n} related demand watches. Historical demand and touch records stay.", - - "crm.board.touchProduct": " · Product: {label}", - "crm.board.primaryProduct": "Primary product: {label}", - "crm.board.primaryProductWithBrand": "Primary product: {label} ({brand})", - "crm.board.noProduct": "No product set", - - "utm.title": "UTM tracking links", - "utm.new": "New", - "utm.dest": "Destination URL", - "utm.label": "Label", - "utm.create": "Create", - "utm.empty": "No links yet", - "utm.track": "Track: {url}", - "utm.destLine": "Destination: {url}", - "utm.clicks": "Clicks: {n}", - "utm.copy": "Copy link", - "utm.created": "Tracking link created", - "utm.copied": "Tracking link copied", - "utm.copyFail": "Could not copy — select the URL manually", - - "tools.pain.title": "Pain-keyword generator", - "tools.pain.subtitle": "No login: describe the product to generate patrol terms and pains.", - "tools.pain.brief": "Product brief", - "tools.pain.audience": "Audience (optional)", - "tools.pain.run": "Generate keywords", - "tools.pain.running": "Generating…", - "tools.pain.result": "Result", - "tools.pain.keywords": "Keywords", - "tools.pain.pains": "Pains", - "tools.pain.scan": "Scan terms", - "tools.pain.login": "Sign in to patrol", - - "tools.style.title": "Style fingerprint quiz", - "tools.style.subtitle": "No login: paste a few Threads posts to see tone and rhythm.", - "tools.style.samples": "Post samples", - "tools.style.run": "Analyze", - "tools.style.running": "Analyzing…", - "tools.style.result": "Result", - "tools.style.tone": "Tone: {v}", - "tools.style.rhythm": "Rhythm: {v}", - "tools.style.hooks": "Hooks: {v}", - "tools.style.avoid": "Watch-outs: {v}", - "tools.style.login": "Sign in to Lapras", - - "inspire.source": "Source: {label}", - - "bench.title": "Benchmark", - "bench.reload": "Refresh", - "bench.sample": "Sample {n}", - "bench.median": "Median engagement {eng}% · median views {views}", - "bench.yours": "Your engagement {eng}% · avg views {views}", - "bench.insufficient": "Not enough sample", - - "insights.summaryTitle": "Last 3 months", - "insights.summaryStats": "Posts {posts} · views {views} · likes {likes} · replies {replies} · avg engagement {eng}%", - "insights.summaryTop": "Stronger posts", - "insights.topPostLine": "{eng}% · {views} views · {text}", - - "playbooks.title": "Playbook market", - "playbooks.allKinds": "All types", - "playbooks.kind.brief": "Patrol brief", - "playbooks.kind.persona": "Persona", - "playbooks.kind.play": "Reply play", - "playbooks.nichePh": "Niche (skincare / parenting…)", - "playbooks.mineOnly": "Mine only", - "playbooks.publish": "Publish template", - "playbooks.cancel": "Cancel", - "playbooks.publishCard": "Publish", - "playbooks.fieldTitle": "Title", - "playbooks.fieldNiche": "Niche", - "playbooks.fieldBody": "Content", - "playbooks.anonymous": "Anonymous", - "playbooks.submit": "Submit", - "playbooks.empty": "No templates yet", - "playbooks.imports": "Imported {n}", - "playbooks.import": "Import", - "playbooks.published": "Published", - "playbooks.imported": "Imported into your playbooks", - - "radar.import.open": "Manual import", - "radar.import.close": "Hide manual import", - "radar.import.title": "Manually import demand", - "radar.import.hint": "Paste a Threads/Facebook post URL and its text; it runs the same five-question judge. Arbitrary URLs are not fetched automatically, so paste the text yourself. Use this to top up today's list when volume is low.", - "radar.import.url": "Post URL", - "radar.import.text": "Post text", - "radar.import.author": "Author (optional)", - "radar.import.addRow": "+ Add row", - "radar.import.removeRow": "Remove", - "radar.import.submit": "Import", - "radar.import.submitting": "Importing…", - "radar.import.needRow": "Fill in at least one row with a URL and text", - "radar.import.csvOpen": "Paste CSV instead", - "radar.import.csvClose": "Hide CSV", - "radar.import.csvLabel": "Paste CSV", - "radar.import.csvHint": "Format: url,text,author (author optional). A header row with url/text is auto-detected; otherwise columns are read as url,text,author.", - "radar.import.csvApply": "Apply to rows below", - "radar.import.csvEmpty": "Couldn't parse any rows — check the format", - "radar.import.status.qualified": "Added to today's demand", - "radar.import.status.rejected": "Judged as not a fit (kept for reference)", - "radar.import.status.skipped": "Already imported — skipped", - "radar.import.status.failed": "Import failed", - - "radar.explore.open": "Explore now", - "radar.explore.close": "Hide explore", - "radar.explore.title": "Explore now", - "radar.explore.hint": "Search Threads right away for people looking for your service. Results go through the same five-question judge into today’s list. Max 2 words per query, 2–4 CJK chars each.", - "radar.explore.loadingSuggest": "Loading suggested terms…", - "radar.explore.suggestions": "Suggested short terms (click to add)", - "radar.explore.selected": "Selected terms", - "radar.explore.emptyTerms": "No terms yet — pick a suggestion or add your own.", - "radar.explore.removeChip": "Click to remove", - "radar.explore.addLabel": "Add a short term", - "radar.explore.addPh": "e.g. nanny recommend", - "radar.explore.add": "Add", - "radar.explore.run": "Start explore", - "radar.explore.running": "Exploring…", - "radar.explore.needTerms": "Pick at least one term", - "radar.explore.result": "Found {hits}, judged {judged}, added {created}", - "radar.explore.resultZeroHint": "Nothing new this run. Try more conversational short terms, or adjust daily watches.", - "radar.explore.resultTruncated": "{n} skipped by daily cap — try again tomorrow or upgrade.", - "radar.explore.termError.empty": "Enter a term", - "radar.explore.termError.tooLong": "At most 12 characters without spaces (long Threads queries often return nothing)", - "radar.explore.termError.tooManyTokens": "At most 2 words (space-separated)", - "radar.explore.termError.invalidToken": "No punctuation, emoji, AND/OR, or tokens that are too short/long", - "radar.explore.termError.duplicate": "Already added", - "radar.explore.termError.max": "At most 6 terms per run", - - "scout.promote": "Save to Demand", - "scout.promoted": "Copied into Today’s demand ({band} · {score}) — follow up under Demand", - "scout.promoteFail": "Could not save to Demand", - - "crm.board.title": "CRM board", - "crm.board.subtitle": "Seven stages plus follow-up. Contacts land here after you accept an opportunity.", - "crm.board.link.today": "Today's opportunities", - "crm.board.link.followups": "Follow-ups", - "crm.board.link.stats": "Conversion stats", - "crm.board.filters": "Contact filters", - "crm.board.search": "Search contacts", - "crm.board.searchPlaceholder": "Search name or Threads handle", - "crm.board.stageFilter": "Stage", - "crm.board.allStages": "All stages", - "crm.board.sort": "Sort", - "crm.board.sortRecent": "Recent touch first", - "crm.board.sortIntent": "Intent score first", - "crm.board.clearFilters": "Clear filters", - "crm.board.results": "{n} contacts", - "crm.board.noResults": "No matching contacts", - "crm.board.noResultsHint": "Try another name or handle, or clear the stage filter.", - "crm.board.empty": "No contacts yet", - "crm.board.emptyHint": "Accept an opportunity on the radar to add someone here.", - "crm.board.oppCount": "opportunities", - "crm.board.lastTouch": "Last touch {time}", - "crm.board.noTouch": "No touch time yet", - "crm.board.stage": "Current stage", - "crm.board.conversion": "Conversion", - "crm.board.amount": "Amount (optional)", - "crm.board.reportWon": "Report won", - "crm.board.notes": "Notes", - "crm.board.noteLabel": "Add a note", - "crm.board.addNote": "Save note", - "crm.board.timeline": "Timeline", - "crm.board.timelineEmpty": "No touches yet", - "crm.board.opps": "Related opportunities", - "crm.board.markFollowUp": "Mark follow-up", - "crm.board.clearFollowUp": "Clear follow-up", - "crm.board.msg.stage": "Stage updated", - "crm.board.msg.followUp": "Follow-up updated", - "crm.board.msg.won": "Conversion reported", - "crm.board.msg.note": "Note added", - "crm.board.deleteTitle": "Remove from working list", - "crm.board.deleteHint": "It disappears and reminders stop; opportunity, touch, and conversion history stays.", - "crm.board.delete": "Remove this contact", - "crm.board.deleting": "Removing…", - "crm.board.confirmDelete": "Remove “{name}” from the list? Existing opportunities, touches, and conversion history stay.", - "crm.board.msg.deleted": "Removed from the list", - - "crm.stage.new_found": "New", - "crm.stage.engaged": "Engaged", - "crm.stage.dm_sent": "DM sent", - "crm.stage.replied": "Replied", - "crm.stage.quoted": "Quoted", - "crm.stage.won": "Won", - "crm.stage.lost": "Lost", - "crm.stage.needs_follow_up": "Follow-up", - - "crm.followups.title": "Follow-ups", - "crm.followups.subtitle": "Due visits, snooze, and AI drafts.", - "crm.followups.link.board": "CRM board", - "crm.followups.link.stats": "Conversion stats", - "crm.followups.empty": "Nothing due", - "crm.followups.emptyHint": "Mark a contact for follow-up or send a reply to create one.", - "crm.followups.due": "Due", - "crm.followups.openContact": "Open contact", - "crm.followups.aiMessage": "AI follow-up draft", - "crm.followups.done": "Done", - "crm.followups.snooze": "Snooze 3 days", - "crm.followups.escalatedHint": "Notified twice with no action — consider marking lost.", - "crm.followups.status.scheduled": "Scheduled", - "crm.followups.status.notified": "Notified", - "crm.followups.status.done": "Done", - "crm.followups.status.snoozed": "Snoozed", - "crm.followups.status.escalated": "Escalated", - "crm.followups.msg.done": "Done", - "crm.followups.msg.snoozed": "Snoozed 3 days", - "crm.followups.msg.drafted": "Follow-up draft ready", - - "crm.stats.title": "Conversion stats", - "crm.stats.subtitle": "Terms, reply variants, and sources. No ranking when samples are thin.", - "crm.stats.terms": "Term conversion", - "crm.stats.variants": "Variant success", - "crm.stats.sources": "Win sources", - "crm.stats.emptyDim": "Not enough data yet", - "crm.stats.insufficient": "Insufficient sample", - "crm.stats.col.term": "Term", - "crm.stats.col.variant": "Variant", - "crm.stats.col.source": "Source", - "crm.stats.col.accepted": "Accepted", - "crm.stats.col.replied": "Replied", - "crm.stats.col.won": "Won", - "crm.stats.col.used": "Used", - "crm.stats.col.rate": "Rate", -}; - -const catalogs: Record = { - "zh-TW": zhTW, - en, -}; - -export function getCatalog(locale: AppLocale): MessageDict { - return catalogs[locale] || zhTW; +export function isCatalogLoaded(locale: AppLocale): boolean { + return Boolean(catalogs[locale]); } -export function translate( - locale: AppLocale, +/** 載入並註冊語系字典;重複呼叫共用同一個 in-flight request。 */ +export function ensureCatalog(locale: AppLocale): Promise { + const loaded = catalogs[locale]; + if (loaded) return Promise.resolve(loaded); + const running = inflight.get(locale); + if (running) return running; + const task = loaders[locale]() + .then((dict) => { + catalogs[locale] = dict; + return dict; + }) + .finally(() => inflight.delete(locale)); + inflight.set(locale, task); + return task; +} + +export function getCatalog(locale: AppLocale): MessageDict { + return catalogs[locale] ?? zhTW; +} + +/** 對已取得的字典取字;缺字回退預設語系,再回退 key 本身。 */ +export function formatMessage( + dict: MessageDict, key: string, params?: Record, ): string { - const dict = getCatalog(locale); let s = dict[key] ?? zhTW[key] ?? key; if (params) { for (const [k, v] of Object.entries(params)) { @@ -5826,3 +54,11 @@ export function translate( } return s; } + +export function translate( + locale: AppLocale, + key: string, + params?: Record, +): string { + return formatMessage(getCatalog(locale), key, params); +} diff --git a/apps/web/src/lib/i18n/types.ts b/apps/web/src/lib/i18n/types.ts index 62a7cd1..ad23e8b 100644 --- a/apps/web/src/lib/i18n/types.ts +++ b/apps/web/src/lib/i18n/types.ts @@ -1,5 +1,8 @@ export type AppLocale = "zh-TW" | "en"; +/** 扁平 key → 字串;{name} 可插值 */ +export type MessageDict = Record; + export type AppCurrency = "TWD" | "USD" | "JPY" | "EUR" | "HKD"; export type LocaleMeta = { diff --git a/apps/web/src/lib/pageHelp.catalog.test.ts b/apps/web/src/lib/pageHelp.catalog.test.ts index 4c84737..18785d5 100644 --- a/apps/web/src/lib/pageHelp.catalog.test.ts +++ b/apps/web/src/lib/pageHelp.catalog.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from "vitest"; -import { translate } from "./i18n/messages"; +import { beforeAll, describe, expect, it } from "vitest"; +import { ensureCatalog, translate } from "./i18n/messages"; import type { AppLocale } from "./i18n/types"; import { pageHelpKeys, type PageHelpId } from "./pageHelp"; @@ -35,6 +35,11 @@ const ALL_IDS: PageHelpId[] = [ const LOCALES: AppLocale[] = ["zh-TW", "en"]; describe("page help catalog completeness", () => { + // 非預設語系是動態載入的;不先載入就只會驗到 zh-TW 後備字串 + beforeAll(async () => { + await Promise.all(LOCALES.map((locale) => ensureCatalog(locale))); + }); + for (const locale of LOCALES) { for (const id of ALL_IDS) { it(`${locale} has full copy for ${id}`, () => { diff --git a/apps/web/src/lib/reviewCopy.test.ts b/apps/web/src/lib/reviewCopy.test.ts index cbe772d..d00db16 100644 --- a/apps/web/src/lib/reviewCopy.test.ts +++ b/apps/web/src/lib/reviewCopy.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "vitest"; -import { en, zhTW } from "./i18n/messages"; +import { en } from "./i18n/catalog.en"; +import { zhTW } from "./i18n/catalog.zhTW"; import { sanitizeReviewCopy } from "./reviewCopy"; const BANNED = /爬蟲|crawler|\bcrawl(?:ing|ed)?\b/i; diff --git a/apps/web/src/pages/CrmStatsPage.tsx b/apps/web/src/pages/CrmStatsPage.tsx index cd65ce7..7b607b7 100644 --- a/apps/web/src/pages/CrmStatsPage.tsx +++ b/apps/web/src/pages/CrmStatsPage.tsx @@ -19,6 +19,8 @@ export function CrmStatsPage() { const [stats, setStats] = useState(null); const [err, setErr] = useState(null); const [loading, setLoading] = useState(true); + // 後端未實作的維度要與「還沒有資料」分開顯示,否則會誤導使用者 + const unavailable = new Set(stats?.unavailable_dimensions ?? []); const load = useCallback(async () => { setStats(await repos.crm.getStats()); @@ -60,7 +62,9 @@ export function CrmStatsPage() {

{t("crm.stats.terms")}

- {stats.terms.length === 0 ? ( + {unavailable.has("terms") ? ( + + ) : stats.terms.length === 0 ? ( ) : ( @@ -96,7 +100,9 @@ export function CrmStatsPage() {

{t("crm.stats.variants")}

- {stats.variants.length === 0 ? ( + {unavailable.has("variants") ? ( + + ) : stats.variants.length === 0 ? ( ) : (
diff --git a/apps/web/src/pages/ScoutPage.test.tsx b/apps/web/src/pages/ScoutPage.test.tsx index f10288c..1bb13b4 100644 --- a/apps/web/src/pages/ScoutPage.test.tsx +++ b/apps/web/src/pages/ScoutPage.test.tsx @@ -1,10 +1,10 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { Repos } from "../data/repos"; import type { Job, ScoutHomeworkRecord, ScoutRun, ScoutRunBrief } from "../domain/types"; import { I18nProvider } from "../i18n/I18nContext"; -import { translate } from "../lib/i18n/messages"; +import { ensureCatalog, translate } from "../lib/i18n/messages"; import { scoutPostFixture } from "../test/scoutFixtures"; import { ScoutPage } from "./ScoutPage"; @@ -125,6 +125,11 @@ function renderPage() { } describe("ScoutPage baseline harness", () => { + // 英文字典是動態載入的,斷言前先確保它到位 + beforeAll(async () => { + await ensureCatalog("en"); + }); + beforeEach(() => { harness.failLoad = false; harness.repos = buildRepos(); diff --git a/apps/web/src/pages/publicPages.test.tsx b/apps/web/src/pages/publicPages.test.tsx index f71213d..91fbc6f 100644 --- a/apps/web/src/pages/publicPages.test.tsx +++ b/apps/web/src/pages/publicPages.test.tsx @@ -1,13 +1,13 @@ import { fireEvent, render, screen, within } from "@testing-library/react"; import { MemoryRouter, Route, Routes } from "react-router-dom"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { KEYS } from "../data/mock/keys"; import { I18nProvider } from "../i18n/I18nContext"; import { DataDeletionPage } from "./DataDeletionPage"; import { HomePage } from "./HomePage"; import { PrivacyPage } from "./PrivacyPage"; import { TermsPage } from "./TermsPage"; -import { translate } from "../lib/i18n/messages"; +import { ensureCatalog, translate } from "../lib/i18n/messages"; import { PLANS } from "../lib/usageMeter"; const authState = vi.hoisted(() => ({ @@ -71,6 +71,11 @@ const PRIVACY_SECTION_IDS = [ ] as const; describe("public intro homepage", () => { + // 切換語系的斷言會用到英文字典,動態載入需先等它到位 + beforeAll(async () => { + await ensureCatalog("en"); + }); + beforeEach(() => { authState.member = null; authState.loading = false;