OPE-296: Add linear integration for starting sessions from issues (#3235)

* feat(linear): start sessions from Linear issues
Authorize a Linear workspace on this OpenChamber server, map teams to
projects, attach an issue from chat, start a session or worktree from an
issue, and post started/completed/failed comments that open the session.
Hidden in VS Code.

* feat(linear): connect more than one Linear workspace

Store each OAuth grant on this OpenChamber server and keep one current, so Settings can add and switch workspaces without dropping the others. Project mapping is per workspace. Remove the Linear button next to New Chat; start-from-issue stays on New Worktree.

* feat(linear): add a right-hand issues panel

Browse and filter issues in the rail, open a card to change status or start a session, and collapse search plus most filters to icons on a narrow panel.

* feat(linear): open issues in the rail and filter by Linear status

The rail icon only shows after Linear is connected. Clicking a Linear row on work status opens the panel. Status options match the card, including Done, Canceled, and Duplicate. The Integrations experimental warning sits under Third-party integrations.

* fix(linear): use stable OAuth callback broker

* fix(chat): preview Linear issue attachments

The context switch missed linear-issue, so tsc treated the preview helpers as incomplete.

* fix(ui): restore Linear i18n parity and the #2903 sync harness

Turkish was missing the Linear dictionaries, and the subagent test still wrapped only SyncContext after reads moved to SyncRuntimeContext.

* fix(linear): drop changelog hunks and close review races

Keep changelogs out of this PR, restore CodeMirror ranges, ignore stale Linear list pages, and leave a persisted Linear tab open until auth has actually resolved.

* fix(linear): tint active issue filters and clear them in one click

* fix(markdown): read escaped brackets as text, not display math

`\[...\]` is display math in LaTeX and an escaped bracket pair in
CommonMark. The block tokenizer claimed every `\[`, so prose like
`[title \[Bug\] more](url)` was handed to KaTeX: "Bug" rendered as a
centered formula and the block token split the paragraph, tearing the
link into three pieces. Linear, GitHub and any other source that escapes
brackets the way CommonMark requires hit this.

Display math now has to own its line — `\[` starts one and `\]` ends
one. A formula on its own line still renders; `\[` mid-sentence stays an
escape, which is what CommonMark says it is and what prose almost always
means. Inline `\(...\)` keeps the same ambiguity, but inline math is
legitimately mid-sentence, so there is no position to judge it by.

Covered by regression tests, including the verbatim comment body that
surfaced this.

* feat(linear): make session status comments opt-in and public-only

A status comment lands in a Linear workspace the whole team reads, and
the link it carried pointed at whatever origin started the session —
usually loopback or a LAN address. Everyone but its author got a dead
link, and nobody had agreed to the comments in the first place.

Comments are now off until the user turns them on in Settings ->
Integrations -> Linear, and the check lives on the server: the event hub
posts completed and failure without going through the interface, so a
client-side gate would not hold. When the resolved origin is not
publicly reachable the server posts nothing at all rather than a link
only its author can open; `isPublicSessionOrigin` rejects loopback,
private LAN, carrier-grade NAT, link-local and single-label hosts. The
desktop deep-link origin is gone with it, since no one else can follow
one either.

The comment body also dropped the session title. It repeated the issue
the comment already sits on, and issue titles routinely carry brackets
("[Bug] ...") that broke the markdown link. The body is now one short
link, and `sessionTitle` is gone from the route, client and types.

Also caps the dedupe file at the newest 500 sessions; it grew forever.

* fix(linear): match the pull request panel and clear review findings

Comments in the Linear panel now render as the same avatar timeline the
pull request panel uses, with the shared time-format preference instead
of a raw locale string. Comment authors carry `avatarUrl`, which the
GraphQL selection was not requesting.

Review findings from the same pass:

- `status-runtime.js` hand-rolled `typeof` narrowing and failed the
  vendored anti-slop lint; it now parses through `parse.js` like every
  other file in the module.
- `useLinearAuthStore` turned any failed request into `connected: false`
  with `hasChecked: true`. Since the rail icon, the composer entry and
  the worktree option all gate on `connected === true`, one network blip
  hid Linear for the rest of the session, and Settings only re-checked
  when it had never checked. It now keeps the last known status and
  leaves `hasChecked` false so the next caller retries.
- `LinearIssuesView` (1096 lines) was a static import in `ContextPanel`,
  shipping in the main bundle although its rail icon stays hidden until
  a workspace is connected. It is lazy now, like `GitView`.
- Dropped dead code: the unused port helpers left over from the loopback
  callback, two re-exported default values nothing read, and a redundant
  export in `linkedIssues`.
- Integrations is no longer badged beta.
This commit is contained in:
Alex Kutas
2026-08-30 02:18:40 +03:00
committed by GitHub
parent 1fdb78dbbe
commit 49f0a9e62f
118 changed files with 11888 additions and 143 deletions
+194
View File
@@ -1145,6 +1145,199 @@ export type GitHubDeviceFlowComplete =
| { connected: true; user: GitHubUserSummary; scope?: string }
| { connected: false; status?: string; error?: string };
export type LinearUserSummary = {
id: string;
name: string | null;
displayName: string | null;
email: string | null;
avatarUrl: string | null;
};
export type LinearOrganizationSummary = {
id: string;
name: string;
urlKey: string | null;
};
export type LinearWorkspaceSummary = {
id: string;
name: string | null;
urlKey: string | null;
current: boolean;
user?: LinearUserSummary | null;
authorizedAt?: number | null;
};
export type LinearAuthStatus = {
connected: boolean;
user?: LinearUserSummary | null;
organization?: LinearOrganizationSummary | null;
scope?: string;
workspaces?: LinearWorkspaceSummary[];
};
export type LinearAuthStart = {
authorizationUrl: string;
expiresIn: number;
scope: string;
};
export type LinearAuthOrigin = 'desktop' | 'web';
export type LinearIssueState = {
id: string | null;
name: string | null;
type: string | null;
};
export type LinearWorkflowState = {
id: string;
name: string;
type: string | null;
position: number;
};
export type LinearIssueAssignee = {
name: string | null;
displayName: string | null;
avatarUrl: string | null;
};
export type LinearIssueTeam = {
id: string;
key: string;
name: string;
};
export type LinearIssuePriority = 0 | 1 | 2 | 3 | 4;
export type LinearIssueLabel = {
id: string;
name: string;
color: string | null;
};
export type LinearIssueSummary = {
id: string;
identifier: string;
title: string;
url: string;
state?: LinearIssueState | null;
assignee?: LinearIssueAssignee | null;
team?: LinearIssueTeam | null;
priority?: LinearIssuePriority | null;
labels?: LinearIssueLabel[];
};
export type LinearIssueComment = {
id: string;
body: string;
createdAt: string | null;
user?: { name: string | null; displayName: string | null; avatarUrl?: string | null } | null;
};
export type LinearIssue = LinearIssueSummary & {
description?: string | null;
comments?: LinearIssueComment[];
};
export type LinearIssueListStatus = 'all' | 'backlog' | 'todo' | 'started' | 'inReview' | 'completed' | 'canceled' | 'duplicate';
export type LinearIssueListAssignee = 'any' | 'me';
export type LinearIssueListPriority = 'all' | 'none' | 'urgent' | 'high' | 'medium' | 'low';
export type LinearIssuesListOptions = {
query?: string;
cursor?: string;
status?: LinearIssueListStatus;
assignee?: LinearIssueListAssignee;
teamId?: string;
priority?: LinearIssueListPriority;
};
export type LinearIssuesListResult = {
connected: boolean;
issues?: LinearIssueSummary[];
cursor?: string | null;
hasMore?: boolean;
};
export type LinearIssueGetResult = {
connected: boolean;
issue?: LinearIssue | null;
};
export type LinearIssueStatesResult = {
connected: boolean;
states?: LinearWorkflowState[];
};
export type LinearIssueUpdateInput = {
id: string;
stateId: string;
};
export type LinearIssueUpdateResult = {
connected: boolean;
issue?: LinearIssue | null;
};
export type LinearTeamMapping = {
id: string;
key: string;
name: string;
projectPath: string | null;
};
export type LinearMappingResult = {
connected: boolean;
defaultProjectPath?: string | null;
teams?: LinearTeamMapping[];
};
export type LinearMappingWrite = {
defaultProjectPath: string | null;
teamProjectPaths: { [teamId: string]: string };
};
export type LinearSessionStatusKind = 'started' | 'completed' | 'failure';
export type LinearSessionStatusPostInput = {
kind: LinearSessionStatusKind;
sessionId: string;
issueIdentifier?: string;
sessionOrigin?: string;
};
export type LinearSessionStatusPostResult =
| { connected: false }
| { connected: true; posted: true; commentId: string | null }
| {
connected: true;
posted: false;
skipped: 'already-posted' | 'issue-not-found' | 'not-started' | 'disabled' | 'origin-not-public';
};
export type LinearPreferences = {
/** Status comments are off until the user opts in. */
sessionComments: boolean;
};
export interface LinearAPI {
authStatus(): Promise<LinearAuthStatus>;
authStart(origin?: LinearAuthOrigin): Promise<LinearAuthStart>;
authDisconnect(): Promise<{ removed: boolean }>;
authActivate(organizationId: string): Promise<LinearAuthStatus>;
issuesList(options?: LinearIssuesListOptions): Promise<LinearIssuesListResult>;
issueGet(id: string): Promise<LinearIssueGetResult>;
issueStates(teamId: string): Promise<LinearIssueStatesResult>;
issueUpdate(input: LinearIssueUpdateInput): Promise<LinearIssueUpdateResult>;
mappingGet(): Promise<LinearMappingResult>;
mappingSet(mapping: LinearMappingWrite): Promise<LinearMappingResult>;
sessionStatusPost(input: LinearSessionStatusPostInput): Promise<LinearSessionStatusPostResult>;
preferencesGet(): Promise<LinearPreferences>;
preferencesSet(preferences: LinearPreferences): Promise<LinearPreferences>;
}
export interface GitHubAPI {
authStatus(): Promise<GitHubAuthStatus>;
authStart(): Promise<GitHubDeviceFlowStart>;
@@ -1269,6 +1462,7 @@ export interface RuntimeAPIs {
permissions: PermissionsAPI;
notifications: NotificationsAPI;
github?: GitHubAPI;
linear?: LinearAPI;
push?: PushAPI;
diagnostics?: DiagnosticsAPI;
clientAuth?: ClientAuthAPI;
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go Nutzungsverfolgung',
@@ -2218,5 +2219,6 @@ export const settingsDict = {
'settings.openchamber.visual.option.themeMode.light.description': 'Immer helles Erscheinungsbild verwenden',
'settings.openchamber.visual.option.themeMode.dark.description': 'Immer dunkles Erscheinungsbild verwenden',
'chat.message.userText.collapseAria': 'Benutzernachricht einklappen',
...linearIntegrationI18n.de,
...thirdPartyIntegrationI18n.de,
};
+4
View File
@@ -1,7 +1,11 @@
import { settingsDict } from './de.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict = {
...settingsDict,
...linearIssuePickerI18n.de,
...linearPanelI18n.de,
'common.language.german': 'Deutsch',
'common.loading': 'Wird geladen...',
'common.unavailable': 'Nicht verfügbar',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go usage tracking',
@@ -2217,5 +2218,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...linearIntegrationI18n.en,
...thirdPartyIntegrationI18n.en,
} as const;
+4
View File
@@ -1,7 +1,11 @@
import { settingsDict } from './en.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict = {
...settingsDict,
...linearIssuePickerI18n.en,
...linearPanelI18n.en,
'terminalView.actions.attachSelection': 'Attach selected output',
'terminalView.actions.restart': 'Restart terminal',
'chat.message.terminalContext': '{terminal}, lines {start}-{end}',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Seguimiento de uso de OpenCode Go',
@@ -2227,5 +2228,6 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
...linearIntegrationI18n.es,
...thirdPartyIntegrationI18n.es,
} as const;
+4
View File
@@ -1,8 +1,12 @@
import type { I18nKey } from './en';
import { settingsDict } from './es.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict: Record<I18nKey, string> = {
...settingsDict,
...linearIssuePickerI18n.es,
...linearPanelI18n.es,
'terminalView.actions.attachSelection': 'Adjuntar salida seleccionada',
'terminalView.actions.restart': 'Reiniciar terminal',
'chat.message.terminalContext': '{terminal}, líneas {start}-{end}',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Suivi de lutilisation dOpenCode Go',
@@ -2227,5 +2228,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...linearIntegrationI18n.fr,
...thirdPartyIntegrationI18n.fr,
} as const;
+4
View File
@@ -1,7 +1,11 @@
import { settingsDict } from './fr.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict = {
...settingsDict,
...linearIssuePickerI18n.fr,
...linearPanelI18n.fr,
'terminalView.actions.attachSelection': 'Joindre la sortie sélectionnée',
'terminalView.actions.restart': 'Redémarrer le terminal',
'chat.message.terminalContext': '{terminal}, lignes {start}-{end}',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go 使用量追跡',
@@ -2227,5 +2228,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'エージェントが応答している間にフォローアップメッセージで Enter を押したときの動作を選択します。',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'ステア',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'キュー',
...linearIntegrationI18n.ja,
...thirdPartyIntegrationI18n.ja,
} as const;
+4
View File
@@ -1,8 +1,12 @@
import type { I18nKey } from './en';
import { settingsDict } from './ja.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict: Record<I18nKey, string> = {
...settingsDict,
...linearIssuePickerI18n.ja,
...linearPanelI18n.ja,
'terminalView.actions.attachSelection': '選択した出力を添付',
'terminalView.actions.restart': 'ターミナルを再起動',
'chat.message.terminalContext': '{terminal}、{start}〜{end}行',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go 사용량 추적',
@@ -2227,5 +2228,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...linearIntegrationI18n.ko,
...thirdPartyIntegrationI18n.ko,
} as const;
+4
View File
@@ -1,8 +1,12 @@
import type { I18nKey } from './en';
import { settingsDict } from './ko.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict: Record<I18nKey, string> = {
...settingsDict,
...linearIssuePickerI18n.ko,
...linearPanelI18n.ko,
'terminalView.actions.attachSelection': '선택한 출력 첨부',
'terminalView.actions.restart': '터미널 다시 시작',
'chat.message.terminalContext': '{terminal}, {start}-{end}행',
@@ -0,0 +1,71 @@
import { describe, expect, test } from 'bun:test';
import { linearIntegrationI18n } from './linear-integration.i18n';
const locales = ['en', 'de', 'fr', 'es', 'ja', 'pt-BR', 'uk', 'ko', 'pl', 'zh-CN', 'zh-TW', 'tr'] as const;
const requiredKeys = [
'settings.integrations.firstParty.title',
'settings.integrations.firstParty.info',
'settings.integrations.linear.title',
'settings.integrations.linear.description',
'settings.integrations.linear.info',
'settings.integrations.linear.status.notConnected',
'settings.integrations.linear.status.connected',
'settings.integrations.linear.status.waiting',
'settings.integrations.linear.actions.connect',
'settings.integrations.linear.actions.disconnect',
'settings.integrations.linear.actions.addWorkspace',
'settings.integrations.linear.actions.switchTo',
'settings.integrations.linear.label.otherWorkspaces',
'settings.integrations.linear.flow.title',
'settings.integrations.linear.flow.description',
'settings.integrations.linear.flow.waiting',
'settings.integrations.linear.toast.connected',
'settings.integrations.linear.toast.disconnected',
'settings.integrations.linear.toast.workspaceSwitched',
'settings.integrations.linear.toast.workspaceSwitchFailed',
'settings.integrations.linear.toast.startConnectFailed',
'settings.integrations.linear.toast.disconnectFailed',
'settings.integrations.linear.toast.authorizationFailed',
'settings.integrations.linear.avatarAlt.withName',
'settings.integrations.linear.avatarAlt.fallback',
'settings.integrations.linear.label.unknownUser',
'settings.integrations.linear.mapping.defaultProject',
'settings.integrations.linear.mapping.defaultProject.info',
'settings.integrations.linear.mapping.defaultProject.placeholder',
'settings.integrations.linear.mapping.defaultProject.aria',
'settings.integrations.linear.mapping.teams',
'settings.integrations.linear.mapping.teams.info',
'settings.integrations.linear.mapping.teams.useDefault',
'settings.integrations.linear.mapping.teams.aria',
'settings.integrations.linear.mapping.emptyProjects',
'settings.integrations.linear.mapping.emptyTeams',
'settings.integrations.linear.mapping.loadFailed',
'settings.integrations.linear.sessionComments.label',
'settings.integrations.linear.sessionComments.info',
'settings.integrations.linear.sessionComments.aria',
'settings.integrations.linear.sessionComments.loadFailed',
'settings.magicPrompts.sidebar.group.linear',
'settings.magicPrompts.sidebar.item.linearIssueReview',
'settings.magicPrompts.page.group.linearIssueReview.title',
'settings.magicPrompts.page.group.linearIssueReview.description',
] as const;
describe('linear integration translations', () => {
test('provides every required key in every supported locale', () => {
const english = linearIntegrationI18n.en;
for (const locale of locales) {
for (const key of requiredKeys) {
const value = linearIntegrationI18n[locale][key];
expect(value).toBeTruthy();
if (
locale !== 'en'
&& key !== 'settings.integrations.linear.title'
&& key !== 'settings.magicPrompts.sidebar.group.linear'
) {
expect(value).not.toBe(english[key]);
}
}
}
});
});
@@ -0,0 +1,567 @@
/** Linear first-party integration settings strings — merged into each locale's settings dictionary. */
export const linearIntegrationI18n = {
en: {
'settings.integrations.firstParty.title': 'Built-in integrations',
'settings.integrations.firstParty.info': 'Sign-ins for services that ship with OpenChamber. The login stays on this computer so web, desktop, and a paired phone share it.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'Connect Linear workspaces on this OpenChamber server.',
'settings.integrations.linear.info': 'Connect one or more Linear workspaces. OpenChamber stores the logins on this computer so web, desktop, and a paired phone share them.',
'settings.integrations.linear.status.notConnected': 'Not connected',
'settings.integrations.linear.status.connected': 'Connected',
'settings.integrations.linear.status.waiting': 'Waiting',
'settings.integrations.linear.actions.connect': 'Connect',
'settings.integrations.linear.actions.disconnect': 'Disconnect',
'settings.integrations.linear.actions.addWorkspace': 'Add workspace',
'settings.integrations.linear.actions.switchTo': 'Switch to',
'settings.integrations.linear.label.otherWorkspaces': 'Other workspaces',
'settings.integrations.linear.flow.title': 'Waiting for Linear',
'settings.integrations.linear.flow.description': 'Finish signing in in the browser tab that just opened.',
'settings.integrations.linear.flow.waiting': 'Waiting for authorization…',
'settings.integrations.linear.toast.connected': 'Linear connected',
'settings.integrations.linear.toast.disconnected': 'Linear disconnected',
'settings.integrations.linear.toast.workspaceSwitched': 'Switched Linear workspace',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Could not switch Linear workspace',
'settings.integrations.linear.toast.startConnectFailed': 'Could not start Linear sign-in',
'settings.integrations.linear.toast.disconnectFailed': 'Could not disconnect Linear',
'settings.integrations.linear.toast.authorizationFailed': 'Linear authorization timed out. Click Connect to try again.',
'settings.integrations.linear.avatarAlt.withName': 'Linear avatar for {name}',
'settings.integrations.linear.avatarAlt.fallback': 'Linear avatar',
'settings.integrations.linear.label.unknownUser': 'Unknown user',
'settings.integrations.linear.mapping.defaultProject': 'Default project',
'settings.integrations.linear.mapping.defaultProject.info': 'New sessions from Linear issues use this project unless the issue\'s team has its own mapping.',
'settings.integrations.linear.mapping.defaultProject.placeholder': 'None',
'settings.integrations.linear.mapping.defaultProject.aria': 'Default project for Linear issues',
'settings.integrations.linear.mapping.teams': 'Team projects',
'settings.integrations.linear.mapping.teams.info': 'Optional. An issue from a mapped team opens in that project instead of the default.',
'settings.integrations.linear.mapping.teams.useDefault': 'Use default',
'settings.integrations.linear.mapping.teams.aria': 'Project for Linear team {team}',
'settings.integrations.linear.mapping.emptyProjects': 'Add a project first, then map Linear teams to it.',
'settings.integrations.linear.mapping.emptyTeams': 'This Linear workspace has no teams.',
'settings.integrations.linear.mapping.loadFailed': 'Could not load Linear project mapping.',
'settings.integrations.linear.sessionComments.label': 'Session comments',
'settings.integrations.linear.sessionComments.info': 'Adds a comment to the issue when a session starts, finishes, or fails. Comments are only posted when this server has a public address, so the link opens the session for everyone on the issue.',
'settings.integrations.linear.sessionComments.aria': 'Post session status comments to Linear',
'settings.integrations.linear.sessionComments.loadFailed': 'Could not load Linear comment settings.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue Review',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue Review',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts used when starting a session from a Linear issue: visible user message + hidden instructions.',
},
de: {
'settings.integrations.firstParty.title': 'Eingebaute Integrationen',
'settings.integrations.firstParty.info': 'Anmeldungen für Dienste, die mit OpenChamber mitgeliefert werden. Die Anmeldung bleibt auf diesem Computer, damit Web, Desktop und ein gekoppeltes Telefon sie teilen.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'Verbinde Linear-Workspaces mit diesem OpenChamber-Server.',
'settings.integrations.linear.info': 'Verbinde einen oder mehrere Linear-Workspaces. OpenChamber speichert die Anmeldungen auf diesem Computer, damit Web, Desktop und ein gekoppeltes Telefon sie teilen.',
'settings.integrations.linear.status.notConnected': 'Nicht verbunden',
'settings.integrations.linear.status.connected': 'Verbunden',
'settings.integrations.linear.status.waiting': 'Warten',
'settings.integrations.linear.actions.connect': 'Verbinden',
'settings.integrations.linear.actions.disconnect': 'Trennen',
'settings.integrations.linear.actions.addWorkspace': 'Workspace hinzufügen',
'settings.integrations.linear.actions.switchTo': 'Wechseln zu',
'settings.integrations.linear.label.otherWorkspaces': 'Andere Workspaces',
'settings.integrations.linear.flow.title': 'Warte auf Linear',
'settings.integrations.linear.flow.description': 'Schließe die Anmeldung im gerade geöffneten Browser-Tab ab.',
'settings.integrations.linear.flow.waiting': 'Warte auf die Autorisierung…',
'settings.integrations.linear.toast.connected': 'Linear verbunden',
'settings.integrations.linear.toast.disconnected': 'Linear getrennt',
'settings.integrations.linear.toast.workspaceSwitched': 'Linear-Workspace gewechselt',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Linear-Workspace konnte nicht gewechselt werden',
'settings.integrations.linear.toast.startConnectFailed': 'Linear-Anmeldung konnte nicht gestartet werden',
'settings.integrations.linear.toast.disconnectFailed': 'Linear konnte nicht getrennt werden',
'settings.integrations.linear.toast.authorizationFailed': 'Die Linear-Autorisierung ist abgelaufen. Klicke auf Verbinden, um es erneut zu versuchen.',
'settings.integrations.linear.avatarAlt.withName': 'Linear-Avatar für {name}',
'settings.integrations.linear.avatarAlt.fallback': 'Linear-Avatar',
'settings.integrations.linear.label.unknownUser': 'Unbekannter Benutzer',
'settings.integrations.linear.mapping.defaultProject': 'Standardprojekt',
'settings.integrations.linear.mapping.defaultProject.info': 'Neue Sitzungen aus Linear-Issues nutzen dieses Projekt, sofern das Team des Issues keine eigene Zuordnung hat.',
'settings.integrations.linear.mapping.defaultProject.placeholder': 'Keines',
'settings.integrations.linear.mapping.defaultProject.aria': 'Standardprojekt für Linear-Issues',
'settings.integrations.linear.mapping.teams': 'Team-Projekte',
'settings.integrations.linear.mapping.teams.info': 'Optional. Ein Issue eines zugeordneten Teams öffnet sich in diesem Projekt statt im Standard.',
'settings.integrations.linear.mapping.teams.useDefault': 'Standard verwenden',
'settings.integrations.linear.mapping.teams.aria': 'Projekt für Linear-Team {team}',
'settings.integrations.linear.mapping.emptyProjects': 'Füge zuerst ein Projekt hinzu und ordne dann Linear-Teams zu.',
'settings.integrations.linear.mapping.emptyTeams': 'Dieser Linear-Workspace hat keine Teams.',
'settings.integrations.linear.mapping.loadFailed': 'Linear-Projektzuordnung konnte nicht geladen werden.',
'settings.integrations.linear.sessionComments.label': 'Sitzungskommentare',
'settings.integrations.linear.sessionComments.info': 'Kommentiert das Issue, wenn eine Sitzung startet, endet oder fehlschlägt. Kommentare werden nur gepostet, wenn dieser Server eine öffentliche Adresse hat, damit der Link die Sitzung für alle Beteiligten öffnet.',
'settings.integrations.linear.sessionComments.aria': 'Statuskommentare zu Sitzungen in Linear posten',
'settings.integrations.linear.sessionComments.loadFailed': 'Linear-Kommentareinstellungen konnten nicht geladen werden.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue-Review',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue-Review',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Eingabeaufforderungen beim Start einer Sitzung aus einem Linear-Issue: sichtbare Benutzernachricht + versteckte Anweisungen.',
},
fr: {
'settings.integrations.firstParty.title': 'Intégrations natives',
'settings.integrations.firstParty.info': 'Connexions aux services fournis avec OpenChamber. La connexion reste sur cet ordinateur pour que le web, le bureau et un téléphone apparié la partagent.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'Connectez des espaces Linear à ce serveur OpenChamber.',
'settings.integrations.linear.info': 'Connectez un ou plusieurs espaces Linear. OpenChamber enregistre les connexions sur cet ordinateur pour que le web, le bureau et un téléphone apparié les partagent.',
'settings.integrations.linear.status.notConnected': 'Non connecté',
'settings.integrations.linear.status.connected': 'Connecté',
'settings.integrations.linear.status.waiting': 'En attente',
'settings.integrations.linear.actions.connect': 'Connecter',
'settings.integrations.linear.actions.disconnect': 'Déconnecter',
'settings.integrations.linear.actions.addWorkspace': 'Ajouter un workspace',
'settings.integrations.linear.actions.switchTo': 'Basculer vers',
'settings.integrations.linear.label.otherWorkspaces': 'Autres workspaces',
'settings.integrations.linear.flow.title': 'En attente de Linear',
'settings.integrations.linear.flow.description': 'Terminez la connexion dans longlet du navigateur qui vient de souvrir.',
'settings.integrations.linear.flow.waiting': 'En attente de lautorisation…',
'settings.integrations.linear.toast.connected': 'Linear connecté',
'settings.integrations.linear.toast.disconnected': 'Linear déconnecté',
'settings.integrations.linear.toast.workspaceSwitched': 'Workspace Linear modifié',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Impossible de changer de workspace Linear',
'settings.integrations.linear.toast.startConnectFailed': 'Impossible de démarrer la connexion Linear',
'settings.integrations.linear.toast.disconnectFailed': 'Impossible de déconnecter Linear',
'settings.integrations.linear.toast.authorizationFailed': 'Lautorisation Linear a expiré. Cliquez sur Connecter pour réessayer.',
'settings.integrations.linear.avatarAlt.withName': 'Avatar Linear de {name}',
'settings.integrations.linear.avatarAlt.fallback': 'Avatar Linear',
'settings.integrations.linear.label.unknownUser': 'Utilisateur inconnu',
'settings.integrations.linear.mapping.defaultProject': 'Projet par défaut',
'settings.integrations.linear.mapping.defaultProject.info': 'Les nouvelles sessions depuis des tickets Linear utilisent ce projet, sauf si l’équipe du ticket a sa propre association.',
'settings.integrations.linear.mapping.defaultProject.placeholder': 'Aucun',
'settings.integrations.linear.mapping.defaultProject.aria': 'Projet par défaut pour les tickets Linear',
'settings.integrations.linear.mapping.teams': 'Projets par équipe',
'settings.integrations.linear.mapping.teams.info': 'Facultatif. Un ticket dune équipe associée souvre dans ce projet plutôt que dans le projet par défaut.',
'settings.integrations.linear.mapping.teams.useDefault': 'Utiliser le défaut',
'settings.integrations.linear.mapping.teams.aria': 'Projet pour l’équipe Linear {team}',
'settings.integrations.linear.mapping.emptyProjects': 'Ajoutez dabord un projet, puis associez les équipes Linear.',
'settings.integrations.linear.mapping.emptyTeams': 'Cet espace Linear na aucune équipe.',
'settings.integrations.linear.mapping.loadFailed': 'Impossible de charger lassociation des projets Linear.',
'settings.integrations.linear.sessionComments.label': 'Commentaires de session',
'settings.integrations.linear.sessionComments.info': 'Ajoute un commentaire au ticket quand une session démarre, se termine ou échoue. Les commentaires ne sont publiés que si ce serveur a une adresse publique, afin que le lien ouvre la session pour tout le monde.',
'settings.integrations.linear.sessionComments.aria': 'Publier les commentaires d’état de session dans Linear',
'settings.integrations.linear.sessionComments.loadFailed': 'Impossible de charger les réglages de commentaires Linear.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Revue dissue',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Revue dissue',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts utilisés au démarrage dune session depuis un ticket Linear : message utilisateur visible + instructions masquées.',
},
es: {
'settings.integrations.firstParty.title': 'Integraciones nativas',
'settings.integrations.firstParty.info': 'Inicios de sesión de los servicios incluidos en OpenChamber. El inicio de sesión se guarda en este ordenador para que la web, el escritorio y un teléfono emparejado lo compartan.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'Conecta espacios de Linear a este servidor de OpenChamber.',
'settings.integrations.linear.info': 'Conecta uno o más espacios de Linear. OpenChamber guarda los inicios de sesión en este ordenador para que la web, el escritorio y un teléfono emparejado los compartan.',
'settings.integrations.linear.status.notConnected': 'No conectado',
'settings.integrations.linear.status.connected': 'Conectado',
'settings.integrations.linear.status.waiting': 'Esperando',
'settings.integrations.linear.actions.connect': 'Conectar',
'settings.integrations.linear.actions.disconnect': 'Desconectar',
'settings.integrations.linear.actions.addWorkspace': 'Añadir workspace',
'settings.integrations.linear.actions.switchTo': 'Cambiar a',
'settings.integrations.linear.label.otherWorkspaces': 'Otros workspaces',
'settings.integrations.linear.flow.title': 'Esperando a Linear',
'settings.integrations.linear.flow.description': 'Termina de iniciar sesión en la pestaña del navegador que acaba de abrirse.',
'settings.integrations.linear.flow.waiting': 'Esperando la autorización…',
'settings.integrations.linear.toast.connected': 'Linear conectado',
'settings.integrations.linear.toast.disconnected': 'Linear desconectado',
'settings.integrations.linear.toast.workspaceSwitched': 'Workspace de Linear cambiado',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'No se pudo cambiar el workspace de Linear',
'settings.integrations.linear.toast.startConnectFailed': 'No se pudo iniciar la conexión con Linear',
'settings.integrations.linear.toast.disconnectFailed': 'No se pudo desconectar Linear',
'settings.integrations.linear.toast.authorizationFailed': 'La autorización de Linear ha caducado. Haz clic en Conectar para intentarlo de nuevo.',
'settings.integrations.linear.avatarAlt.withName': 'Avatar de Linear de {name}',
'settings.integrations.linear.avatarAlt.fallback': 'Avatar de Linear',
'settings.integrations.linear.label.unknownUser': 'Usuario desconocido',
'settings.integrations.linear.mapping.defaultProject': 'Proyecto predeterminado',
'settings.integrations.linear.mapping.defaultProject.info': 'Las sesiones nuevas desde issues de Linear usan este proyecto, salvo que el equipo del issue tenga su propia asignación.',
'settings.integrations.linear.mapping.defaultProject.placeholder': 'Ninguno',
'settings.integrations.linear.mapping.defaultProject.aria': 'Proyecto predeterminado para issues de Linear',
'settings.integrations.linear.mapping.teams': 'Proyectos por equipo',
'settings.integrations.linear.mapping.teams.info': 'Opcional. Un issue de un equipo asignado se abre en ese proyecto en lugar del predeterminado.',
'settings.integrations.linear.mapping.teams.useDefault': 'Usar el predeterminado',
'settings.integrations.linear.mapping.teams.aria': 'Proyecto para el equipo de Linear {team}',
'settings.integrations.linear.mapping.emptyProjects': 'Añade primero un proyecto y luego asigna equipos de Linear.',
'settings.integrations.linear.mapping.emptyTeams': 'Este espacio de Linear no tiene equipos.',
'settings.integrations.linear.mapping.loadFailed': 'No se pudo cargar la asignación de proyectos de Linear.',
'settings.integrations.linear.sessionComments.label': 'Comentarios de sesión',
'settings.integrations.linear.sessionComments.info': 'Añade un comentario a la incidencia cuando una sesión empieza, termina o falla. Los comentarios solo se publican si este servidor tiene una dirección pública, para que el enlace abra la sesión a todos.',
'settings.integrations.linear.sessionComments.aria': 'Publicar comentarios de estado de sesión en Linear',
'settings.integrations.linear.sessionComments.loadFailed': 'No se pudieron cargar los ajustes de comentarios de Linear.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Revisión de issue',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Revisión de issue',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts usados al iniciar una sesión desde un issue de Linear: mensaje visible del usuario e instrucciones ocultas.',
},
ja: {
'settings.integrations.firstParty.title': '標準連携',
'settings.integrations.firstParty.info': 'OpenChamber に同梱されているサービスのログインです。このコンピュータに保存され、Web、デスクトップ、ペアリングしたスマホで共有されます。',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'この OpenChamber サーバーに Linear ワークスペースを接続します。複数接続できます。',
'settings.integrations.linear.info': 'Linear ワークスペースを1つ以上接続します。ログインはこのコンピュータに保存され、Web、デスクトップ、ペアリングしたスマホで共有されます。',
'settings.integrations.linear.status.notConnected': '未接続',
'settings.integrations.linear.status.connected': '接続済み',
'settings.integrations.linear.status.waiting': '待機中',
'settings.integrations.linear.actions.connect': '接続',
'settings.integrations.linear.actions.disconnect': '切断',
'settings.integrations.linear.actions.addWorkspace': 'ワークスペースを追加',
'settings.integrations.linear.actions.switchTo': '切り替える',
'settings.integrations.linear.label.otherWorkspaces': '他のワークスペース',
'settings.integrations.linear.flow.title': 'Linear を待っています',
'settings.integrations.linear.flow.description': '開いたブラウザタブでサインインを完了してください。',
'settings.integrations.linear.flow.waiting': '認可を待っています…',
'settings.integrations.linear.toast.connected': 'Linear に接続しました',
'settings.integrations.linear.toast.disconnected': 'Linear を切断しました',
'settings.integrations.linear.toast.workspaceSwitched': 'Linear ワークスペースを切り替えました',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Linear ワークスペースを切り替えられませんでした',
'settings.integrations.linear.toast.startConnectFailed': 'Linear のサインインを開始できませんでした',
'settings.integrations.linear.toast.disconnectFailed': 'Linear を切断できませんでした',
'settings.integrations.linear.toast.authorizationFailed': 'Linear の認可がタイムアウトしました。接続をもう一度押してください。',
'settings.integrations.linear.avatarAlt.withName': '{name} の Linear アバター',
'settings.integrations.linear.avatarAlt.fallback': 'Linear アバター',
'settings.integrations.linear.label.unknownUser': '不明なユーザー',
'settings.integrations.linear.mapping.defaultProject': 'デフォルトのプロジェクト',
'settings.integrations.linear.mapping.defaultProject.info': 'Linear Issueから作る新しいセッションはこのプロジェクトを使います。チームに個別の割り当てがある場合はそちらを使います。',
'settings.integrations.linear.mapping.defaultProject.placeholder': 'なし',
'settings.integrations.linear.mapping.defaultProject.aria': 'Linear Issueのデフォルトプロジェクト',
'settings.integrations.linear.mapping.teams': 'チームのプロジェクト',
'settings.integrations.linear.mapping.teams.info': '任意。割り当てたチームのIssueは、デフォルトではなくそのプロジェクトで開きます。',
'settings.integrations.linear.mapping.teams.useDefault': 'デフォルトを使う',
'settings.integrations.linear.mapping.teams.aria': 'Linearチーム {team} のプロジェクト',
'settings.integrations.linear.mapping.emptyProjects': '先にプロジェクトを追加してから、Linearチームを割り当ててください。',
'settings.integrations.linear.mapping.emptyTeams': 'このLinearワークスペースにはチームがありません。',
'settings.integrations.linear.mapping.loadFailed': 'Linearのプロジェクト割り当てを読み込めませんでした。',
'settings.integrations.linear.sessionComments.label': 'セッションのコメント',
'settings.integrations.linear.sessionComments.info': 'セッションの開始・完了・失敗時にイシューへコメントします。リンクを誰でも開けるよう、このサーバーが公開アドレスを持つ場合のみ投稿します。',
'settings.integrations.linear.sessionComments.aria': 'セッション状態のコメントを Linear に投稿',
'settings.integrations.linear.sessionComments.loadFailed': 'Linear のコメント設定を読み込めませんでした。',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue レビュー',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue レビュー',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Linear の Issue からセッションを開始するときに使うプロンプト: 表示ユーザーメッセージ + 非表示の指示。',
},
ko: {
'settings.integrations.firstParty.title': '기본 제공 통합',
'settings.integrations.firstParty.info': 'OpenChamber에 포함된 서비스 로그인입니다. 이 컴퓨터에 저장되며 웹, 데스크톱, 페어링된 휴대폰이 공유합니다.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': '이 OpenChamber 서버에 Linear 워크스페이스를 연결하세요. 여러 개를 연결할 수 있습니다.',
'settings.integrations.linear.info': 'Linear 워크스페이스를 하나 이상 연결하세요. 로그인은 이 컴퓨터에 저장되며 웹, 데스크톱, 페어링된 휴대폰이 공유합니다.',
'settings.integrations.linear.status.notConnected': '연결되지 않음',
'settings.integrations.linear.status.connected': '연결됨',
'settings.integrations.linear.status.waiting': '대기 중',
'settings.integrations.linear.actions.connect': '연결',
'settings.integrations.linear.actions.disconnect': '연결 해제',
'settings.integrations.linear.actions.addWorkspace': '워크스페이스 추가',
'settings.integrations.linear.actions.switchTo': '전환',
'settings.integrations.linear.label.otherWorkspaces': '다른 워크스페이스',
'settings.integrations.linear.flow.title': 'Linear 대기 중',
'settings.integrations.linear.flow.description': '방금 열린 브라우저 탭에서 로그인을 완료하세요.',
'settings.integrations.linear.flow.waiting': '권한 부여를 기다리는 중…',
'settings.integrations.linear.toast.connected': 'Linear가 연결됨',
'settings.integrations.linear.toast.disconnected': 'Linear 연결이 해제됨',
'settings.integrations.linear.toast.workspaceSwitched': 'Linear 워크스페이스를 전환했습니다',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Linear 워크스페이스를 전환하지 못했습니다',
'settings.integrations.linear.toast.startConnectFailed': 'Linear 로그인을 시작하지 못했습니다',
'settings.integrations.linear.toast.disconnectFailed': 'Linear 연결을 해제하지 못했습니다',
'settings.integrations.linear.toast.authorizationFailed': 'Linear 권한 부여가 시간 초과되었습니다. 연결을 다시 누르세요.',
'settings.integrations.linear.avatarAlt.withName': '{name}의 Linear 아바타',
'settings.integrations.linear.avatarAlt.fallback': 'Linear 아바타',
'settings.integrations.linear.label.unknownUser': '알 수 없는 사용자',
'settings.integrations.linear.mapping.defaultProject': '기본 프로젝트',
'settings.integrations.linear.mapping.defaultProject.info': 'Linear 이슈에서 만드는 새 세션은 이 프로젝트를 사용합니다. 해당 팀에 별도 연결이 있으면 그쪽을 씁니다.',
'settings.integrations.linear.mapping.defaultProject.placeholder': '없음',
'settings.integrations.linear.mapping.defaultProject.aria': 'Linear 이슈의 기본 프로젝트',
'settings.integrations.linear.mapping.teams': '팀 프로젝트',
'settings.integrations.linear.mapping.teams.info': '선택 사항입니다. 연결한 팀의 이슈는 기본값 대신 그 프로젝트에서 열립니다.',
'settings.integrations.linear.mapping.teams.useDefault': '기본값 사용',
'settings.integrations.linear.mapping.teams.aria': 'Linear 팀 {team}의 프로젝트',
'settings.integrations.linear.mapping.emptyProjects': '먼저 프로젝트를 추가한 다음 Linear 팀을 연결하세요.',
'settings.integrations.linear.mapping.emptyTeams': '이 Linear 워크스페이스에는 팀이 없습니다.',
'settings.integrations.linear.mapping.loadFailed': 'Linear 프로젝트 연결을 불러오지 못했습니다.',
'settings.integrations.linear.sessionComments.label': '세션 댓글',
'settings.integrations.linear.sessionComments.info': '세션이 시작, 완료, 실패할 때 이슈에 댓글을 남깁니다. 링크를 모두가 열 수 있도록 이 서버에 공개 주소가 있을 때만 게시합니다.',
'settings.integrations.linear.sessionComments.aria': '세션 상태 댓글을 Linear에 게시',
'settings.integrations.linear.sessionComments.loadFailed': 'Linear 댓글 설정을 불러오지 못했습니다.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': '이슈 리뷰',
'settings.magicPrompts.page.group.linearIssueReview.title': '이슈 리뷰',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Linear 이슈로 세션을 시작할 때 쓰는 프롬프트입니다: 표시 사용자 메시지 + 숨겨진 지침.',
},
pl: {
'settings.integrations.firstParty.title': 'Wbudowane integracje',
'settings.integrations.firstParty.info': 'Logowania do usług dostarczanych z OpenChamber. Zapisujemy je na tym komputerze, żeby przeglądarka, aplikacja desktopowa i sparowany telefon z nich korzystały.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'Połącz przestrzenie Linear z tym serwerem OpenChamber.',
'settings.integrations.linear.info': 'Połącz jedną lub kilka przestrzeni Linear. OpenChamber zapisuje logowania na tym komputerze, żeby przeglądarka, aplikacja desktopowa i sparowany telefon z nich korzystały.',
'settings.integrations.linear.status.notConnected': 'Nie połączono',
'settings.integrations.linear.status.connected': 'Połączono',
'settings.integrations.linear.status.waiting': 'Oczekiwanie',
'settings.integrations.linear.actions.connect': 'Połącz',
'settings.integrations.linear.actions.disconnect': 'Rozłącz',
'settings.integrations.linear.actions.addWorkspace': 'Dodaj workspace',
'settings.integrations.linear.actions.switchTo': 'Przełącz na',
'settings.integrations.linear.label.otherWorkspaces': 'Inne przestrzenie',
'settings.integrations.linear.flow.title': 'Oczekiwanie na Linear',
'settings.integrations.linear.flow.description': 'Dokończ logowanie w karcie przeglądarki, która właśnie się otworzyła.',
'settings.integrations.linear.flow.waiting': 'Oczekiwanie na autoryzację…',
'settings.integrations.linear.toast.connected': 'Połączono z Linear',
'settings.integrations.linear.toast.disconnected': 'Rozłączono Linear',
'settings.integrations.linear.toast.workspaceSwitched': 'Przełączono workspace Linear',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Nie udało się przełączyć workspace Linear',
'settings.integrations.linear.toast.startConnectFailed': 'Nie udało się rozpocząć logowania do Linear',
'settings.integrations.linear.toast.disconnectFailed': 'Nie udało się rozłączyć Linear',
'settings.integrations.linear.toast.authorizationFailed': 'Autoryzacja Linear wygasła. Kliknij Połącz, aby spróbować ponownie.',
'settings.integrations.linear.avatarAlt.withName': 'Awatar Linear użytkownika {name}',
'settings.integrations.linear.avatarAlt.fallback': 'Awatar Linear',
'settings.integrations.linear.label.unknownUser': 'Nieznany użytkownik',
'settings.integrations.linear.mapping.defaultProject': 'Domyślny projekt',
'settings.integrations.linear.mapping.defaultProject.info': 'Nowe sesje ze zgłoszeń Linear używają tego projektu, chyba że zespół zgłoszenia ma własne przypisanie.',
'settings.integrations.linear.mapping.defaultProject.placeholder': 'Brak',
'settings.integrations.linear.mapping.defaultProject.aria': 'Domyślny projekt dla zgłoszeń Linear',
'settings.integrations.linear.mapping.teams': 'Projekty zespołów',
'settings.integrations.linear.mapping.teams.info': 'Opcjonalnie. Zgłoszenie z przypisanego zespołu otworzy się w tym projekcie zamiast w domyślnym.',
'settings.integrations.linear.mapping.teams.useDefault': 'Użyj domyślnego',
'settings.integrations.linear.mapping.teams.aria': 'Projekt dla zespołu Linear {team}',
'settings.integrations.linear.mapping.emptyProjects': 'Najpierw dodaj projekt, a potem przypisz zespoły Linear.',
'settings.integrations.linear.mapping.emptyTeams': 'Ten obszar Linear nie ma zespołów.',
'settings.integrations.linear.mapping.loadFailed': 'Nie udało się wczytać przypisania projektów Linear.',
'settings.integrations.linear.sessionComments.label': 'Komentarze o sesji',
'settings.integrations.linear.sessionComments.info': 'Dodaje komentarz do zgłoszenia, gdy sesja się zaczyna, kończy lub kończy błędem. Komentarze pojawiają się tylko wtedy, gdy ten serwer ma publiczny adres, żeby link otwierał sesję każdemu.',
'settings.integrations.linear.sessionComments.aria': 'Publikuj komentarze o stanie sesji w Linear',
'settings.integrations.linear.sessionComments.loadFailed': 'Nie udało się wczytać ustawień komentarzy Linear.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Przegląd zgłoszenia',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Przegląd zgłoszenia',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompty używane przy starcie sesji ze zgłoszenia Linear: widoczna wiadomość użytkownika i ukryte instrukcje.',
},
'pt-BR': {
'settings.integrations.firstParty.title': 'Integrações nativas',
'settings.integrations.firstParty.info': 'Logins dos serviços inclusos no OpenChamber. O login fica neste computador para que a web, o app desktop e um celular emparelhado o compartilhem.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'Conecte espaços do Linear a este servidor OpenChamber.',
'settings.integrations.linear.info': 'Conecte um ou mais espaços do Linear. O OpenChamber guarda os logins neste computador para que a web, o app desktop e um celular emparelhado os compartilhem.',
'settings.integrations.linear.status.notConnected': 'Não conectado',
'settings.integrations.linear.status.connected': 'Conectado',
'settings.integrations.linear.status.waiting': 'Aguardando',
'settings.integrations.linear.actions.connect': 'Conectar',
'settings.integrations.linear.actions.disconnect': 'Desconectar',
'settings.integrations.linear.actions.addWorkspace': 'Adicionar workspace',
'settings.integrations.linear.actions.switchTo': 'Alternar para',
'settings.integrations.linear.label.otherWorkspaces': 'Outros workspaces',
'settings.integrations.linear.flow.title': 'Aguardando o Linear',
'settings.integrations.linear.flow.description': 'Conclua o login na aba do navegador que acabou de abrir.',
'settings.integrations.linear.flow.waiting': 'Aguardando autorização…',
'settings.integrations.linear.toast.connected': 'Linear conectado',
'settings.integrations.linear.toast.disconnected': 'Linear desconectado',
'settings.integrations.linear.toast.workspaceSwitched': 'Workspace do Linear alterado',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Não foi possível alternar o workspace do Linear',
'settings.integrations.linear.toast.startConnectFailed': 'Não foi possível iniciar o login no Linear',
'settings.integrations.linear.toast.disconnectFailed': 'Não foi possível desconectar o Linear',
'settings.integrations.linear.toast.authorizationFailed': 'A autorização do Linear expirou. Clique em Conectar para tentar de novo.',
'settings.integrations.linear.avatarAlt.withName': 'Avatar do Linear de {name}',
'settings.integrations.linear.avatarAlt.fallback': 'Avatar do Linear',
'settings.integrations.linear.label.unknownUser': 'Usuário desconhecido',
'settings.integrations.linear.mapping.defaultProject': 'Projeto padrão',
'settings.integrations.linear.mapping.defaultProject.info': 'Novas sessões a partir de issues do Linear usam este projeto, a menos que a equipe da issue tenha o próprio mapeamento.',
'settings.integrations.linear.mapping.defaultProject.placeholder': 'Nenhum',
'settings.integrations.linear.mapping.defaultProject.aria': 'Projeto padrão para issues do Linear',
'settings.integrations.linear.mapping.teams': 'Projetos por equipe',
'settings.integrations.linear.mapping.teams.info': 'Opcional. Uma issue de uma equipe mapeada abre nesse projeto em vez do padrão.',
'settings.integrations.linear.mapping.teams.useDefault': 'Usar o padrão',
'settings.integrations.linear.mapping.teams.aria': 'Projeto para a equipe do Linear {team}',
'settings.integrations.linear.mapping.emptyProjects': 'Adicione um projeto primeiro e depois mapeie as equipes do Linear.',
'settings.integrations.linear.mapping.emptyTeams': 'Este espaço do Linear não tem equipes.',
'settings.integrations.linear.mapping.loadFailed': 'Não foi possível carregar o mapeamento de projetos do Linear.',
'settings.integrations.linear.sessionComments.label': 'Comentários de sessão',
'settings.integrations.linear.sessionComments.info': 'Comenta na issue quando uma sessão começa, termina ou falha. Os comentários só são publicados se este servidor tiver um endereço público, para que o link abra a sessão para todos.',
'settings.integrations.linear.sessionComments.aria': 'Publicar comentários de status de sessão no Linear',
'settings.integrations.linear.sessionComments.loadFailed': 'Não foi possível carregar as configurações de comentários do Linear.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Revisão de issue',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Revisão de issue',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Prompts usados ao iniciar uma sessão a partir de uma issue do Linear: mensagem visível do usuário e instruções ocultas.',
},
uk: {
'settings.integrations.firstParty.title': 'Вбудовані інтеграції',
'settings.integrations.firstParty.info': 'Входи до сервісів, що входять до OpenChamber. Логін лишається на цьому комп’ютері, тож веб, десктоп і спарений телефон користуються одним обліковим записом.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'Підключіть Linear workspace до цього сервера OpenChamber. Можна кілька.',
'settings.integrations.linear.info': 'Підключіть один або кілька Linear workspace. OpenChamber зберігає входи на цьому комп’ютері, тож веб, десктоп і спарений телефон користуються ними.',
'settings.integrations.linear.status.notConnected': 'Не підключено',
'settings.integrations.linear.status.connected': 'Підключено',
'settings.integrations.linear.status.waiting': 'Очікування',
'settings.integrations.linear.actions.connect': 'Підключити',
'settings.integrations.linear.actions.disconnect': 'Відключити',
'settings.integrations.linear.actions.addWorkspace': 'Додати workspace',
'settings.integrations.linear.actions.switchTo': 'Перемкнути на',
'settings.integrations.linear.label.otherWorkspaces': 'Інші workspace',
'settings.integrations.linear.flow.title': 'Очікування Linear',
'settings.integrations.linear.flow.description': 'Завершіть вхід у вкладці браузера, яка щойно відкрилась.',
'settings.integrations.linear.flow.waiting': 'Очікування авторизації…',
'settings.integrations.linear.toast.connected': 'Linear підключено',
'settings.integrations.linear.toast.disconnected': 'Linear відключено',
'settings.integrations.linear.toast.workspaceSwitched': 'Перемкнуто Linear workspace',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Не вдалося перемкнути Linear workspace',
'settings.integrations.linear.toast.startConnectFailed': 'Не вдалося почати вхід у Linear',
'settings.integrations.linear.toast.disconnectFailed': 'Не вдалося відключити Linear',
'settings.integrations.linear.toast.authorizationFailed': 'Авторизація Linear завершилась за часом. Натисніть Підключити ще раз.',
'settings.integrations.linear.avatarAlt.withName': 'Аватар Linear для {name}',
'settings.integrations.linear.avatarAlt.fallback': 'Аватар Linear',
'settings.integrations.linear.label.unknownUser': 'Невідомий користувач',
'settings.integrations.linear.mapping.defaultProject': 'Проєкт за замовчуванням',
'settings.integrations.linear.mapping.defaultProject.info': 'Нові сесії з Linear issue використовують цей проєкт, якщо в команди issue немає власної прив’язки.',
'settings.integrations.linear.mapping.defaultProject.placeholder': 'Немає',
'settings.integrations.linear.mapping.defaultProject.aria': 'Проєкт за замовчуванням для Linear issue',
'settings.integrations.linear.mapping.teams': 'Проєкти команд',
'settings.integrations.linear.mapping.teams.info': 'Не обов’язково. Issue з прив’язаної команди відкриється в цьому проєкті, а не в типовому.',
'settings.integrations.linear.mapping.teams.useDefault': 'Використати типовий',
'settings.integrations.linear.mapping.teams.aria': 'Проєкт для команди Linear {team}',
'settings.integrations.linear.mapping.emptyProjects': 'Спочатку додайте проєкт, потім прив’яжіть команди Linear.',
'settings.integrations.linear.mapping.emptyTeams': 'У цьому робочому просторі Linear немає команд.',
'settings.integrations.linear.mapping.loadFailed': 'Не вдалося завантажити прив’язку проєктів Linear.',
'settings.integrations.linear.sessionComments.label': 'Коментарі про сесію',
'settings.integrations.linear.sessionComments.info': 'Додає коментар до тікета, коли сесія починається, завершується або падає. Коментарі публікуються, лише якщо цей сервер має публічну адресу, щоб посилання відкривало сесію для всіх.',
'settings.integrations.linear.sessionComments.aria': 'Публікувати коментарі про стан сесії в Linear',
'settings.integrations.linear.sessionComments.loadFailed': 'Не вдалося завантажити налаштування коментарів Linear.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Огляд issue',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Огляд issue',
'settings.magicPrompts.page.group.linearIssueReview.description': 'Промпти для старту сесії з Linear issue: видиме повідомлення користувача та приховані інструкції.',
},
'zh-CN': {
'settings.integrations.firstParty.title': '内置集成',
'settings.integrations.firstParty.info': 'OpenChamber 自带服务的登录。登录保存在这台电脑上,网页、桌面应用和已配对的手机会共用它。',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': '将 Linear 工作区连接到此 OpenChamber 服务器。可以连接多个。',
'settings.integrations.linear.info': '连接一个或多个 Linear 工作区。OpenChamber 把登录保存在这台电脑上,网页、桌面应用和已配对的手机会共用它们。',
'settings.integrations.linear.status.notConnected': '未连接',
'settings.integrations.linear.status.connected': '已连接',
'settings.integrations.linear.status.waiting': '等待中',
'settings.integrations.linear.actions.connect': '连接',
'settings.integrations.linear.actions.disconnect': '断开',
'settings.integrations.linear.actions.addWorkspace': '添加工作区',
'settings.integrations.linear.actions.switchTo': '切换到',
'settings.integrations.linear.label.otherWorkspaces': '其他工作区',
'settings.integrations.linear.flow.title': '正在等待 Linear',
'settings.integrations.linear.flow.description': '请在刚打开的浏览器标签页中完成登录。',
'settings.integrations.linear.flow.waiting': '正在等待授权…',
'settings.integrations.linear.toast.connected': '已连接 Linear',
'settings.integrations.linear.toast.disconnected': '已断开 Linear',
'settings.integrations.linear.toast.workspaceSwitched': '已切换 Linear 工作区',
'settings.integrations.linear.toast.workspaceSwitchFailed': '无法切换 Linear 工作区',
'settings.integrations.linear.toast.startConnectFailed': '无法开始 Linear 登录',
'settings.integrations.linear.toast.disconnectFailed': '无法断开 Linear',
'settings.integrations.linear.toast.authorizationFailed': 'Linear 授权已超时。请再次点击连接。',
'settings.integrations.linear.avatarAlt.withName': '{name} 的 Linear 头像',
'settings.integrations.linear.avatarAlt.fallback': 'Linear 头像',
'settings.integrations.linear.label.unknownUser': '未知用户',
'settings.integrations.linear.mapping.defaultProject': '默认项目',
'settings.integrations.linear.mapping.defaultProject.info': '从 Linear Issue 新建的会话会使用此项目,除非该 Issue 所属团队有单独映射。',
'settings.integrations.linear.mapping.defaultProject.placeholder': '无',
'settings.integrations.linear.mapping.defaultProject.aria': 'Linear Issue 的默认项目',
'settings.integrations.linear.mapping.teams': '团队项目',
'settings.integrations.linear.mapping.teams.info': '可选。来自已映射团队的 Issue 会在该项目中打开,而不是默认项目。',
'settings.integrations.linear.mapping.teams.useDefault': '使用默认',
'settings.integrations.linear.mapping.teams.aria': 'Linear 团队 {team} 的项目',
'settings.integrations.linear.mapping.emptyProjects': '请先添加一个项目,再映射 Linear 团队。',
'settings.integrations.linear.mapping.emptyTeams': '此 Linear 工作区没有团队。',
'settings.integrations.linear.mapping.loadFailed': '无法加载 Linear 项目映射。',
'settings.integrations.linear.sessionComments.label': '会话评论',
'settings.integrations.linear.sessionComments.info': '会话开始、完成或失败时在议题下留言。仅当此服务器拥有公网地址时才发布,这样链接才能让所有人打开该会话。',
'settings.integrations.linear.sessionComments.aria': '将会话状态评论发布到 Linear',
'settings.integrations.linear.sessionComments.loadFailed': '无法加载 Linear 评论设置。',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue 审查',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue 审查',
'settings.magicPrompts.page.group.linearIssueReview.description': '从 Linear Issue 开始会话时使用的提示词:可见用户消息 + 隐藏指令。',
},
'zh-TW': {
'settings.integrations.firstParty.title': '內建整合',
'settings.integrations.firstParty.info': 'OpenChamber 內建服務的登入。登入保存在這台電腦上,網頁、桌面應用程式和已配對的手機會共用它。',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': '將 Linear 工作區連線到此 OpenChamber 伺服器。可以連線多個。',
'settings.integrations.linear.info': '連接一個或多個 Linear 工作區。OpenChamber 把登入保存在這台電腦上,網頁、桌面應用程式和已配對的手機會共用它們。',
'settings.integrations.linear.status.notConnected': '未連線',
'settings.integrations.linear.status.connected': '已連線',
'settings.integrations.linear.status.waiting': '等待中',
'settings.integrations.linear.actions.connect': '連線',
'settings.integrations.linear.actions.disconnect': '中斷連線',
'settings.integrations.linear.actions.addWorkspace': '新增工作區',
'settings.integrations.linear.actions.switchTo': '切換到',
'settings.integrations.linear.label.otherWorkspaces': '其他工作區',
'settings.integrations.linear.flow.title': '正在等待 Linear',
'settings.integrations.linear.flow.description': '請在剛開啟的瀏覽器分頁中完成登入。',
'settings.integrations.linear.flow.waiting': '正在等待授權…',
'settings.integrations.linear.toast.connected': '已連線 Linear',
'settings.integrations.linear.toast.disconnected': '已中斷 Linear',
'settings.integrations.linear.toast.workspaceSwitched': '已切換 Linear 工作區',
'settings.integrations.linear.toast.workspaceSwitchFailed': '無法切換 Linear 工作區',
'settings.integrations.linear.toast.startConnectFailed': '無法開始 Linear 登入',
'settings.integrations.linear.toast.disconnectFailed': '無法中斷 Linear',
'settings.integrations.linear.toast.authorizationFailed': 'Linear 授權已逾時。請再次按連線。',
'settings.integrations.linear.avatarAlt.withName': '{name} 的 Linear 頭像',
'settings.integrations.linear.avatarAlt.fallback': 'Linear 頭像',
'settings.integrations.linear.label.unknownUser': '未知使用者',
'settings.integrations.linear.mapping.defaultProject': '預設專案',
'settings.integrations.linear.mapping.defaultProject.info': '從 Linear Issue 新增的會話會使用此專案,除非該 Issue 所屬團隊有單獨對應。',
'settings.integrations.linear.mapping.defaultProject.placeholder': '無',
'settings.integrations.linear.mapping.defaultProject.aria': 'Linear Issue 的預設專案',
'settings.integrations.linear.mapping.teams': '團隊專案',
'settings.integrations.linear.mapping.teams.info': '選用。來自已對應團隊的 Issue 會在該專案中開啟,而不是預設專案。',
'settings.integrations.linear.mapping.teams.useDefault': '使用預設',
'settings.integrations.linear.mapping.teams.aria': 'Linear 團隊 {team} 的專案',
'settings.integrations.linear.mapping.emptyProjects': '請先新增一個專案,再對應 Linear 團隊。',
'settings.integrations.linear.mapping.emptyTeams': '此 Linear 工作區沒有團隊。',
'settings.integrations.linear.mapping.loadFailed': '無法載入 Linear 專案對應。',
'settings.integrations.linear.sessionComments.label': '工作階段留言',
'settings.integrations.linear.sessionComments.info': '工作階段開始、完成或失敗時在議題留言。僅在這台伺服器有公開位址時才發布,這樣連結才能讓所有人開啟該工作階段。',
'settings.integrations.linear.sessionComments.aria': '將工作階段狀態留言發布到 Linear',
'settings.integrations.linear.sessionComments.loadFailed': '無法載入 Linear 留言設定。',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue 審查',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue 審查',
'settings.magicPrompts.page.group.linearIssueReview.description': '從 Linear Issue 開始會話時使用的提示詞:可見使用者訊息 + 隱藏指令。',
},
tr: {
'settings.integrations.firstParty.title': 'Yerleşik entegrasyonlar',
'settings.integrations.firstParty.info': 'OpenChamber ile gelen hizmetlerin oturumları. Giriş bu bilgisayarda kalır; web, masaüstü ve eşlenen telefon paylaşır.',
'settings.integrations.linear.title': 'Linear',
'settings.integrations.linear.description': 'Linear çalışma alanlarını bu OpenChamber sunucusuna bağla.',
'settings.integrations.linear.info': 'Bir veya daha fazla Linear çalışma alanı bağla. OpenChamber girişleri bu bilgisayarda tutar; web, masaüstü ve eşlenen telefon paylaşır.',
'settings.integrations.linear.status.notConnected': 'Bağlı değil',
'settings.integrations.linear.status.connected': 'Bağlı',
'settings.integrations.linear.status.waiting': 'Bekleniyor',
'settings.integrations.linear.actions.connect': 'Bağlan',
'settings.integrations.linear.actions.disconnect': 'Bağlantıyı kes',
'settings.integrations.linear.actions.addWorkspace': 'Çalışma alanı ekle',
'settings.integrations.linear.actions.switchTo': 'Şuna geç',
'settings.integrations.linear.label.otherWorkspaces': 'Diğer çalışma alanları',
'settings.integrations.linear.flow.title': 'Linear bekleniyor',
'settings.integrations.linear.flow.description': 'Az önce açılan tarayıcı sekmesinde girişi bitir.',
'settings.integrations.linear.flow.waiting': 'Yetkilendirme bekleniyor…',
'settings.integrations.linear.toast.connected': 'Linear bağlandı',
'settings.integrations.linear.toast.disconnected': 'Linear bağlantısı kesildi',
'settings.integrations.linear.toast.workspaceSwitched': 'Linear çalışma alanı değiştirildi',
'settings.integrations.linear.toast.workspaceSwitchFailed': 'Linear çalışma alanı değiştirilemedi',
'settings.integrations.linear.toast.startConnectFailed': 'Linear girişi başlatılamadı',
'settings.integrations.linear.toast.disconnectFailed': 'Linear bağlantısı kesilemedi',
'settings.integrations.linear.toast.authorizationFailed': "Linear yetkilendirmesi zaman aşımına uğradı. Yeniden bağlanmak için Bağlan'a bas.",
'settings.integrations.linear.avatarAlt.withName': '{name} için Linear avatarı',
'settings.integrations.linear.avatarAlt.fallback': 'Linear avatarı',
'settings.integrations.linear.label.unknownUser': 'Bilinmeyen kullanıcı',
'settings.integrations.linear.mapping.defaultProject': 'Varsayılan proje',
'settings.integrations.linear.mapping.defaultProject.info': "Linear issue'larından yeni session'lar, ekibin kendi eşlemesi yoksa bu projeyi kullanır.",
'settings.integrations.linear.mapping.defaultProject.placeholder': 'Yok',
'settings.integrations.linear.mapping.defaultProject.aria': "Linear issue'ları için varsayılan proje",
'settings.integrations.linear.mapping.teams': 'Ekip projeleri',
'settings.integrations.linear.mapping.teams.info': 'İsteğe bağlı. Eşlenen bir ekipten gelen issue varsayılan yerine o projede açılır.',
'settings.integrations.linear.mapping.teams.useDefault': 'Varsayılanı kullan',
'settings.integrations.linear.mapping.teams.aria': 'Linear ekibi {team} için proje',
'settings.integrations.linear.mapping.emptyProjects': 'Önce bir proje ekle, sonra Linear ekiplerini ona eşle.',
'settings.integrations.linear.mapping.emptyTeams': 'Bu Linear çalışma alanında ekip yok.',
'settings.integrations.linear.mapping.loadFailed': 'Linear proje eşlemesi yüklenemedi.',
'settings.integrations.linear.sessionComments.label': 'Oturum yorumları',
'settings.integrations.linear.sessionComments.info': 'Bir oturum başladığında, bittiğinde veya başarısız olduğunda göreve yorum ekler. Bağlantının herkeste açılabilmesi için yorumlar yalnızca bu sunucunun genel bir adresi varsa gönderilir.',
'settings.integrations.linear.sessionComments.aria': 'Oturum durumu yorumlarını Lineara gönder',
'settings.integrations.linear.sessionComments.loadFailed': 'Linear yorum ayarları yüklenemedi.',
'settings.magicPrompts.sidebar.group.linear': 'Linear',
'settings.magicPrompts.sidebar.item.linearIssueReview': 'Issue incelemesi',
'settings.magicPrompts.page.group.linearIssueReview.title': 'Issue incelemesi',
'settings.magicPrompts.page.group.linearIssueReview.description': "Linear issue'dan session başlatırken kullanılan prompt'lar: görünen kullanıcı mesajı + gizli talimatlar.",
},
} as const;
@@ -0,0 +1,59 @@
import { describe, expect, test } from 'bun:test';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
const locales = ['en', 'de', 'fr', 'es', 'ja', 'pt-BR', 'uk', 'ko', 'pl', 'zh-CN', 'zh-TW', 'tr'] as const;
const requiredKeys = [
'chat.chatInput.actions.linkLinearIssue',
'chat.chatInput.linked.linearIssue.openInBrowserAria',
'chat.chatInput.linked.linearIssue.removeAria',
'session.linearIssuePicker.title',
'session.linearIssuePicker.description',
'session.linearIssuePicker.searchPlaceholder',
'session.linearIssuePicker.empty.notConnected',
'session.linearIssuePicker.empty.runtimeUnavailable',
'session.linearIssuePicker.empty.noIssuesFound',
'session.linearIssuePicker.empty.noOpenIssuesFound',
'session.linearIssuePicker.loading.issues',
'session.linearIssuePicker.loading.more',
'session.linearIssuePicker.actions.openSettings',
'session.linearIssuePicker.actions.useIssue',
'session.linearIssuePicker.actions.loadMore',
'session.linearIssuePicker.actions.openInLinearAria',
'session.linearIssuePicker.toast.loadMoreFailed',
'session.linearIssuePicker.toast.loadIssueDetailsFailed',
'session.linearIssuePicker.error.notConnected',
'session.linearIssuePicker.error.runtimeUnavailable',
'session.linearIssuePicker.error.issueNotFound',
'chat.chatInput.actions.newSessionFromLinearIssue',
'session.linearIssuePicker.title.createSession',
'session.linearIssuePicker.description.createSession',
'session.linearIssuePicker.error.noMappedProject',
'session.linearIssuePicker.error.noModelSelected',
'session.linearIssuePicker.toast.sendContextFailed',
'session.linearIssuePicker.toast.sessionCreated',
'session.linearIssuePicker.toast.startSessionFailed',
'session.linearIssuePicker.actions.sectionTitle',
'session.linearIssuePicker.actions.toggleWorktreeAria',
'session.linearIssuePicker.actions.createInWorktree',
'session.linearIssuePicker.actions.refresh',
'chat.workStatus.linkedIssues.openLinear',
'session.newWorktree.actions.startFromLinearIssue',
'session.newWorktree.fromLinearIssue',
'session.newWorktree.error.sendLinearContextFailed',
] as const;
describe('linear issue picker translations', () => {
test('provides every required key in every supported locale', () => {
const english = linearIssuePickerI18n.en;
for (const locale of locales) {
for (const key of requiredKeys) {
const value = linearIssuePickerI18n[locale][key];
expect(value).toBeTruthy();
if (locale !== 'en') {
expect(value).not.toBe(english[key]);
}
}
}
});
});
@@ -0,0 +1,471 @@
/** Linear issue picker / composer strings — merged into each locale's main dictionary. */
export const linearIssuePickerI18n = {
en: {
'chat.chatInput.actions.linkLinearIssue': 'Link Linear Issue',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'Open issue in Linear',
'chat.chatInput.linked.linearIssue.removeAria': 'Remove linked Linear issue',
'session.linearIssuePicker.title': 'Link Linear Issue',
'session.linearIssuePicker.description': 'Select an issue from your connected Linear workspace.',
'session.linearIssuePicker.searchPlaceholder': 'Search by title, identifier, or Linear URL',
'session.linearIssuePicker.empty.notConnected': 'Linear is not connected. Connect it in Settings → Integrations.',
'session.linearIssuePicker.empty.runtimeUnavailable': 'Linear is not available in this app.',
'session.linearIssuePicker.empty.noIssuesFound': 'No issues found',
'session.linearIssuePicker.empty.noOpenIssuesFound': 'No open issues found',
'session.linearIssuePicker.loading.issues': 'Loading issues...',
'session.linearIssuePicker.loading.more': 'Loading...',
'session.linearIssuePicker.actions.openSettings': 'Open settings',
'session.linearIssuePicker.actions.useIssue': 'Use {identifier}',
'session.linearIssuePicker.actions.loadMore': 'Load more',
'session.linearIssuePicker.actions.openInLinearAria': 'Open in Linear',
'session.linearIssuePicker.toast.loadMoreFailed': 'Failed to load more issues',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'Failed to load issue details',
'session.linearIssuePicker.error.notConnected': 'Linear not connected',
'session.linearIssuePicker.error.runtimeUnavailable': 'Linear is not available in this app',
'session.linearIssuePicker.error.issueNotFound': 'Issue not found',
'chat.chatInput.actions.newSessionFromLinearIssue': 'New Session From Linear Issue',
'session.linearIssuePicker.title.createSession': 'New Session From Linear Issue',
'session.linearIssuePicker.description.createSession': 'Creates a session in the project mapped to this Linear team, with the issue as the first prompt.',
'session.linearIssuePicker.error.noMappedProject': 'Map this Linear team to a project in Settings → Integrations',
'session.linearIssuePicker.error.noModelSelected': 'No model selected',
'session.linearIssuePicker.toast.sendContextFailed': 'Failed to send issue context',
'session.linearIssuePicker.toast.sessionCreated': 'Session created from issue',
'session.linearIssuePicker.toast.startSessionFailed': 'Failed to start session',
'session.linearIssuePicker.actions.sectionTitle': 'Actions',
'session.linearIssuePicker.actions.toggleWorktreeAria': 'Toggle worktree',
'session.linearIssuePicker.actions.createInWorktree': 'Create in worktree',
'session.linearIssuePicker.actions.refresh': 'Refresh',
'chat.workStatus.linkedIssues.openLinear': 'Open {identifier} in Linear',
'session.newWorktree.actions.startFromLinearIssue': 'Start from Linear Issue',
'session.newWorktree.fromLinearIssue': 'From {identifier}: {title}',
'session.newWorktree.error.sendLinearContextFailed': 'Failed to send Linear context',
},
de: {
'chat.chatInput.actions.linkLinearIssue': 'Linear-Issue verknüpfen',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'Issue in Linear öffnen',
'chat.chatInput.linked.linearIssue.removeAria': 'Verknüpftes Linear-Issue entfernen',
'session.linearIssuePicker.title': 'Linear-Issue verknüpfen',
'session.linearIssuePicker.description': 'Wähle ein Issue aus deinem verbundenen Linear-Workspace.',
'session.linearIssuePicker.searchPlaceholder': 'Nach Titel, Kennung oder Linear-URL suchen',
'session.linearIssuePicker.empty.notConnected': 'Linear ist nicht verbunden. Verbinde es unter Einstellungen → Integrationen.',
'session.linearIssuePicker.empty.runtimeUnavailable': 'Linear ist in dieser App nicht verfügbar.',
'session.linearIssuePicker.empty.noIssuesFound': 'Keine Issues gefunden',
'session.linearIssuePicker.empty.noOpenIssuesFound': 'Keine offenen Issues gefunden',
'session.linearIssuePicker.loading.issues': 'Issues werden geladen...',
'session.linearIssuePicker.loading.more': 'Wird geladen...',
'session.linearIssuePicker.actions.openSettings': 'Einstellungen öffnen',
'session.linearIssuePicker.actions.useIssue': '{identifier} verwenden',
'session.linearIssuePicker.actions.loadMore': 'Mehr laden',
'session.linearIssuePicker.actions.openInLinearAria': 'In Linear öffnen',
'session.linearIssuePicker.toast.loadMoreFailed': 'Weitere Issues konnten nicht geladen werden',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'Issue-Details konnten nicht geladen werden',
'session.linearIssuePicker.error.notConnected': 'Linear nicht verbunden',
'session.linearIssuePicker.error.runtimeUnavailable': 'Linear ist in dieser App nicht verfügbar',
'session.linearIssuePicker.error.issueNotFound': 'Issue nicht gefunden',
'chat.chatInput.actions.newSessionFromLinearIssue': 'Neue Sitzung aus Linear-Issue',
'session.linearIssuePicker.title.createSession': 'Neue Sitzung aus Linear-Issue',
'session.linearIssuePicker.description.createSession': 'Erstellt eine Sitzung im diesem Linear-Team zugeordneten Projekt, mit dem Issue als erstem Prompt.',
'session.linearIssuePicker.error.noMappedProject': 'Ordne dieses Linear-Team in Einstellungen → Integrationen einem Projekt zu',
'session.linearIssuePicker.error.noModelSelected': 'Kein Modell ausgewählt',
'session.linearIssuePicker.toast.sendContextFailed': 'Issue-Kontext konnte nicht gesendet werden',
'session.linearIssuePicker.toast.sessionCreated': 'Sitzung aus Issue erstellt',
'session.linearIssuePicker.toast.startSessionFailed': 'Sitzung konnte nicht gestartet werden',
'session.linearIssuePicker.actions.sectionTitle': 'Aktionen',
'session.linearIssuePicker.actions.toggleWorktreeAria': 'Worktree umschalten',
'session.linearIssuePicker.actions.createInWorktree': 'In Worktree erstellen',
'session.linearIssuePicker.actions.refresh': 'Aktualisieren',
'chat.workStatus.linkedIssues.openLinear': '{identifier} in Linear öffnen',
'session.newWorktree.actions.startFromLinearIssue': 'Von Linear-Issue starten',
'session.newWorktree.fromLinearIssue': 'Von {identifier}: {title}',
'session.newWorktree.error.sendLinearContextFailed': 'Linear-Kontext konnte nicht gesendet werden',
},
fr: {
'chat.chatInput.actions.linkLinearIssue': 'Lier un ticket Linear',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'Ouvrir le ticket dans Linear',
'chat.chatInput.linked.linearIssue.removeAria': 'Retirer le ticket Linear lié',
'session.linearIssuePicker.title': 'Lier un ticket Linear',
'session.linearIssuePicker.description': 'Choisissez un ticket dans votre espace Linear connecté.',
'session.linearIssuePicker.searchPlaceholder': 'Rechercher par titre, identifiant ou URL Linear',
'session.linearIssuePicker.empty.notConnected': 'Linear nest pas connecté. Connectez-le dans Paramètres → Intégrations.',
'session.linearIssuePicker.empty.runtimeUnavailable': 'Linear nest pas disponible dans cette application.',
'session.linearIssuePicker.empty.noIssuesFound': 'Aucun ticket trouvé',
'session.linearIssuePicker.empty.noOpenIssuesFound': 'Aucun ticket ouvert trouvé',
'session.linearIssuePicker.loading.issues': 'Chargement des tickets...',
'session.linearIssuePicker.loading.more': 'Chargement...',
'session.linearIssuePicker.actions.openSettings': 'Ouvrir les paramètres',
'session.linearIssuePicker.actions.useIssue': 'Utiliser {identifier}',
'session.linearIssuePicker.actions.loadMore': 'Charger plus',
'session.linearIssuePicker.actions.openInLinearAria': 'Ouvrir dans Linear',
'session.linearIssuePicker.toast.loadMoreFailed': 'Impossible de charger dautres tickets',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'Impossible de charger les détails du ticket',
'session.linearIssuePicker.error.notConnected': 'Linear non connecté',
'session.linearIssuePicker.error.runtimeUnavailable': 'Linear nest pas disponible dans cette application',
'session.linearIssuePicker.error.issueNotFound': 'Ticket introuvable',
'chat.chatInput.actions.newSessionFromLinearIssue': 'Nouvelle session depuis un ticket Linear',
'session.linearIssuePicker.title.createSession': 'Nouvelle session depuis un ticket Linear',
'session.linearIssuePicker.description.createSession': 'Crée une session dans le projet associé à cette équipe Linear, avec le ticket comme premier message.',
'session.linearIssuePicker.error.noMappedProject': 'Associez cette équipe Linear à un projet dans Paramètres → Intégrations',
'session.linearIssuePicker.error.noModelSelected': 'Aucun modèle sélectionné',
'session.linearIssuePicker.toast.sendContextFailed': 'Impossible denvoyer le contexte du ticket',
'session.linearIssuePicker.toast.sessionCreated': 'Session créée depuis le ticket',
'session.linearIssuePicker.toast.startSessionFailed': 'Impossible de démarrer la session',
'session.linearIssuePicker.actions.sectionTitle': 'Actions disponibles',
'session.linearIssuePicker.actions.toggleWorktreeAria': 'Activer ou désactiver le worktree',
'session.linearIssuePicker.actions.createInWorktree': 'Créer dans un worktree',
'session.linearIssuePicker.actions.refresh': 'Actualiser',
'chat.workStatus.linkedIssues.openLinear': 'Ouvrir {identifier} dans Linear',
'session.newWorktree.actions.startFromLinearIssue': 'Démarrer depuis un ticket Linear',
'session.newWorktree.fromLinearIssue': 'Depuis {identifier} : {title}',
'session.newWorktree.error.sendLinearContextFailed': 'Impossible denvoyer le contexte Linear',
},
es: {
'chat.chatInput.actions.linkLinearIssue': 'Vincular issue de Linear',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'Abrir issue en Linear',
'chat.chatInput.linked.linearIssue.removeAria': 'Quitar issue de Linear vinculado',
'session.linearIssuePicker.title': 'Vincular issue de Linear',
'session.linearIssuePicker.description': 'Elige un issue del espacio de Linear conectado.',
'session.linearIssuePicker.searchPlaceholder': 'Buscar por título, identificador o URL de Linear',
'session.linearIssuePicker.empty.notConnected': 'Linear no está conectado. Conéctalo en Ajustes → Integraciones.',
'session.linearIssuePicker.empty.runtimeUnavailable': 'Linear no está disponible en esta aplicación.',
'session.linearIssuePicker.empty.noIssuesFound': 'No se encontraron issues',
'session.linearIssuePicker.empty.noOpenIssuesFound': 'No se encontraron issues abiertos',
'session.linearIssuePicker.loading.issues': 'Cargando issues...',
'session.linearIssuePicker.loading.more': 'Cargando...',
'session.linearIssuePicker.actions.openSettings': 'Abrir ajustes',
'session.linearIssuePicker.actions.useIssue': 'Usar {identifier}',
'session.linearIssuePicker.actions.loadMore': 'Cargar más',
'session.linearIssuePicker.actions.openInLinearAria': 'Abrir en Linear',
'session.linearIssuePicker.toast.loadMoreFailed': 'No se pudieron cargar más issues',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'No se pudieron cargar los detalles del issue',
'session.linearIssuePicker.error.notConnected': 'Linear no conectado',
'session.linearIssuePicker.error.runtimeUnavailable': 'Linear no está disponible en esta aplicación',
'session.linearIssuePicker.error.issueNotFound': 'Issue no encontrado',
'chat.chatInput.actions.newSessionFromLinearIssue': 'Nueva sesión desde un issue de Linear',
'session.linearIssuePicker.title.createSession': 'Nueva sesión desde un issue de Linear',
'session.linearIssuePicker.description.createSession': 'Crea una sesión en el proyecto asignado a este equipo de Linear, con el issue como primer mensaje.',
'session.linearIssuePicker.error.noMappedProject': 'Asigna este equipo de Linear a un proyecto en Ajustes → Integraciones',
'session.linearIssuePicker.error.noModelSelected': 'Ningún modelo seleccionado',
'session.linearIssuePicker.toast.sendContextFailed': 'No se pudo enviar el contexto del issue',
'session.linearIssuePicker.toast.sessionCreated': 'Sesión creada desde el issue',
'session.linearIssuePicker.toast.startSessionFailed': 'No se pudo iniciar la sesión',
'session.linearIssuePicker.actions.sectionTitle': 'Acciones',
'session.linearIssuePicker.actions.toggleWorktreeAria': 'Activar o desactivar worktree',
'session.linearIssuePicker.actions.createInWorktree': 'Crear en worktree',
'session.linearIssuePicker.actions.refresh': 'Actualizar',
'chat.workStatus.linkedIssues.openLinear': 'Abrir {identifier} en Linear',
'session.newWorktree.actions.startFromLinearIssue': 'Empezar desde un issue de Linear',
'session.newWorktree.fromLinearIssue': 'Desde {identifier}: {title}',
'session.newWorktree.error.sendLinearContextFailed': 'No se pudo enviar el contexto de Linear',
},
ja: {
'chat.chatInput.actions.linkLinearIssue': 'Linear Issueをリンク',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'LinearでIssueを開く',
'chat.chatInput.linked.linearIssue.removeAria': 'リンクしたLinear Issueを削除',
'session.linearIssuePicker.title': 'Linear Issueをリンク',
'session.linearIssuePicker.description': '接続中のLinearワークスペースからIssueを選びます。',
'session.linearIssuePicker.searchPlaceholder': 'タイトル、識別子、またはLinearのURLで検索',
'session.linearIssuePicker.empty.notConnected': 'Linearは未接続です。設定 → 連携 で接続してください。',
'session.linearIssuePicker.empty.runtimeUnavailable': 'このアプリではLinearを利用できません。',
'session.linearIssuePicker.empty.noIssuesFound': 'Issueが見つかりません',
'session.linearIssuePicker.empty.noOpenIssuesFound': '未完了のIssueはありません',
'session.linearIssuePicker.loading.issues': 'Issueを読み込み中...',
'session.linearIssuePicker.loading.more': '読み込み中...',
'session.linearIssuePicker.actions.openSettings': '設定を開く',
'session.linearIssuePicker.actions.useIssue': '{identifier} を使う',
'session.linearIssuePicker.actions.loadMore': 'さらに読み込む',
'session.linearIssuePicker.actions.openInLinearAria': 'Linearで開く',
'session.linearIssuePicker.toast.loadMoreFailed': 'これ以上のIssueを読み込めませんでした',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'Issueの詳細を読み込めませんでした',
'session.linearIssuePicker.error.notConnected': 'Linear未接続',
'session.linearIssuePicker.error.runtimeUnavailable': 'このアプリではLinearを利用できません',
'session.linearIssuePicker.error.issueNotFound': 'Issueが見つかりません',
'chat.chatInput.actions.newSessionFromLinearIssue': 'Linear Issueから新しいセッション',
'session.linearIssuePicker.title.createSession': 'Linear Issueから新しいセッション',
'session.linearIssuePicker.description.createSession': 'このLinearチームに割り当てたプロジェクトでセッションを作り、Issueを最初のプロンプトにします。',
'session.linearIssuePicker.error.noMappedProject': '設定 → 連携 でこのLinearチームをプロジェクトに割り当ててください',
'session.linearIssuePicker.error.noModelSelected': 'モデルが選択されていません',
'session.linearIssuePicker.toast.sendContextFailed': 'Issueのコンテキストを送信できませんでした',
'session.linearIssuePicker.toast.sessionCreated': 'Issueからセッションを作成しました',
'session.linearIssuePicker.toast.startSessionFailed': 'セッションを開始できませんでした',
'session.linearIssuePicker.actions.sectionTitle': '操作',
'session.linearIssuePicker.actions.toggleWorktreeAria': 'ワークツリーを切り替え',
'session.linearIssuePicker.actions.createInWorktree': 'ワークツリーで作成',
'session.linearIssuePicker.actions.refresh': '更新',
'chat.workStatus.linkedIssues.openLinear': 'Linearで {identifier} を開く',
'session.newWorktree.actions.startFromLinearIssue': 'Linear Issueから開始',
'session.newWorktree.fromLinearIssue': '{identifier}: {title}から',
'session.newWorktree.error.sendLinearContextFailed': 'Linearのコンテキストを送信できませんでした',
},
'pt-BR': {
'chat.chatInput.actions.linkLinearIssue': 'Vincular issue do Linear',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'Abrir issue no Linear',
'chat.chatInput.linked.linearIssue.removeAria': 'Remover issue do Linear vinculada',
'session.linearIssuePicker.title': 'Vincular issue do Linear',
'session.linearIssuePicker.description': 'Selecione uma issue do espaço Linear conectado.',
'session.linearIssuePicker.searchPlaceholder': 'Buscar por título, identificador ou URL do Linear',
'session.linearIssuePicker.empty.notConnected': 'O Linear não está conectado. Conecte em Configurações → Integrações.',
'session.linearIssuePicker.empty.runtimeUnavailable': 'O Linear não está disponível neste app.',
'session.linearIssuePicker.empty.noIssuesFound': 'Nenhuma issue encontrada',
'session.linearIssuePicker.empty.noOpenIssuesFound': 'Nenhuma issue aberta encontrada',
'session.linearIssuePicker.loading.issues': 'Carregando issues...',
'session.linearIssuePicker.loading.more': 'Carregando...',
'session.linearIssuePicker.actions.openSettings': 'Abrir configurações',
'session.linearIssuePicker.actions.useIssue': 'Usar {identifier}',
'session.linearIssuePicker.actions.loadMore': 'Carregar mais',
'session.linearIssuePicker.actions.openInLinearAria': 'Abrir no Linear',
'session.linearIssuePicker.toast.loadMoreFailed': 'Não foi possível carregar mais issues',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'Não foi possível carregar os detalhes da issue',
'session.linearIssuePicker.error.notConnected': 'Linear não conectado',
'session.linearIssuePicker.error.runtimeUnavailable': 'O Linear não está disponível neste app',
'session.linearIssuePicker.error.issueNotFound': 'Issue não encontrada',
'chat.chatInput.actions.newSessionFromLinearIssue': 'Nova sessão a partir de uma issue do Linear',
'session.linearIssuePicker.title.createSession': 'Nova sessão a partir de uma issue do Linear',
'session.linearIssuePicker.description.createSession': 'Cria uma sessão no projeto associado a esta equipe do Linear, com a issue como o primeiro prompt.',
'session.linearIssuePicker.error.noMappedProject': 'Associe esta equipe do Linear a um projeto em Configurações → Integrações',
'session.linearIssuePicker.error.noModelSelected': 'Nenhum modelo selecionado',
'session.linearIssuePicker.toast.sendContextFailed': 'Não foi possível enviar o contexto da issue',
'session.linearIssuePicker.toast.sessionCreated': 'Sessão criada a partir da issue',
'session.linearIssuePicker.toast.startSessionFailed': 'Não foi possível iniciar a sessão',
'session.linearIssuePicker.actions.sectionTitle': 'Ações',
'session.linearIssuePicker.actions.toggleWorktreeAria': 'Ativar ou desativar worktree',
'session.linearIssuePicker.actions.createInWorktree': 'Criar em worktree',
'session.linearIssuePicker.actions.refresh': 'Atualizar',
'chat.workStatus.linkedIssues.openLinear': 'Abrir {identifier} no Linear',
'session.newWorktree.actions.startFromLinearIssue': 'Começar a partir de uma issue do Linear',
'session.newWorktree.fromLinearIssue': 'De {identifier}: {title}',
'session.newWorktree.error.sendLinearContextFailed': 'Não foi possível enviar o contexto do Linear',
},
uk: {
'chat.chatInput.actions.linkLinearIssue': 'Прив’язати Linear issue',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'Відкрити issue в Linear',
'chat.chatInput.linked.linearIssue.removeAria': 'Прибрати прив’язаний Linear issue',
'session.linearIssuePicker.title': 'Прив’язати Linear issue',
'session.linearIssuePicker.description': 'Оберіть issue з підключеного робочого простору Linear.',
'session.linearIssuePicker.searchPlaceholder': 'Пошук за назвою, ідентифікатором або URL Linear',
'session.linearIssuePicker.empty.notConnected': 'Linear не підключено. Підключіть його в Налаштуваннях → Інтеграції.',
'session.linearIssuePicker.empty.runtimeUnavailable': 'Linear недоступний у цьому застосунку.',
'session.linearIssuePicker.empty.noIssuesFound': 'Issue не знайдено',
'session.linearIssuePicker.empty.noOpenIssuesFound': 'Відкритих issue немає',
'session.linearIssuePicker.loading.issues': 'Завантаження issue...',
'session.linearIssuePicker.loading.more': 'Завантаження...',
'session.linearIssuePicker.actions.openSettings': 'Відкрити налаштування',
'session.linearIssuePicker.actions.useIssue': 'Використати {identifier}',
'session.linearIssuePicker.actions.loadMore': 'Завантажити ще',
'session.linearIssuePicker.actions.openInLinearAria': 'Відкрити в Linear',
'session.linearIssuePicker.toast.loadMoreFailed': 'Не вдалося завантажити більше issue',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'Не вдалося завантажити деталі issue',
'session.linearIssuePicker.error.notConnected': 'Linear не підключено',
'session.linearIssuePicker.error.runtimeUnavailable': 'Linear недоступний у цьому застосунку',
'session.linearIssuePicker.error.issueNotFound': 'Issue не знайдено',
'chat.chatInput.actions.newSessionFromLinearIssue': 'Нова сесія з Linear issue',
'session.linearIssuePicker.title.createSession': 'Нова сесія з Linear issue',
'session.linearIssuePicker.description.createSession': 'Створює сесію в проєкті, прив’язаному до цієї команди Linear, з issue як першим запитом.',
'session.linearIssuePicker.error.noMappedProject': 'Прив’яжіть цю команду Linear до проєкту в Налаштуваннях → Інтеграції',
'session.linearIssuePicker.error.noModelSelected': 'Модель не вибрано',
'session.linearIssuePicker.toast.sendContextFailed': 'Не вдалося надіслати контекст issue',
'session.linearIssuePicker.toast.sessionCreated': 'Сесію створено з issue',
'session.linearIssuePicker.toast.startSessionFailed': 'Не вдалося почати сесію',
'session.linearIssuePicker.actions.sectionTitle': 'Дії',
'session.linearIssuePicker.actions.toggleWorktreeAria': 'Перемкнути worktree',
'session.linearIssuePicker.actions.createInWorktree': 'Створити у worktree',
'session.linearIssuePicker.actions.refresh': 'Оновити',
'chat.workStatus.linkedIssues.openLinear': 'Відкрити {identifier} у Linear',
'session.newWorktree.actions.startFromLinearIssue': 'Почати з Linear issue',
'session.newWorktree.fromLinearIssue': 'З {identifier}: {title}',
'session.newWorktree.error.sendLinearContextFailed': 'Не вдалося надіслати контекст Linear',
},
ko: {
'chat.chatInput.actions.linkLinearIssue': 'Linear 이슈 연결',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'Linear에서 이슈 열기',
'chat.chatInput.linked.linearIssue.removeAria': '연결된 Linear 이슈 제거',
'session.linearIssuePicker.title': 'Linear 이슈 연결',
'session.linearIssuePicker.description': '연결된 Linear 워크스페이스에서 이슈를 선택하세요.',
'session.linearIssuePicker.searchPlaceholder': '제목, 식별자 또는 Linear URL로 검색',
'session.linearIssuePicker.empty.notConnected': 'Linear가 연결되어 있지 않습니다. 설정 → 연동에서 연결하세요.',
'session.linearIssuePicker.empty.runtimeUnavailable': '이 앱에서는 Linear를 사용할 수 없습니다.',
'session.linearIssuePicker.empty.noIssuesFound': '이슈를 찾을 수 없습니다',
'session.linearIssuePicker.empty.noOpenIssuesFound': '열린 이슈가 없습니다',
'session.linearIssuePicker.loading.issues': '이슈를 불러오는 중...',
'session.linearIssuePicker.loading.more': '불러오는 중...',
'session.linearIssuePicker.actions.openSettings': '설정 열기',
'session.linearIssuePicker.actions.useIssue': '{identifier} 사용',
'session.linearIssuePicker.actions.loadMore': '더 보기',
'session.linearIssuePicker.actions.openInLinearAria': 'Linear에서 열기',
'session.linearIssuePicker.toast.loadMoreFailed': '이슈를 더 불러오지 못했습니다',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': '이슈 세부 정보를 불러오지 못했습니다',
'session.linearIssuePicker.error.notConnected': 'Linear가 연결되지 않음',
'session.linearIssuePicker.error.runtimeUnavailable': '이 앱에서는 Linear를 사용할 수 없습니다',
'session.linearIssuePicker.error.issueNotFound': '이슈를 찾을 수 없습니다',
'chat.chatInput.actions.newSessionFromLinearIssue': 'Linear 이슈로 새 세션 만들기',
'session.linearIssuePicker.title.createSession': 'Linear 이슈로 새 세션 만들기',
'session.linearIssuePicker.description.createSession': '이 Linear 팀에 연결한 프로젝트에서 세션을 만들고, 이슈를 첫 프롬프트로 넣습니다.',
'session.linearIssuePicker.error.noMappedProject': '설정 → 연동에서 이 Linear 팀을 프로젝트에 연결하세요',
'session.linearIssuePicker.error.noModelSelected': '모델이 선택되지 않았습니다',
'session.linearIssuePicker.toast.sendContextFailed': '이슈 컨텍스트를 보내지 못했습니다',
'session.linearIssuePicker.toast.sessionCreated': '이슈에서 세션을 만들었습니다',
'session.linearIssuePicker.toast.startSessionFailed': '세션을 시작하지 못했습니다',
'session.linearIssuePicker.actions.sectionTitle': '작업',
'session.linearIssuePicker.actions.toggleWorktreeAria': '워크트리 전환',
'session.linearIssuePicker.actions.createInWorktree': '워크트리에서 만들기',
'session.linearIssuePicker.actions.refresh': '새로고침',
'chat.workStatus.linkedIssues.openLinear': 'Linear에서 {identifier} 열기',
'session.newWorktree.actions.startFromLinearIssue': 'Linear 이슈에서 시작',
'session.newWorktree.fromLinearIssue': '{identifier}: {title}에서',
'session.newWorktree.error.sendLinearContextFailed': 'Linear 컨텍스트를 보내지 못했습니다',
},
pl: {
'chat.chatInput.actions.linkLinearIssue': 'Powiąż zgłoszenie Linear',
'chat.chatInput.linked.linearIssue.openInBrowserAria': 'Otwórz zgłoszenie w Linear',
'chat.chatInput.linked.linearIssue.removeAria': 'Usuń powiązane zgłoszenie Linear',
'session.linearIssuePicker.title': 'Powiąż zgłoszenie Linear',
'session.linearIssuePicker.description': 'Wybierz zgłoszenie z połączonego obszaru Linear.',
'session.linearIssuePicker.searchPlaceholder': 'Szukaj po tytule, identyfikatorze lub adresie URL Linear',
'session.linearIssuePicker.empty.notConnected': 'Linear nie jest połączony. Połącz go w Ustawieniach → Integracje.',
'session.linearIssuePicker.empty.runtimeUnavailable': 'Linear jest niedostępny w tej aplikacji.',
'session.linearIssuePicker.empty.noIssuesFound': 'Nie znaleziono zgłoszeń',
'session.linearIssuePicker.empty.noOpenIssuesFound': 'Nie znaleziono otwartych zgłoszeń',
'session.linearIssuePicker.loading.issues': 'Ładowanie zgłoszeń...',
'session.linearIssuePicker.loading.more': 'Ładowanie...',
'session.linearIssuePicker.actions.openSettings': 'Otwórz ustawienia',
'session.linearIssuePicker.actions.useIssue': 'Użyj {identifier}',
'session.linearIssuePicker.actions.loadMore': 'Załaduj więcej',
'session.linearIssuePicker.actions.openInLinearAria': 'Otwórz w Linear',
'session.linearIssuePicker.toast.loadMoreFailed': 'Nie udało się załadować kolejnych zgłoszeń',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'Nie udało się załadować szczegółów zgłoszenia',
'session.linearIssuePicker.error.notConnected': 'Linear niepołączony',
'session.linearIssuePicker.error.runtimeUnavailable': 'Linear jest niedostępny w tej aplikacji',
'session.linearIssuePicker.error.issueNotFound': 'Nie znaleziono zgłoszenia',
'chat.chatInput.actions.newSessionFromLinearIssue': 'Nowa sesja ze zgłoszenia Linear',
'session.linearIssuePicker.title.createSession': 'Nowa sesja ze zgłoszenia Linear',
'session.linearIssuePicker.description.createSession': 'Tworzy sesję w projekcie przypisanym do tego zespołu Linear, ze zgłoszeniem jako pierwszym poleceniem.',
'session.linearIssuePicker.error.noMappedProject': 'Przypisz ten zespół Linear do projektu w Ustawieniach → Integracje',
'session.linearIssuePicker.error.noModelSelected': 'Nie wybrano modelu',
'session.linearIssuePicker.toast.sendContextFailed': 'Nie udało się wysłać kontekstu zgłoszenia',
'session.linearIssuePicker.toast.sessionCreated': 'Utworzono sesję ze zgłoszenia',
'session.linearIssuePicker.toast.startSessionFailed': 'Nie udało się rozpocząć sesji',
'session.linearIssuePicker.actions.sectionTitle': 'Czynności',
'session.linearIssuePicker.actions.toggleWorktreeAria': 'Przełącz worktree',
'session.linearIssuePicker.actions.createInWorktree': 'Utwórz w worktree',
'session.linearIssuePicker.actions.refresh': 'Odśwież',
'chat.workStatus.linkedIssues.openLinear': 'Otwórz {identifier} w Linear',
'session.newWorktree.actions.startFromLinearIssue': 'Zacznij od zgłoszenia Linear',
'session.newWorktree.fromLinearIssue': 'Z {identifier}: {title}',
'session.newWorktree.error.sendLinearContextFailed': 'Nie udało się wysłać kontekstu Linear',
},
'zh-CN': {
'chat.chatInput.actions.linkLinearIssue': '关联 Linear Issue',
'chat.chatInput.linked.linearIssue.openInBrowserAria': '在 Linear 中打开 Issue',
'chat.chatInput.linked.linearIssue.removeAria': '移除已关联的 Linear Issue',
'session.linearIssuePicker.title': '关联 Linear Issue',
'session.linearIssuePicker.description': '从已连接的 Linear 工作区选择一个 Issue。',
'session.linearIssuePicker.searchPlaceholder': '按标题、标识符或 Linear 链接搜索',
'session.linearIssuePicker.empty.notConnected': '尚未连接 Linear。请到设置 → 集成 中连接。',
'session.linearIssuePicker.empty.runtimeUnavailable': '此应用中无法使用 Linear。',
'session.linearIssuePicker.empty.noIssuesFound': '未找到 Issue',
'session.linearIssuePicker.empty.noOpenIssuesFound': '没有未完成的 Issue',
'session.linearIssuePicker.loading.issues': '正在加载 Issue...',
'session.linearIssuePicker.loading.more': '正在加载...',
'session.linearIssuePicker.actions.openSettings': '打开设置',
'session.linearIssuePicker.actions.useIssue': '使用 {identifier}',
'session.linearIssuePicker.actions.loadMore': '加载更多',
'session.linearIssuePicker.actions.openInLinearAria': '在 Linear 中打开',
'session.linearIssuePicker.toast.loadMoreFailed': '无法加载更多 Issue',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': '无法加载 Issue 详情',
'session.linearIssuePicker.error.notConnected': '未连接 Linear',
'session.linearIssuePicker.error.runtimeUnavailable': '此应用中无法使用 Linear',
'session.linearIssuePicker.error.issueNotFound': '未找到 Issue',
'chat.chatInput.actions.newSessionFromLinearIssue': '从 Linear Issue 新建会话',
'session.linearIssuePicker.title.createSession': '从 Linear Issue 新建会话',
'session.linearIssuePicker.description.createSession': '在映射到此 Linear 团队的项目中创建会话,并以该 Issue 作为第一条提示。',
'session.linearIssuePicker.error.noMappedProject': '请在设置 → 集成 中将此 Linear 团队映射到一个项目',
'session.linearIssuePicker.error.noModelSelected': '未选择模型',
'session.linearIssuePicker.toast.sendContextFailed': '无法发送 Issue 上下文',
'session.linearIssuePicker.toast.sessionCreated': '已从 Issue 创建会话',
'session.linearIssuePicker.toast.startSessionFailed': '无法开始会话',
'session.linearIssuePicker.actions.sectionTitle': '操作',
'session.linearIssuePicker.actions.toggleWorktreeAria': '切换 worktree',
'session.linearIssuePicker.actions.createInWorktree': '在 worktree 中创建',
'session.linearIssuePicker.actions.refresh': '刷新',
'chat.workStatus.linkedIssues.openLinear': '在 Linear 中打开 {identifier}',
'session.newWorktree.actions.startFromLinearIssue': '从 Linear Issue 开始',
'session.newWorktree.fromLinearIssue': '来自 {identifier}{title}',
'session.newWorktree.error.sendLinearContextFailed': '无法发送 Linear 上下文',
},
'zh-TW': {
'chat.chatInput.actions.linkLinearIssue': '關聯 Linear Issue',
'chat.chatInput.linked.linearIssue.openInBrowserAria': '在 Linear 中開啟 Issue',
'chat.chatInput.linked.linearIssue.removeAria': '移除已關聯的 Linear Issue',
'session.linearIssuePicker.title': '關聯 Linear Issue',
'session.linearIssuePicker.description': '從已連線的 Linear 工作區選擇一個 Issue。',
'session.linearIssuePicker.searchPlaceholder': '依標題、識別碼或 Linear 網址搜尋',
'session.linearIssuePicker.empty.notConnected': '尚未連線 Linear。請到設定 → 整合 中連線。',
'session.linearIssuePicker.empty.runtimeUnavailable': '此應用程式無法使用 Linear。',
'session.linearIssuePicker.empty.noIssuesFound': '找不到 Issue',
'session.linearIssuePicker.empty.noOpenIssuesFound': '沒有未完成的 Issue',
'session.linearIssuePicker.loading.issues': '正在載入 Issue...',
'session.linearIssuePicker.loading.more': '正在載入...',
'session.linearIssuePicker.actions.openSettings': '開啟設定',
'session.linearIssuePicker.actions.useIssue': '使用 {identifier}',
'session.linearIssuePicker.actions.loadMore': '載入更多',
'session.linearIssuePicker.actions.openInLinearAria': '在 Linear 中開啟',
'session.linearIssuePicker.toast.loadMoreFailed': '無法載入更多 Issue',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': '無法載入 Issue 詳細資料',
'session.linearIssuePicker.error.notConnected': '未連線 Linear',
'session.linearIssuePicker.error.runtimeUnavailable': '此應用程式無法使用 Linear',
'session.linearIssuePicker.error.issueNotFound': '找不到 Issue',
'chat.chatInput.actions.newSessionFromLinearIssue': '從 Linear Issue 新增會話',
'session.linearIssuePicker.title.createSession': '從 Linear Issue 新增會話',
'session.linearIssuePicker.description.createSession': '在對應到此 Linear 團隊的專案中建立會話,並以該 Issue 作為第一則提示。',
'session.linearIssuePicker.error.noMappedProject': '請在設定 → 整合 中將此 Linear 團隊對應到一個專案',
'session.linearIssuePicker.error.noModelSelected': '尚未選擇模型',
'session.linearIssuePicker.toast.sendContextFailed': '無法傳送 Issue 內容',
'session.linearIssuePicker.toast.sessionCreated': '已從 Issue 建立會話',
'session.linearIssuePicker.toast.startSessionFailed': '無法開始會話',
'session.linearIssuePicker.actions.sectionTitle': '操作',
'session.linearIssuePicker.actions.toggleWorktreeAria': '切換 worktree',
'session.linearIssuePicker.actions.createInWorktree': '在 worktree 中建立',
'session.linearIssuePicker.actions.refresh': '重新整理',
'chat.workStatus.linkedIssues.openLinear': '在 Linear 中開啟 {identifier}',
'session.newWorktree.actions.startFromLinearIssue': '從 Linear Issue 開始',
'session.newWorktree.fromLinearIssue': '來自 {identifier}{title}',
'session.newWorktree.error.sendLinearContextFailed': '無法傳送 Linear 內容',
},
tr: {
'chat.chatInput.actions.linkLinearIssue': 'Linear Issue bağla',
'chat.chatInput.linked.linearIssue.openInBrowserAria': "Issue'u Linear'da aç",
'chat.chatInput.linked.linearIssue.removeAria': "Bağlı Linear issue'u kaldır",
'session.linearIssuePicker.title': 'Linear Issue bağla',
'session.linearIssuePicker.description': 'Bağlı Linear çalışma alanından bir issue seç.',
'session.linearIssuePicker.searchPlaceholder': "Başlığa, tanımlayıcıya veya Linear URL'sine göre ara",
'session.linearIssuePicker.empty.notConnected': "Linear bağlı değil. Ayarlar → Entegrasyonlar'dan bağla.",
'session.linearIssuePicker.empty.runtimeUnavailable': 'Linear bu uygulamada kullanılamıyor.',
'session.linearIssuePicker.empty.noIssuesFound': 'Issue bulunamadı',
'session.linearIssuePicker.empty.noOpenIssuesFound': 'Açık issue bulunamadı',
'session.linearIssuePicker.loading.issues': "Issue'lar yükleniyor...",
'session.linearIssuePicker.loading.more': 'Yükleniyor...',
'session.linearIssuePicker.actions.openSettings': 'Ayarları aç',
'session.linearIssuePicker.actions.useIssue': '{identifier} kullan',
'session.linearIssuePicker.actions.loadMore': 'Daha fazla yükle',
'session.linearIssuePicker.actions.openInLinearAria': "Linear'da aç",
'session.linearIssuePicker.toast.loadMoreFailed': 'Daha fazla issue yüklenemedi',
'session.linearIssuePicker.toast.loadIssueDetailsFailed': 'Issue ayrıntıları yüklenemedi',
'session.linearIssuePicker.error.notConnected': 'Linear bağlı değil',
'session.linearIssuePicker.error.runtimeUnavailable': 'Linear bu uygulamada kullanılamıyor',
'session.linearIssuePicker.error.issueNotFound': 'Issue bulunamadı',
'chat.chatInput.actions.newSessionFromLinearIssue': "Linear Issue'dan yeni session",
'session.linearIssuePicker.title.createSession': "Linear Issue'dan yeni session",
'session.linearIssuePicker.description.createSession': 'Bu Linear ekibine eşlenen projede bir session oluşturur; ilk prompt issue olur.',
'session.linearIssuePicker.error.noMappedProject': "Bu Linear ekibini Ayarlar → Entegrasyonlar'da bir projeye eşle",
'session.linearIssuePicker.error.noModelSelected': 'Model seçilmedi',
'session.linearIssuePicker.toast.sendContextFailed': 'Issue bağlamı gönderilemedi',
'session.linearIssuePicker.toast.sessionCreated': "Issue'dan session oluşturuldu",
'session.linearIssuePicker.toast.startSessionFailed': 'Session başlatılamadı',
'session.linearIssuePicker.actions.sectionTitle': 'İşlemler',
'session.linearIssuePicker.actions.toggleWorktreeAria': "Worktree'yi aç veya kapat",
'session.linearIssuePicker.actions.createInWorktree': "Worktree'de oluştur",
'session.linearIssuePicker.actions.refresh': 'Yenile',
'chat.workStatus.linkedIssues.openLinear': "{identifier} issue'unu Linear'da aç",
'session.newWorktree.actions.startFromLinearIssue': "Linear Issue'dan başla",
'session.newWorktree.fromLinearIssue': '{identifier}: {title}',
'session.newWorktree.error.sendLinearContextFailed': 'Linear bağlamı gönderilemedi',
},
} as const;
@@ -0,0 +1,78 @@
import { describe, expect, test } from 'bun:test';
import { linearPanelI18n } from './linear-panel.i18n';
const locales = ['en', 'de', 'fr', 'es', 'ja', 'pt-BR', 'uk', 'ko', 'pl', 'zh-CN', 'zh-TW', 'tr'] as const;
const requiredKeys = [
'contextPanel.mode.linear',
'contextRail.surface.linear.description',
'contextPanel.linear.actions.backToList',
'contextPanel.linear.actions.startSession',
'contextPanel.linear.actions.closeIssue',
'contextPanel.linear.actions.closeSearch',
'contextPanel.linear.label.status',
'contextPanel.linear.label.team',
'contextPanel.linear.label.assignee',
'contextPanel.linear.label.unassigned',
'contextPanel.linear.label.priority',
'contextPanel.linear.label.labels',
'contextPanel.linear.priority.none',
'contextPanel.linear.priority.urgent',
'contextPanel.linear.priority.high',
'contextPanel.linear.priority.medium',
'contextPanel.linear.priority.low',
'contextPanel.linear.label.comments',
'contextPanel.linear.label.statusAria',
'contextPanel.linear.label.workspace',
'contextPanel.linear.label.workspaceAria',
'contextPanel.linear.filter.statusAria',
'contextPanel.linear.filter.assigneeAria',
'contextPanel.linear.filter.teamAria',
'contextPanel.linear.filter.priorityAria',
'contextPanel.linear.filter.searchAria',
'contextPanel.linear.filter.clear',
'contextPanel.linear.filter.clearAria',
'contextPanel.linear.filter.status.all',
'contextPanel.linear.filter.status.backlog',
'contextPanel.linear.filter.status.todo',
'contextPanel.linear.filter.status.started',
'contextPanel.linear.filter.status.inReview',
'contextPanel.linear.filter.status.completed',
'contextPanel.linear.filter.status.canceled',
'contextPanel.linear.filter.status.duplicate',
'contextPanel.linear.filter.assignee.any',
'contextPanel.linear.filter.assignee.me',
'contextPanel.linear.filter.team.all',
'contextPanel.linear.filter.priority.all',
'contextPanel.linear.empty.noDescription',
'contextPanel.linear.empty.noComments',
'contextPanel.linear.empty.noMatchingIssues',
'contextPanel.linear.loading.issue',
'contextPanel.linear.toast.statusUpdated',
'contextPanel.linear.toast.statusUpdateFailed',
'contextPanel.linear.toast.closeFailed',
'contextPanel.linear.toast.workspaceSwitched',
'contextPanel.linear.toast.workspaceSwitchFailed',
'contextPanel.linear.error.noCompletedState',
] as const;
const matchingEnglishAllowed = new Set<string>([
'contextPanel.mode.linear',
'contextPanel.linear.label.status',
'contextPanel.linear.label.team',
]);
describe('linear panel translations', () => {
test('provides every required key in every supported locale', () => {
const english = linearPanelI18n.en;
for (const locale of locales) {
for (const key of requiredKeys) {
const value = linearPanelI18n[locale][key];
expect(value).toBeTruthy();
if (locale !== 'en' && !matchingEnglishAllowed.has(key)) {
expect(value).not.toBe(english[key]);
}
}
}
});
});
@@ -0,0 +1,627 @@
/** Linear context-rail panel strings — merged into each locale's main dictionary. */
export const linearPanelI18n = {
en: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Browse Linear issues, change status, and start a session',
'contextPanel.linear.actions.backToList': 'Back to issues',
'contextPanel.linear.actions.startSession': 'Start session',
'contextPanel.linear.actions.closeIssue': 'Close issue',
'contextPanel.linear.actions.closeSearch': 'Close search',
'contextPanel.linear.label.status': 'Status',
'contextPanel.linear.label.team': 'Team',
'contextPanel.linear.label.assignee': 'Assignee',
'contextPanel.linear.label.unassigned': 'Unassigned',
'contextPanel.linear.label.priority': 'Priority',
'contextPanel.linear.label.labels': 'Labels',
'contextPanel.linear.priority.none': 'No priority',
'contextPanel.linear.priority.urgent': 'Urgent',
'contextPanel.linear.priority.high': 'High',
'contextPanel.linear.priority.medium': 'Medium',
'contextPanel.linear.priority.low': 'Low',
'contextPanel.linear.label.comments': 'Comments',
'contextPanel.linear.label.statusAria': 'Linear issue status',
'contextPanel.linear.label.workspace': 'Workspace',
'contextPanel.linear.label.workspaceAria': 'Linear workspace',
'contextPanel.linear.filter.statusAria': 'Filter issues by status',
'contextPanel.linear.filter.assigneeAria': 'Filter issues by assignee',
'contextPanel.linear.filter.teamAria': 'Filter issues by team',
'contextPanel.linear.filter.priorityAria': 'Filter issues by priority',
'contextPanel.linear.filter.searchAria': 'Search issues',
'contextPanel.linear.filter.clear': 'Clear',
'contextPanel.linear.filter.clearAria': 'Clear issue filters',
'contextPanel.linear.filter.status.all': 'All',
'contextPanel.linear.filter.status.backlog': 'Backlog',
'contextPanel.linear.filter.status.todo': 'To Do',
'contextPanel.linear.filter.status.started': 'In Progress',
'contextPanel.linear.filter.status.inReview': 'In Review',
'contextPanel.linear.filter.status.completed': 'Done',
'contextPanel.linear.filter.status.canceled': 'Canceled',
'contextPanel.linear.filter.status.duplicate': 'Duplicate',
'contextPanel.linear.filter.assignee.any': 'Anyone',
'contextPanel.linear.filter.assignee.me': 'Assigned to me',
'contextPanel.linear.filter.team.all': 'All teams',
'contextPanel.linear.filter.priority.all': 'All priorities',
'contextPanel.linear.empty.noDescription': 'No description',
'contextPanel.linear.empty.noComments': 'No comments',
'contextPanel.linear.empty.noMatchingIssues': 'No issues match these filters',
'contextPanel.linear.loading.issue': 'Loading issue…',
'contextPanel.linear.toast.statusUpdated': 'Issue status updated',
'contextPanel.linear.toast.statusUpdateFailed': 'Could not update issue status',
'contextPanel.linear.toast.closeFailed': 'Could not close issue',
'contextPanel.linear.toast.workspaceSwitched': 'Switched Linear workspace',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Could not switch Linear workspace',
'contextPanel.linear.error.noCompletedState': 'This team has no completed status',
},
de: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Linear-Issues durchsuchen, Status ändern und eine Sitzung starten',
'contextPanel.linear.actions.backToList': 'Zurück zu den Issues',
'contextPanel.linear.actions.startSession': 'Sitzung starten',
'contextPanel.linear.actions.closeIssue': 'Issue schließen',
'contextPanel.linear.actions.closeSearch': 'Suche schließen',
'contextPanel.linear.label.status': 'Status',
'contextPanel.linear.label.team': 'Team',
'contextPanel.linear.label.assignee': 'Zugewiesen',
'contextPanel.linear.label.unassigned': 'Nicht zugewiesen',
'contextPanel.linear.label.priority': 'Priorität',
'contextPanel.linear.label.labels': 'Kennzeichnungen',
'contextPanel.linear.priority.none': 'Keine Priorität',
'contextPanel.linear.priority.urgent': 'Dringend',
'contextPanel.linear.priority.high': 'Hoch',
'contextPanel.linear.priority.medium': 'Mittel',
'contextPanel.linear.priority.low': 'Niedrig',
'contextPanel.linear.label.comments': 'Kommentare',
'contextPanel.linear.label.statusAria': 'Status des Linear-Issues',
'contextPanel.linear.label.workspace': 'Arbeitsbereich',
'contextPanel.linear.label.workspaceAria': 'Linear-Workspace',
'contextPanel.linear.filter.statusAria': 'Issues nach Status filtern',
'contextPanel.linear.filter.assigneeAria': 'Issues nach Zuweisung filtern',
'contextPanel.linear.filter.teamAria': 'Issues nach Team filtern',
'contextPanel.linear.filter.priorityAria': 'Issues nach Priorität filtern',
'contextPanel.linear.filter.searchAria': 'Issues durchsuchen',
'contextPanel.linear.filter.clear': 'Zurücksetzen',
'contextPanel.linear.filter.clearAria': 'Issue-Filter zurücksetzen',
'contextPanel.linear.filter.status.all': 'Alle',
'contextPanel.linear.filter.status.backlog': 'Warteliste',
'contextPanel.linear.filter.status.todo': 'Zu tun',
'contextPanel.linear.filter.status.started': 'In Bearbeitung',
'contextPanel.linear.filter.status.inReview': 'In Prüfung',
'contextPanel.linear.filter.status.completed': 'Erledigt',
'contextPanel.linear.filter.status.canceled': 'Abgebrochen',
'contextPanel.linear.filter.status.duplicate': 'Duplikat',
'contextPanel.linear.filter.assignee.any': 'Alle Personen',
'contextPanel.linear.filter.assignee.me': 'Mir zugewiesen',
'contextPanel.linear.filter.team.all': 'Alle Teams',
'contextPanel.linear.filter.priority.all': 'Alle Prioritäten',
'contextPanel.linear.empty.noDescription': 'Keine Beschreibung',
'contextPanel.linear.empty.noComments': 'Keine Kommentare',
'contextPanel.linear.empty.noMatchingIssues': 'Keine Issues passen zu diesen Filtern',
'contextPanel.linear.loading.issue': 'Issue wird geladen…',
'contextPanel.linear.toast.statusUpdated': 'Issue-Status aktualisiert',
'contextPanel.linear.toast.statusUpdateFailed': 'Issue-Status konnte nicht aktualisiert werden',
'contextPanel.linear.toast.closeFailed': 'Issue konnte nicht geschlossen werden',
'contextPanel.linear.toast.workspaceSwitched': 'Linear-Workspace gewechselt',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Linear-Workspace konnte nicht gewechselt werden',
'contextPanel.linear.error.noCompletedState': 'Dieses Team hat keinen erledigten Status',
},
fr: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Parcourir les tickets Linear, changer le statut et démarrer une session',
'contextPanel.linear.actions.backToList': 'Retour aux tickets',
'contextPanel.linear.actions.startSession': 'Démarrer une session',
'contextPanel.linear.actions.closeIssue': 'Fermer le ticket',
'contextPanel.linear.actions.closeSearch': 'Fermer la recherche',
'contextPanel.linear.label.status': 'Statut',
'contextPanel.linear.label.team': 'Équipe',
'contextPanel.linear.label.assignee': 'Assigné',
'contextPanel.linear.label.unassigned': 'Non assigné',
'contextPanel.linear.label.priority': 'Priorité',
'contextPanel.linear.label.labels': 'Libellés',
'contextPanel.linear.priority.none': 'Sans priorité',
'contextPanel.linear.priority.urgent': 'Urgente',
'contextPanel.linear.priority.high': 'Haute',
'contextPanel.linear.priority.medium': 'Moyenne',
'contextPanel.linear.priority.low': 'Basse',
'contextPanel.linear.label.comments': 'Commentaires',
'contextPanel.linear.label.statusAria': 'Statut du ticket Linear',
'contextPanel.linear.label.workspace': 'Espace de travail',
'contextPanel.linear.label.workspaceAria': 'Espace de travail Linear',
'contextPanel.linear.filter.statusAria': 'Filtrer les tickets par statut',
'contextPanel.linear.filter.assigneeAria': 'Filtrer les tickets par assigné',
'contextPanel.linear.filter.teamAria': 'Filtrer les tickets par équipe',
'contextPanel.linear.filter.priorityAria': 'Filtrer les tickets par priorité',
'contextPanel.linear.filter.searchAria': 'Rechercher des tickets',
'contextPanel.linear.filter.clear': 'Effacer',
'contextPanel.linear.filter.clearAria': 'Effacer les filtres des tickets',
'contextPanel.linear.filter.status.all': 'Tous',
'contextPanel.linear.filter.status.backlog': 'Liste dattente',
'contextPanel.linear.filter.status.todo': 'À faire',
'contextPanel.linear.filter.status.started': 'En cours',
'contextPanel.linear.filter.status.inReview': 'En revue',
'contextPanel.linear.filter.status.completed': 'Terminé',
'contextPanel.linear.filter.status.canceled': 'Annulé',
'contextPanel.linear.filter.status.duplicate': 'Doublon',
'contextPanel.linear.filter.assignee.any': 'Tout le monde',
'contextPanel.linear.filter.assignee.me': 'Assignés à moi',
'contextPanel.linear.filter.team.all': 'Toutes les équipes',
'contextPanel.linear.filter.priority.all': 'Toutes les priorités',
'contextPanel.linear.empty.noDescription': 'Aucune description',
'contextPanel.linear.empty.noComments': 'Aucun commentaire',
'contextPanel.linear.empty.noMatchingIssues': 'Aucun ticket ne correspond à ces filtres',
'contextPanel.linear.loading.issue': 'Chargement du ticket…',
'contextPanel.linear.toast.statusUpdated': 'Statut du ticket mis à jour',
'contextPanel.linear.toast.statusUpdateFailed': 'Impossible de mettre à jour le statut du ticket',
'contextPanel.linear.toast.closeFailed': 'Impossible de fermer le ticket',
'contextPanel.linear.toast.workspaceSwitched': 'Workspace Linear modifié',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Impossible de changer de workspace Linear',
'contextPanel.linear.error.noCompletedState': 'Cette équipe na pas de statut terminé',
},
es: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Explora issues de Linear, cambia el estado e inicia una sesión',
'contextPanel.linear.actions.backToList': 'Volver a los issues',
'contextPanel.linear.actions.startSession': 'Iniciar sesión',
'contextPanel.linear.actions.closeIssue': 'Cerrar issue',
'contextPanel.linear.actions.closeSearch': 'Cerrar búsqueda',
'contextPanel.linear.label.status': 'Estado',
'contextPanel.linear.label.team': 'Equipo',
'contextPanel.linear.label.assignee': 'Asignado',
'contextPanel.linear.label.unassigned': 'Sin asignar',
'contextPanel.linear.label.priority': 'Prioridad',
'contextPanel.linear.label.labels': 'Etiquetas',
'contextPanel.linear.priority.none': 'Sin prioridad',
'contextPanel.linear.priority.urgent': 'Urgente',
'contextPanel.linear.priority.high': 'Alta',
'contextPanel.linear.priority.medium': 'Media',
'contextPanel.linear.priority.low': 'Baja',
'contextPanel.linear.label.comments': 'Comentarios',
'contextPanel.linear.label.statusAria': 'Estado del issue de Linear',
'contextPanel.linear.label.workspace': 'Espacio de trabajo',
'contextPanel.linear.label.workspaceAria': 'Espacio de trabajo de Linear',
'contextPanel.linear.filter.statusAria': 'Filtrar issues por estado',
'contextPanel.linear.filter.assigneeAria': 'Filtrar issues por asignado',
'contextPanel.linear.filter.teamAria': 'Filtrar issues por equipo',
'contextPanel.linear.filter.priorityAria': 'Filtrar issues por prioridad',
'contextPanel.linear.filter.searchAria': 'Buscar issues',
'contextPanel.linear.filter.clear': 'Borrar',
'contextPanel.linear.filter.clearAria': 'Borrar filtros de issues',
'contextPanel.linear.filter.status.all': 'Todos',
'contextPanel.linear.filter.status.backlog': 'Lista de espera',
'contextPanel.linear.filter.status.todo': 'Por hacer',
'contextPanel.linear.filter.status.started': 'En curso',
'contextPanel.linear.filter.status.inReview': 'En revisión',
'contextPanel.linear.filter.status.completed': 'Hecho',
'contextPanel.linear.filter.status.canceled': 'Cancelado',
'contextPanel.linear.filter.status.duplicate': 'Duplicado',
'contextPanel.linear.filter.assignee.any': 'Cualquiera',
'contextPanel.linear.filter.assignee.me': 'Asignados a mí',
'contextPanel.linear.filter.team.all': 'Todos los equipos',
'contextPanel.linear.filter.priority.all': 'Todas las prioridades',
'contextPanel.linear.empty.noDescription': 'Sin descripción',
'contextPanel.linear.empty.noComments': 'Sin comentarios',
'contextPanel.linear.empty.noMatchingIssues': 'Ningún issue coincide con estos filtros',
'contextPanel.linear.loading.issue': 'Cargando issue…',
'contextPanel.linear.toast.statusUpdated': 'Estado del issue actualizado',
'contextPanel.linear.toast.statusUpdateFailed': 'No se pudo actualizar el estado del issue',
'contextPanel.linear.toast.closeFailed': 'No se pudo cerrar el issue',
'contextPanel.linear.toast.workspaceSwitched': 'Workspace de Linear cambiado',
'contextPanel.linear.toast.workspaceSwitchFailed': 'No se pudo cambiar el workspace de Linear',
'contextPanel.linear.error.noCompletedState': 'Este equipo no tiene un estado completado',
},
ja: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Linear の Issue を一覧し、状態を変えてセッションを開始します',
'contextPanel.linear.actions.backToList': 'Issue 一覧に戻る',
'contextPanel.linear.actions.startSession': 'セッションを開始',
'contextPanel.linear.actions.closeIssue': 'Issue をクローズ',
'contextPanel.linear.actions.closeSearch': '検索を閉じる',
'contextPanel.linear.label.status': '状態',
'contextPanel.linear.label.team': 'チーム',
'contextPanel.linear.label.assignee': '担当者',
'contextPanel.linear.label.unassigned': '未割り当て',
'contextPanel.linear.label.priority': '優先度',
'contextPanel.linear.label.labels': 'ラベル',
'contextPanel.linear.priority.none': '優先度なし',
'contextPanel.linear.priority.urgent': '緊急',
'contextPanel.linear.priority.high': '高',
'contextPanel.linear.priority.medium': '中',
'contextPanel.linear.priority.low': '低',
'contextPanel.linear.label.comments': 'コメント',
'contextPanel.linear.label.statusAria': 'Linear Issue の状態',
'contextPanel.linear.label.workspace': 'ワークスペース',
'contextPanel.linear.label.workspaceAria': 'Linear ワークスペース',
'contextPanel.linear.filter.statusAria': '状態で Issue を絞り込む',
'contextPanel.linear.filter.assigneeAria': '担当者で Issue を絞り込む',
'contextPanel.linear.filter.teamAria': 'チームで Issue を絞り込む',
'contextPanel.linear.filter.priorityAria': '優先度で Issue を絞り込む',
'contextPanel.linear.filter.searchAria': 'Issue を検索',
'contextPanel.linear.filter.clear': 'クリア',
'contextPanel.linear.filter.clearAria': 'Issue フィルターをクリア',
'contextPanel.linear.filter.status.all': 'すべて',
'contextPanel.linear.filter.status.backlog': 'バックログ',
'contextPanel.linear.filter.status.todo': '未着手',
'contextPanel.linear.filter.status.started': '進行中',
'contextPanel.linear.filter.status.inReview': 'レビュー中',
'contextPanel.linear.filter.status.completed': '完了',
'contextPanel.linear.filter.status.canceled': 'キャンセル',
'contextPanel.linear.filter.status.duplicate': '重複',
'contextPanel.linear.filter.assignee.any': '全員',
'contextPanel.linear.filter.assignee.me': '自分に割り当て',
'contextPanel.linear.filter.team.all': 'すべてのチーム',
'contextPanel.linear.filter.priority.all': 'すべての優先度',
'contextPanel.linear.empty.noDescription': '説明はありません',
'contextPanel.linear.empty.noComments': 'コメントはありません',
'contextPanel.linear.empty.noMatchingIssues': 'この条件に合う Issue はありません',
'contextPanel.linear.loading.issue': 'Issue を読み込み中…',
'contextPanel.linear.toast.statusUpdated': 'Issue の状態を更新しました',
'contextPanel.linear.toast.statusUpdateFailed': 'Issue の状態を更新できませんでした',
'contextPanel.linear.toast.closeFailed': 'Issue をクローズできませんでした',
'contextPanel.linear.toast.workspaceSwitched': 'Linear ワークスペースを切り替えました',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Linear ワークスペースを切り替えられませんでした',
'contextPanel.linear.error.noCompletedState': 'このチームには完了ステータスがありません',
},
ko: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Linear 이슈를 보고 상태를 바꾼 뒤 세션을 시작합니다',
'contextPanel.linear.actions.backToList': '이슈 목록으로',
'contextPanel.linear.actions.startSession': '세션 시작',
'contextPanel.linear.actions.closeIssue': '이슈 닫기',
'contextPanel.linear.actions.closeSearch': '검색 닫기',
'contextPanel.linear.label.status': '상태',
'contextPanel.linear.label.team': '팀',
'contextPanel.linear.label.assignee': '담당자',
'contextPanel.linear.label.unassigned': '담당자 없음',
'contextPanel.linear.label.priority': '우선순위',
'contextPanel.linear.label.labels': '레이블',
'contextPanel.linear.priority.none': '우선순위 없음',
'contextPanel.linear.priority.urgent': '긴급',
'contextPanel.linear.priority.high': '높음',
'contextPanel.linear.priority.medium': '보통',
'contextPanel.linear.priority.low': '낮음',
'contextPanel.linear.label.comments': '댓글',
'contextPanel.linear.label.statusAria': 'Linear 이슈 상태',
'contextPanel.linear.label.workspace': '워크스페이스',
'contextPanel.linear.label.workspaceAria': 'Linear 워크스페이스',
'contextPanel.linear.filter.statusAria': '상태로 이슈 필터',
'contextPanel.linear.filter.assigneeAria': '담당자로 이슈 필터',
'contextPanel.linear.filter.teamAria': '팀으로 이슈 필터',
'contextPanel.linear.filter.priorityAria': '우선순위로 이슈 필터',
'contextPanel.linear.filter.searchAria': '이슈 검색',
'contextPanel.linear.filter.clear': '지우기',
'contextPanel.linear.filter.clearAria': '이슈 필터 지우기',
'contextPanel.linear.filter.status.all': '전체',
'contextPanel.linear.filter.status.backlog': '백로그',
'contextPanel.linear.filter.status.todo': '할 일',
'contextPanel.linear.filter.status.started': '작업 중',
'contextPanel.linear.filter.status.inReview': '검토 중',
'contextPanel.linear.filter.status.completed': '완료',
'contextPanel.linear.filter.status.canceled': '취소됨',
'contextPanel.linear.filter.status.duplicate': '중복',
'contextPanel.linear.filter.assignee.any': '누구나',
'contextPanel.linear.filter.assignee.me': '내게 할당됨',
'contextPanel.linear.filter.team.all': '모든 팀',
'contextPanel.linear.filter.priority.all': '모든 우선순위',
'contextPanel.linear.empty.noDescription': '설명이 없습니다',
'contextPanel.linear.empty.noComments': '댓글이 없습니다',
'contextPanel.linear.empty.noMatchingIssues': '이 필터에 맞는 이슈가 없습니다',
'contextPanel.linear.loading.issue': '이슈를 불러오는 중…',
'contextPanel.linear.toast.statusUpdated': '이슈 상태를 업데이트했습니다',
'contextPanel.linear.toast.statusUpdateFailed': '이슈 상태를 업데이트하지 못했습니다',
'contextPanel.linear.toast.closeFailed': '이슈를 닫지 못했습니다',
'contextPanel.linear.toast.workspaceSwitched': 'Linear 워크스페이스를 전환했습니다',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Linear 워크스페이스를 전환하지 못했습니다',
'contextPanel.linear.error.noCompletedState': '이 팀에는 완료 상태가 없습니다',
},
pl: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Przeglądaj zgłoszenia Linear, zmieniaj status i uruchamiaj sesję',
'contextPanel.linear.actions.backToList': 'Wróć do zgłoszeń',
'contextPanel.linear.actions.startSession': 'Uruchom sesję',
'contextPanel.linear.actions.closeIssue': 'Zamknij zgłoszenie',
'contextPanel.linear.actions.closeSearch': 'Zamknij wyszukiwanie',
'contextPanel.linear.label.status': 'Status',
'contextPanel.linear.label.team': 'Zespół',
'contextPanel.linear.label.assignee': 'Przypisane',
'contextPanel.linear.label.unassigned': 'Nieprzypisane',
'contextPanel.linear.label.priority': 'Priorytet',
'contextPanel.linear.label.labels': 'Etykiety',
'contextPanel.linear.priority.none': 'Brak priorytetu',
'contextPanel.linear.priority.urgent': 'Pilne',
'contextPanel.linear.priority.high': 'Wysoki',
'contextPanel.linear.priority.medium': 'Średni',
'contextPanel.linear.priority.low': 'Niski',
'contextPanel.linear.label.comments': 'Komentarze',
'contextPanel.linear.label.statusAria': 'Status zgłoszenia Linear',
'contextPanel.linear.label.workspace': 'Obszar roboczy',
'contextPanel.linear.label.workspaceAria': 'Workspace Linear',
'contextPanel.linear.filter.statusAria': 'Filtruj zgłoszenia według statusu',
'contextPanel.linear.filter.assigneeAria': 'Filtruj zgłoszenia według osoby',
'contextPanel.linear.filter.teamAria': 'Filtruj zgłoszenia według zespołu',
'contextPanel.linear.filter.priorityAria': 'Filtruj zgłoszenia według priorytetu',
'contextPanel.linear.filter.searchAria': 'Szukaj zgłoszeń',
'contextPanel.linear.filter.clear': 'Wyczyść',
'contextPanel.linear.filter.clearAria': 'Wyczyść filtry zgłoszeń',
'contextPanel.linear.filter.status.all': 'Wszystkie',
'contextPanel.linear.filter.status.backlog': 'Lista oczekujących',
'contextPanel.linear.filter.status.todo': 'Do zrobienia',
'contextPanel.linear.filter.status.started': 'W toku',
'contextPanel.linear.filter.status.inReview': 'W recenzji',
'contextPanel.linear.filter.status.completed': 'Ukończone',
'contextPanel.linear.filter.status.canceled': 'Anulowane',
'contextPanel.linear.filter.status.duplicate': 'Duplikat',
'contextPanel.linear.filter.assignee.any': 'Ktokolwiek',
'contextPanel.linear.filter.assignee.me': 'Przypisane do mnie',
'contextPanel.linear.filter.team.all': 'Wszystkie zespoły',
'contextPanel.linear.filter.priority.all': 'Wszystkie priorytety',
'contextPanel.linear.empty.noDescription': 'Brak opisu',
'contextPanel.linear.empty.noComments': 'Brak komentarzy',
'contextPanel.linear.empty.noMatchingIssues': 'Żadne zgłoszenie nie pasuje do tych filtrów',
'contextPanel.linear.loading.issue': 'Wczytywanie zgłoszenia…',
'contextPanel.linear.toast.statusUpdated': 'Zaktualizowano status zgłoszenia',
'contextPanel.linear.toast.statusUpdateFailed': 'Nie udało się zaktualizować statusu zgłoszenia',
'contextPanel.linear.toast.closeFailed': 'Nie udało się zamknąć zgłoszenia',
'contextPanel.linear.toast.workspaceSwitched': 'Przełączono workspace Linear',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Nie udało się przełączyć workspace Linear',
'contextPanel.linear.error.noCompletedState': 'Ten zespół nie ma statusu ukończenia',
},
'pt-BR': {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Navegue pelas issues do Linear, altere o status e inicie uma sessão',
'contextPanel.linear.actions.backToList': 'Voltar às issues',
'contextPanel.linear.actions.startSession': 'Iniciar sessão',
'contextPanel.linear.actions.closeIssue': 'Fechar issue',
'contextPanel.linear.actions.closeSearch': 'Fechar pesquisa',
'contextPanel.linear.label.status': 'Status',
'contextPanel.linear.label.team': 'Equipe',
'contextPanel.linear.label.assignee': 'Responsável',
'contextPanel.linear.label.unassigned': 'Sem responsável',
'contextPanel.linear.label.priority': 'Prioridade',
'contextPanel.linear.label.labels': 'Etiquetas',
'contextPanel.linear.priority.none': 'Sem prioridade',
'contextPanel.linear.priority.urgent': 'Urgente',
'contextPanel.linear.priority.high': 'Alta',
'contextPanel.linear.priority.medium': 'Média',
'contextPanel.linear.priority.low': 'Baixa',
'contextPanel.linear.label.comments': 'Comentários',
'contextPanel.linear.label.statusAria': 'Status da issue do Linear',
'contextPanel.linear.label.workspace': 'Espaço de trabalho',
'contextPanel.linear.label.workspaceAria': 'Workspace do Linear',
'contextPanel.linear.filter.statusAria': 'Filtrar issues por status',
'contextPanel.linear.filter.assigneeAria': 'Filtrar issues por responsável',
'contextPanel.linear.filter.teamAria': 'Filtrar issues por equipe',
'contextPanel.linear.filter.priorityAria': 'Filtrar issues por prioridade',
'contextPanel.linear.filter.searchAria': 'Pesquisar issues',
'contextPanel.linear.filter.clear': 'Limpar',
'contextPanel.linear.filter.clearAria': 'Limpar filtros de issues',
'contextPanel.linear.filter.status.all': 'Todas',
'contextPanel.linear.filter.status.backlog': 'Lista de espera',
'contextPanel.linear.filter.status.todo': 'A fazer',
'contextPanel.linear.filter.status.started': 'Em andamento',
'contextPanel.linear.filter.status.inReview': 'Em revisão',
'contextPanel.linear.filter.status.completed': 'Concluído',
'contextPanel.linear.filter.status.canceled': 'Cancelado',
'contextPanel.linear.filter.status.duplicate': 'Duplicado',
'contextPanel.linear.filter.assignee.any': 'Qualquer pessoa',
'contextPanel.linear.filter.assignee.me': 'Atribuídas a mim',
'contextPanel.linear.filter.team.all': 'Todas as equipes',
'contextPanel.linear.filter.priority.all': 'Todas as prioridades',
'contextPanel.linear.empty.noDescription': 'Sem descrição',
'contextPanel.linear.empty.noComments': 'Sem comentários',
'contextPanel.linear.empty.noMatchingIssues': 'Nenhuma issue corresponde a estes filtros',
'contextPanel.linear.loading.issue': 'Carregando issue…',
'contextPanel.linear.toast.statusUpdated': 'Status da issue atualizado',
'contextPanel.linear.toast.statusUpdateFailed': 'Não foi possível atualizar o status da issue',
'contextPanel.linear.toast.closeFailed': 'Não foi possível fechar a issue',
'contextPanel.linear.toast.workspaceSwitched': 'Workspace do Linear alterado',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Não foi possível alternar o workspace do Linear',
'contextPanel.linear.error.noCompletedState': 'Esta equipe não tem um status de concluído',
},
uk: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': 'Переглядайте Linear issue, змінюйте статус і запускайте сесію',
'contextPanel.linear.actions.backToList': 'Назад до issues',
'contextPanel.linear.actions.startSession': 'Почати сесію',
'contextPanel.linear.actions.closeIssue': 'Закрити issue',
'contextPanel.linear.actions.closeSearch': 'Закрити пошук',
'contextPanel.linear.label.status': 'Статус',
'contextPanel.linear.label.team': 'Команда',
'contextPanel.linear.label.assignee': 'Виконавець',
'contextPanel.linear.label.unassigned': 'Не призначено',
'contextPanel.linear.label.priority': 'Пріоритет',
'contextPanel.linear.label.labels': 'Мітки',
'contextPanel.linear.priority.none': 'Без пріоритету',
'contextPanel.linear.priority.urgent': 'Терміновий',
'contextPanel.linear.priority.high': 'Високий',
'contextPanel.linear.priority.medium': 'Середній',
'contextPanel.linear.priority.low': 'Низький',
'contextPanel.linear.label.comments': 'Коментарі',
'contextPanel.linear.label.statusAria': 'Статус Linear issue',
'contextPanel.linear.label.workspace': 'Робочий простір',
'contextPanel.linear.label.workspaceAria': 'Робочий простір Linear',
'contextPanel.linear.filter.statusAria': 'Фільтрувати issues за статусом',
'contextPanel.linear.filter.assigneeAria': 'Фільтрувати issues за виконавцем',
'contextPanel.linear.filter.teamAria': 'Фільтрувати issues за командою',
'contextPanel.linear.filter.priorityAria': 'Фільтрувати issues за пріоритетом',
'contextPanel.linear.filter.searchAria': 'Шукати issues',
'contextPanel.linear.filter.clear': 'Скинути',
'contextPanel.linear.filter.clearAria': 'Скинути фільтри issues',
'contextPanel.linear.filter.status.all': 'Усі',
'contextPanel.linear.filter.status.backlog': 'Беклог',
'contextPanel.linear.filter.status.todo': 'До виконання',
'contextPanel.linear.filter.status.started': 'У роботі',
'contextPanel.linear.filter.status.inReview': 'На перегляді',
'contextPanel.linear.filter.status.completed': 'Готово',
'contextPanel.linear.filter.status.canceled': 'Скасовано',
'contextPanel.linear.filter.status.duplicate': 'Дублікат',
'contextPanel.linear.filter.assignee.any': 'Будь-хто',
'contextPanel.linear.filter.assignee.me': 'Призначені мені',
'contextPanel.linear.filter.team.all': 'Усі команди',
'contextPanel.linear.filter.priority.all': 'Усі пріоритети',
'contextPanel.linear.empty.noDescription': 'Немає опису',
'contextPanel.linear.empty.noComments': 'Немає коментарів',
'contextPanel.linear.empty.noMatchingIssues': 'Немає issues за цими фільтрами',
'contextPanel.linear.loading.issue': 'Завантаження issue…',
'contextPanel.linear.toast.statusUpdated': 'Статус issue оновлено',
'contextPanel.linear.toast.statusUpdateFailed': 'Не вдалося оновити статус issue',
'contextPanel.linear.toast.closeFailed': 'Не вдалося закрити issue',
'contextPanel.linear.toast.workspaceSwitched': 'Перемкнуто Linear workspace',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Не вдалося перемкнути Linear workspace',
'contextPanel.linear.error.noCompletedState': 'У цієї команди немає статусу completed',
},
'zh-CN': {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': '浏览 Linear Issue、更改状态并开始会话',
'contextPanel.linear.actions.backToList': '返回 Issue 列表',
'contextPanel.linear.actions.startSession': '开始会话',
'contextPanel.linear.actions.closeIssue': '关闭 Issue',
'contextPanel.linear.actions.closeSearch': '关闭搜索',
'contextPanel.linear.label.status': '状态',
'contextPanel.linear.label.team': '团队',
'contextPanel.linear.label.assignee': '负责人',
'contextPanel.linear.label.unassigned': '未指派',
'contextPanel.linear.label.priority': '优先级',
'contextPanel.linear.label.labels': '标签',
'contextPanel.linear.priority.none': '无优先级',
'contextPanel.linear.priority.urgent': '紧急',
'contextPanel.linear.priority.high': '高',
'contextPanel.linear.priority.medium': '中',
'contextPanel.linear.priority.low': '低',
'contextPanel.linear.label.comments': '评论',
'contextPanel.linear.label.statusAria': 'Linear Issue 状态',
'contextPanel.linear.label.workspace': '工作区',
'contextPanel.linear.label.workspaceAria': 'Linear 工作区',
'contextPanel.linear.filter.statusAria': '按状态筛选 Issue',
'contextPanel.linear.filter.assigneeAria': '按负责人筛选 Issue',
'contextPanel.linear.filter.teamAria': '按团队筛选 Issue',
'contextPanel.linear.filter.priorityAria': '按优先级筛选 Issue',
'contextPanel.linear.filter.searchAria': '搜索 Issue',
'contextPanel.linear.filter.clear': '清除',
'contextPanel.linear.filter.clearAria': '清除 Issue 筛选',
'contextPanel.linear.filter.status.all': '全部',
'contextPanel.linear.filter.status.backlog': '待办池',
'contextPanel.linear.filter.status.todo': '待办',
'contextPanel.linear.filter.status.started': '进行中',
'contextPanel.linear.filter.status.inReview': '审核中',
'contextPanel.linear.filter.status.completed': '已完成',
'contextPanel.linear.filter.status.canceled': '已取消',
'contextPanel.linear.filter.status.duplicate': '重复',
'contextPanel.linear.filter.assignee.any': '任何人',
'contextPanel.linear.filter.assignee.me': '指派给我',
'contextPanel.linear.filter.team.all': '所有团队',
'contextPanel.linear.filter.priority.all': '所有优先级',
'contextPanel.linear.empty.noDescription': '没有描述',
'contextPanel.linear.empty.noComments': '没有评论',
'contextPanel.linear.empty.noMatchingIssues': '没有符合这些筛选条件的 Issue',
'contextPanel.linear.loading.issue': '正在加载 Issue…',
'contextPanel.linear.toast.statusUpdated': '已更新 Issue 状态',
'contextPanel.linear.toast.statusUpdateFailed': '无法更新 Issue 状态',
'contextPanel.linear.toast.closeFailed': '无法关闭 Issue',
'contextPanel.linear.toast.workspaceSwitched': '已切换 Linear 工作区',
'contextPanel.linear.toast.workspaceSwitchFailed': '无法切换 Linear 工作区',
'contextPanel.linear.error.noCompletedState': '此团队没有已完成状态',
},
'zh-TW': {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': '瀏覽 Linear Issue、變更狀態並開始會話',
'contextPanel.linear.actions.backToList': '返回 Issue 列表',
'contextPanel.linear.actions.startSession': '開始會話',
'contextPanel.linear.actions.closeIssue': '關閉 Issue',
'contextPanel.linear.actions.closeSearch': '關閉搜尋',
'contextPanel.linear.label.status': '狀態',
'contextPanel.linear.label.team': '團隊',
'contextPanel.linear.label.assignee': '負責人',
'contextPanel.linear.label.unassigned': '未指派',
'contextPanel.linear.label.priority': '優先級',
'contextPanel.linear.label.labels': '標籤',
'contextPanel.linear.priority.none': '無優先級',
'contextPanel.linear.priority.urgent': '緊急',
'contextPanel.linear.priority.high': '高',
'contextPanel.linear.priority.medium': '中',
'contextPanel.linear.priority.low': '低',
'contextPanel.linear.label.comments': '留言',
'contextPanel.linear.label.statusAria': 'Linear Issue 狀態',
'contextPanel.linear.label.workspace': '工作區',
'contextPanel.linear.label.workspaceAria': 'Linear 工作區',
'contextPanel.linear.filter.statusAria': '依狀態篩選 Issue',
'contextPanel.linear.filter.assigneeAria': '依負責人篩選 Issue',
'contextPanel.linear.filter.teamAria': '依團隊篩選 Issue',
'contextPanel.linear.filter.priorityAria': '依優先級篩選 Issue',
'contextPanel.linear.filter.searchAria': '搜尋 Issue',
'contextPanel.linear.filter.clear': '清除',
'contextPanel.linear.filter.clearAria': '清除 Issue 篩選',
'contextPanel.linear.filter.status.all': '全部',
'contextPanel.linear.filter.status.backlog': '待辦池',
'contextPanel.linear.filter.status.todo': '待辦',
'contextPanel.linear.filter.status.started': '進行中',
'contextPanel.linear.filter.status.inReview': '審核中',
'contextPanel.linear.filter.status.completed': '已完成',
'contextPanel.linear.filter.status.canceled': '已取消',
'contextPanel.linear.filter.status.duplicate': '重複',
'contextPanel.linear.filter.assignee.any': '任何人',
'contextPanel.linear.filter.assignee.me': '指派給我',
'contextPanel.linear.filter.team.all': '所有團隊',
'contextPanel.linear.filter.priority.all': '所有優先級',
'contextPanel.linear.empty.noDescription': '沒有描述',
'contextPanel.linear.empty.noComments': '沒有留言',
'contextPanel.linear.empty.noMatchingIssues': '沒有符合這些篩選條件的 Issue',
'contextPanel.linear.loading.issue': '正在載入 Issue…',
'contextPanel.linear.toast.statusUpdated': '已更新 Issue 狀態',
'contextPanel.linear.toast.statusUpdateFailed': '無法更新 Issue 狀態',
'contextPanel.linear.toast.closeFailed': '無法關閉 Issue',
'contextPanel.linear.toast.workspaceSwitched': '已切換 Linear 工作區',
'contextPanel.linear.toast.workspaceSwitchFailed': '無法切換 Linear 工作區',
'contextPanel.linear.error.noCompletedState': '此團隊沒有已完成狀態',
},
tr: {
'contextPanel.mode.linear': 'Linear',
'contextRail.surface.linear.description': "Linear issue'larını incele, durumu değiştir ve session başlat",
'contextPanel.linear.actions.backToList': 'Issue listesine dön',
'contextPanel.linear.actions.startSession': 'Session başlat',
'contextPanel.linear.actions.closeIssue': "Issue'u kapat",
'contextPanel.linear.actions.closeSearch': 'Aramayı kapat',
'contextPanel.linear.label.status': 'Durum',
'contextPanel.linear.label.team': 'Ekip',
'contextPanel.linear.label.assignee': 'Atanan',
'contextPanel.linear.label.unassigned': 'Atanmamış',
'contextPanel.linear.label.priority': 'Öncelik',
'contextPanel.linear.label.labels': 'Etiketler',
'contextPanel.linear.priority.none': 'Öncelik yok',
'contextPanel.linear.priority.urgent': 'Acil',
'contextPanel.linear.priority.high': 'Yüksek',
'contextPanel.linear.priority.medium': 'Orta',
'contextPanel.linear.priority.low': 'Düşük',
'contextPanel.linear.label.comments': 'Yorumlar',
'contextPanel.linear.label.statusAria': 'Linear issue durumu',
'contextPanel.linear.label.workspace': 'Çalışma alanı',
'contextPanel.linear.label.workspaceAria': 'Linear çalışma alanı',
'contextPanel.linear.filter.statusAria': "Issue'ları duruma göre süz",
'contextPanel.linear.filter.assigneeAria': "Issue'ları atanan kişiye göre süz",
'contextPanel.linear.filter.teamAria': "Issue'ları ekibe göre süz",
'contextPanel.linear.filter.priorityAria': "Issue'ları önceliğe göre süz",
'contextPanel.linear.filter.searchAria': "Issue'larda ara",
'contextPanel.linear.filter.clear': 'Temizle',
'contextPanel.linear.filter.clearAria': "Issue filtrelerini temizle",
'contextPanel.linear.filter.status.all': 'Tümü',
'contextPanel.linear.filter.status.backlog': 'Bekleme listesi',
'contextPanel.linear.filter.status.todo': 'Yapılacak',
'contextPanel.linear.filter.status.started': 'Devam ediyor',
'contextPanel.linear.filter.status.inReview': 'İncelemede',
'contextPanel.linear.filter.status.completed': 'Bitti',
'contextPanel.linear.filter.status.canceled': 'İptal',
'contextPanel.linear.filter.status.duplicate': 'Yinelenen',
'contextPanel.linear.filter.assignee.any': 'Herkes',
'contextPanel.linear.filter.assignee.me': 'Bana atananlar',
'contextPanel.linear.filter.team.all': 'Tüm ekipler',
'contextPanel.linear.filter.priority.all': 'Tüm öncelikler',
'contextPanel.linear.empty.noDescription': 'Açıklama yok',
'contextPanel.linear.empty.noComments': 'Yorum yok',
'contextPanel.linear.empty.noMatchingIssues': 'Bu süzgeçlere uyan issue yok',
'contextPanel.linear.loading.issue': 'Issue yükleniyor…',
'contextPanel.linear.toast.statusUpdated': 'Issue durumu güncellendi',
'contextPanel.linear.toast.statusUpdateFailed': 'Issue durumu güncellenemedi',
'contextPanel.linear.toast.closeFailed': 'Issue kapatılamadı',
'contextPanel.linear.toast.workspaceSwitched': 'Linear çalışma alanı değiştirildi',
'contextPanel.linear.toast.workspaceSwitchFailed': 'Linear çalışma alanı değiştirilemedi',
'contextPanel.linear.error.noCompletedState': 'Bu ekibin tamamlandı durumu yok',
},
} as const;
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Śledzenie użycia OpenCode Go',
@@ -2220,5 +2221,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...linearIntegrationI18n.pl,
...thirdPartyIntegrationI18n.pl,
};
+4
View File
@@ -1,8 +1,12 @@
import type { I18nKey } from './en';
import { settingsDict } from './pl.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict: Record<I18nKey, string> = {
...settingsDict,
...linearIssuePickerI18n.pl,
...linearPanelI18n.pl,
'terminalView.actions.attachSelection': 'Dołącz zaznaczone dane wyjściowe',
'terminalView.actions.restart': 'Uruchom terminal ponownie',
'chat.message.terminalContext': '{terminal}, wiersze {start}-{end}',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Monitoramento de uso do OpenCode Go',
@@ -2227,5 +2228,6 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
...linearIntegrationI18n['pt-BR'],
...thirdPartyIntegrationI18n['pt-BR'],
} as const;
@@ -1,8 +1,12 @@
import type { I18nKey } from './en';
import { settingsDict } from './pt-BR.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict: Record<I18nKey, string> = {
...settingsDict,
...linearIssuePickerI18n['pt-BR'],
...linearPanelI18n['pt-BR'],
'terminalView.actions.attachSelection': 'Anexar saída selecionada',
'terminalView.actions.restart': 'Reiniciar terminal',
'chat.message.terminalContext': '{terminal}, linhas {start}-{end}',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go kullanım takibi',
@@ -2218,4 +2219,5 @@ export const settingsDict = {
'settings.openchamber.visual.field.sessionTabsAria': 'Başlıktaki session sekmelerini aç/kapat',
'settings.openchamber.visual.field.sessionTabsInfo': 'Açtığınız session\'lar başlıkta sekmeler olarak dizilir. Kapatırsanız düz session başlığına döner.',
...thirdPartyIntegrationI18n.tr,
...linearIntegrationI18n.tr,
};
+4
View File
@@ -1,7 +1,11 @@
import { settingsDict } from './tr.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict = {
...settingsDict,
...linearIssuePickerI18n.tr,
...linearPanelI18n.tr,
'terminalView.actions.attachSelection': 'Seçili çıktıyı ekle',
'terminalView.actions.restart': 'Terminali yeniden başlat',
'chat.message.terminalContext': '{terminal}, {start}-{end}. satırlar',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'Відстеження використання OpenCode Go',
@@ -2227,5 +2228,6 @@ export const settingsDict = {
"settings.openchamber.visual.field.followUpBehaviorDescription": "Choose what happens when you press Enter on a follow-up message while the agent is still responding.",
"settings.openchamber.visual.option.followUpBehavior.steer.label": "Steer",
"settings.openchamber.visual.option.followUpBehavior.queue.label": "Queue",
...linearIntegrationI18n.uk,
...thirdPartyIntegrationI18n.uk,
} as const;
+4
View File
@@ -1,8 +1,12 @@
import type { I18nKey } from './en';
import { settingsDict } from './uk.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict: Record<I18nKey, string> = {
...settingsDict,
...linearIssuePickerI18n.uk,
...linearPanelI18n.uk,
'terminalView.actions.attachSelection': 'Прикріпити вибраний вивід',
'terminalView.actions.restart': 'Перезапустити термінал',
'chat.message.terminalContext': '{terminal}, рядки {start}-{end}',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go 用量跟踪',
@@ -2227,5 +2228,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...linearIntegrationI18n['zh-CN'],
...thirdPartyIntegrationI18n['zh-CN'],
} as const;
@@ -1,8 +1,12 @@
import type { I18nKey } from './en';
import { settingsDict } from './zh-CN.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict: Record<I18nKey, string> = {
...settingsDict,
...linearIssuePickerI18n['zh-CN'],
...linearPanelI18n['zh-CN'],
'terminalView.actions.attachSelection': '附加所选输出',
'terminalView.actions.restart': '重启终端',
'chat.message.terminalContext': '{terminal},第 {start}-{end} 行',
@@ -1,3 +1,4 @@
import { linearIntegrationI18n } from './linear-integration.i18n';
import { thirdPartyIntegrationI18n } from './third-party-integrations.i18n';
export const settingsDict = {
'settings.providers.page.openCodeGo.title': 'OpenCode Go 用量追蹤',
@@ -2227,5 +2228,6 @@ export const settingsDict = {
'settings.openchamber.visual.field.followUpBehaviorDescription': 'Choose what happens when you press Enter on a follow-up message while the agent is still responding.',
'settings.openchamber.visual.option.followUpBehavior.steer.label': 'Steer',
'settings.openchamber.visual.option.followUpBehavior.queue.label': 'Queue',
...linearIntegrationI18n['zh-TW'],
...thirdPartyIntegrationI18n['zh-TW'],
} as const;
@@ -1,8 +1,12 @@
import type { I18nKey } from './en';
import { settingsDict } from './zh-TW.settings';
import { linearIssuePickerI18n } from './linear-issue-picker.i18n';
import { linearPanelI18n } from './linear-panel.i18n';
export const dict: Record<I18nKey, string> = {
...settingsDict,
...linearIssuePickerI18n['zh-TW'],
...linearPanelI18n['zh-TW'],
'terminalView.actions.attachSelection': '附加所選輸出',
'terminalView.actions.restart': '重新啟動終端',
'chat.message.terminalContext': '{terminal},第 {start}-{end} 行',
@@ -0,0 +1,39 @@
import { describe, expect, test } from 'bun:test';
import { resolveLinearMappedProjectPath } from './linearProjectMapping';
import type { LinearMappingResult } from './api/types';
const mapping = (): LinearMappingResult => ({
connected: true,
defaultProjectPath: '/default',
teams: [
{ id: 'team-eng', key: 'ENG', name: 'Engineering', projectPath: '/eng' },
{ id: 'team-des', key: 'DES', name: 'Design', projectPath: null },
],
});
describe('resolveLinearMappedProjectPath', () => {
test('prefers the team path over the default', () => {
expect(resolveLinearMappedProjectPath(mapping(), { id: 'team-eng', key: 'ENG', name: 'Engineering' }))
.toBe('/eng');
});
test('falls back to the default when the team has no path', () => {
expect(resolveLinearMappedProjectPath(mapping(), { id: 'team-des', key: 'DES', name: 'Design' }))
.toBe('/default');
});
test('matches a team by key when the id is missing', () => {
expect(resolveLinearMappedProjectPath(mapping(), { id: '', key: 'ENG', name: 'Engineering' }))
.toBe('/eng');
});
test('returns null when Linear is disconnected or unmapped', () => {
expect(resolveLinearMappedProjectPath({ connected: false }, { id: 'team-eng', key: 'ENG', name: 'Engineering' }))
.toBeNull();
expect(resolveLinearMappedProjectPath({
connected: true,
defaultProjectPath: null,
teams: [{ id: 'team-des', key: 'DES', name: 'Design', projectPath: null }],
}, { id: 'team-des', key: 'DES', name: 'Design' })).toBeNull();
});
});
@@ -0,0 +1,24 @@
import type { LinearIssueTeam, LinearMappingResult } from '@/lib/api/types';
export function resolveLinearMappedProjectPath(
mapping: LinearMappingResult | null | undefined,
team: LinearIssueTeam | null | undefined,
): string | null {
if (!mapping || mapping.connected === false) {
return null;
}
const teams = mapping.teams ?? [];
if (team?.id) {
const byId = teams.find((entry) => entry.id === team.id);
if (byId?.projectPath) {
return byId.projectPath;
}
}
if (team?.key) {
const byKey = teams.find((entry) => entry.key === team.key);
if (byKey?.projectPath) {
return byKey.projectPath;
}
}
return mapping.defaultProjectPath?.trim() || null;
}
@@ -0,0 +1,50 @@
import { afterEach, describe, expect, test } from 'bun:test';
import { resolveLinearSessionOrigin } from './linearSessionStatus';
describe('resolveLinearSessionOrigin', () => {
const originalWindow = globalThis.window;
afterEach(() => {
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: originalWindow,
});
});
test('uses the page origin on web', () => {
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
location: { origin: 'https://app.example.com' },
},
});
expect(resolveLinearSessionOrigin()).toBe('https://app.example.com');
});
test('uses the desktop loopback origin instead of the packaged UI scheme', () => {
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
location: { origin: 'openchamber-ui://app' },
__OPENCHAMBER_ELECTRON__: { runtime: 'electron' },
__OPENCHAMBER_LOCAL_ORIGIN__: 'http://127.0.0.1:3001',
},
});
expect(resolveLinearSessionOrigin()).toBe('http://127.0.0.1:3001');
});
test('reports no origin when the desktop shell has no http loopback', () => {
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: {
location: { origin: 'openchamber-ui://app' },
__OPENCHAMBER_ELECTRON__: { runtime: 'electron' },
__OPENCHAMBER_LOCAL_ORIGIN__: 'openchamber-ui://app',
},
});
// A deep link is unopenable for everyone but this machine, so the server
// gets no origin and posts no comment.
expect(resolveLinearSessionOrigin()).toBe(undefined);
});
});
@@ -0,0 +1,45 @@
import type { LinearAPI } from '@/lib/api/types';
import { isElectronShell } from '@/lib/desktop';
import { getLocalDesktopOrigin } from '@/lib/desktopCurrentHost';
function isHttpOrigin(value: string): boolean {
try {
const url = new URL(value);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return false;
}
}
/**
* Origin Linear comments should open. Packaged desktop UI lives on
* `openchamber-ui://`, which is not a URL a browser can load from Linear, so
* report the http origin the local server actually listens on instead. The
* server decides whether that origin is reachable by anyone else; a comment is
* only posted when it is.
*/
export function resolveLinearSessionOrigin(): string | undefined {
if (typeof window === 'undefined') return undefined;
if (isElectronShell()) {
const localOrigin = getLocalDesktopOrigin().trim();
if (localOrigin && isHttpOrigin(localOrigin)) {
return new URL(localOrigin).origin;
}
return undefined;
}
const origin = window.location.origin.trim();
return origin || undefined;
}
export function postLinearSessionStarted(
linear: LinearAPI | undefined,
args: { sessionId: string; issueIdentifier: string },
): void {
if (!linear?.sessionStatusPost) return;
void linear.sessionStatusPost({
kind: 'started',
sessionId: args.sessionId,
issueIdentifier: args.issueIdentifier,
sessionOrigin: resolveLinearSessionOrigin(),
}).catch(() => undefined);
}
@@ -0,0 +1,29 @@
import { describe, expect, test } from 'bun:test';
import { buildIssueContextText } from './linearStartSession';
import type { LinearIssue } from '@/lib/api/types';
const issue: LinearIssue = {
id: 'issue-1',
identifier: 'ENG-12',
title: 'Broken login',
url: 'https://linear.app/openchamber/issue/ENG-12',
description: 'Users cannot sign in.',
comments: [],
};
describe('buildIssueContextText', () => {
test('serializes the issue and comments as JSON context', () => {
const text = buildIssueContextText({
issue,
comments: [{
id: 'comment-1',
body: 'Still broken',
createdAt: '2026-08-24T10:00:00.000Z',
user: { name: 'Ada', displayName: 'Ada Lovelace' },
}],
});
expect(text.startsWith('Linear issue context (JSON)\n')).toBe(true);
expect(text).toContain('"identifier": "ENG-12"');
expect(text).toContain('Still broken');
});
});
+235
View File
@@ -0,0 +1,235 @@
import { toast } from '@/components/ui';
import type { LinearAPI, LinearIssue, LinearIssueComment, LinearMappingResult } from '@/lib/api/types';
import type { I18nKey, I18nParams } from '@/lib/i18n';
import { parseModelIdentifier } from '@/lib/modelIdentifier';
import { modelVariantNames } from '@/lib/modelVariants';
import { renderMagicPrompt } from '@/lib/magicPrompts';
import { createWorktreeSessionForNewBranch } from '@/lib/worktreeSessionCreator';
import { generateBranchSlug } from '@/lib/git/branchNameGenerator';
import { buildLinkedLinearIssue } from '@/lib/linkedIssues';
import { resolveLinearMappedProjectPath } from '@/lib/linearProjectMapping';
import { postLinearSessionStarted } from '@/lib/linearSessionStatus';
import { useConfigStore } from '@/stores/useConfigStore';
import { useUIStore } from '@/stores/useUIStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSelectionStore } from '@/sync/selection-store';
import * as sessionActions from '@/sync/session-actions';
type TranslateFn = (key: I18nKey, params?: I18nParams) => string;
export function buildIssueContextText(args: {
issue: LinearIssue;
comments: LinearIssueComment[];
}): string {
const payload = {
issue: args.issue,
comments: args.comments,
};
return `Linear issue context (JSON)\n${JSON.stringify(payload, null, 2)}`;
}
function resolveDefaultAgentName(): string | undefined {
const configState = useConfigStore.getState();
const settingsDefaultAgent = configState.settingsDefaultAgent;
if (settingsDefaultAgent) {
return settingsDefaultAgent;
}
const visibleAgents = configState.agents.filter((agent) => !agent.hidden);
return (
configState.currentAgentName
|| visibleAgents.find((agent) => agent.mode === 'primary' || !agent.mode)?.name
|| visibleAgents[0]?.name
);
}
function resolveDefaultModelSelection(): { providerID: string; modelID: string } | null {
const configState = useConfigStore.getState();
const settingsDefaultModel = configState.settingsDefaultModel;
if (!settingsDefaultModel) {
return null;
}
const parsed = parseModelIdentifier(settingsDefaultModel);
if (!parsed) {
return null;
}
const { providerId: providerID, modelId: modelID } = parsed;
const modelMetadata = configState.getModelMetadata(providerID, modelID);
if (!modelMetadata) {
return null;
}
return { providerID, modelID };
}
function resolveDefaultVariant(providerID: string, modelID: string): string | undefined {
const configState = useConfigStore.getState();
const settingsDefaultVariant = configState.settingsDefaultVariant;
const currentVariant = configState.currentProviderId === providerID && configState.currentModelId === modelID
? configState.currentVariant
: undefined;
const provider = configState.providers.find((entry) => entry.id === providerID);
const model = provider?.models.find((entry) => entry.id === modelID);
const variantNames = modelVariantNames(model);
if (variantNames.length === 0) {
return settingsDefaultVariant || currentVariant || undefined;
}
if (settingsDefaultVariant && variantNames.includes(settingsDefaultVariant)) {
return settingsDefaultVariant;
}
if (currentVariant && variantNames.includes(currentVariant)) {
return currentVariant;
}
return undefined;
}
export async function startLinearIssueSession(args: {
linear: LinearAPI | undefined;
issueKey: string;
createInWorktree: boolean;
mapping?: LinearMappingResult | null;
onMappingLoaded?: (mapping: LinearMappingResult) => void;
onSessionCreated?: () => void;
t: TranslateFn;
}): Promise<boolean> {
const { linear, issueKey, createInWorktree, t } = args;
if (!linear?.issueGet || !linear.mappingGet) {
toast.error(t('session.linearIssuePicker.error.runtimeUnavailable'));
return false;
}
try {
let mappingView = args.mapping;
if (!mappingView) {
mappingView = await linear.mappingGet();
args.onMappingLoaded?.(mappingView);
}
if (mappingView.connected === false) {
toast.error(t('session.linearIssuePicker.error.notConnected'));
return false;
}
const issueRes = await linear.issueGet(issueKey);
if (issueRes.connected === false) {
toast.error(t('session.linearIssuePicker.error.notConnected'));
return false;
}
const issue = issueRes.issue;
if (!issue) {
toast.error(t('session.linearIssuePicker.error.issueNotFound'));
return false;
}
const projectDirectory = resolveLinearMappedProjectPath(mappingView, issue.team);
if (!projectDirectory) {
toast.error(t('session.linearIssuePicker.error.noMappedProject'));
return false;
}
const comments = issue.comments ?? [];
const sessionTitle = `${issue.identifier} ${issue.title}`.trim();
const login = issue.assignee?.displayName || issue.assignee?.name;
const { sessionId, sessionDirectory } = await (async () => {
if (createInWorktree) {
const preferred = `issue-${issue.identifier}-${generateBranchSlug()}`;
const created = await createWorktreeSessionForNewBranch(
projectDirectory,
preferred,
undefined,
{ returnAfterDirectoryCreated: true },
);
if (!created?.id) {
throw new Error('Failed to create worktree session');
}
return { sessionId: created.id, sessionDirectory: created.path };
}
const session = await sessionActions.createSession(sessionTitle, projectDirectory, null);
if (!session?.id) {
throw new Error('Failed to create session');
}
return { sessionId: session.id, sessionDirectory: session.directory ?? projectDirectory };
})();
void sessionActions.updateSessionTitle(sessionId, sessionTitle).catch(() => undefined);
try {
useSessionUIStore.getState().initializeNewOpenChamberSession(sessionId, useConfigStore.getState().agents);
} catch {
// ignore
}
args.onSessionCreated?.();
useUIStore.getState().closeMainSurfaces();
useUIStore.getState().setSessionSwitcherOpen(false);
postLinearSessionStarted(linear, {
sessionId,
issueIdentifier: issue.identifier,
});
const configState = useConfigStore.getState();
const lastUsedProvider = useSelectionStore.getState().lastUsedProvider;
const defaultModel = resolveDefaultModelSelection();
const providerID = defaultModel?.providerID || configState.currentProviderId || lastUsedProvider?.providerID;
const modelID = defaultModel?.modelID || configState.currentModelId || lastUsedProvider?.modelID;
const agentName = resolveDefaultAgentName() || configState.currentAgentName || undefined;
if (!providerID || !modelID) {
toast.error(t('session.linearIssuePicker.error.noModelSelected'));
return true;
}
const variant = resolveDefaultVariant(providerID, modelID);
const visiblePromptText = await renderMagicPrompt('linear.issue.review.visible', {
identifier: issue.identifier,
});
const instructionsText = await renderMagicPrompt('linear.issue.review.instructions');
const contextText = buildIssueContextText({ issue, comments });
void sessionActions.setLinkedIssue(
sessionId,
sessionDirectory,
buildLinkedLinearIssue({
identifier: issue.identifier,
title: issue.title,
url: issue.url,
author: login
? { login, avatarUrl: issue.assignee?.avatarUrl || undefined }
: undefined,
linkedAt: Date.now(),
}),
true,
).catch(() => undefined);
void useSessionUIStore.getState().sendMessage(
visiblePromptText,
providerID,
modelID,
agentName,
undefined,
undefined,
[
{ text: instructionsText, synthetic: true },
{ text: contextText, synthetic: true },
],
variant,
undefined,
{ sessionId, directory: sessionDirectory },
).catch((error) => {
const message = error instanceof Error ? error.message : String(error);
toast.error(t('session.linearIssuePicker.toast.sendContextFailed'), {
description: message,
});
});
toast.success(t('session.linearIssuePicker.toast.sessionCreated'));
return true;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
toast.error(t('session.linearIssuePicker.toast.startSessionFailed'), { description: message });
return false;
}
}
+75 -2
View File
@@ -1,8 +1,10 @@
import { describe, expect, test } from 'bun:test';
import type { Session } from '@opencode-ai/sdk/v2';
import { buildLinkedIssue, buildLinkedIssueId, getLinkedIssues, withLinkedIssue, type LinkedIssue } from './linkedIssues';
import { buildLinkedIssue, buildLinkedIssueId, buildLinkedLinearIssue, canOpenLinearIssueInContextPanel, getLinkedIssues, withLinkedIssue, type LinkedIssue } from './linkedIssues';
const issue = (overrides: Partial<LinkedIssue> = {}): LinkedIssue => ({
type LinkedGitHubIssue = Exclude<LinkedIssue, { kind: 'linear' }>;
const issue = (overrides: Partial<LinkedGitHubIssue> = {}): LinkedGitHubIssue => ({
id: 'owner/repo#12',
number: 12,
title: 'Rail badge count',
@@ -76,6 +78,28 @@ describe('buildLinkedIssue', () => {
});
});
describe('buildLinkedLinearIssue', () => {
test('stores the Linear identifier without inventing a GitHub number', () => {
const built = buildLinkedLinearIssue({
identifier: 'ENG-12',
title: 'Broken login',
url: 'https://linear.app/openchamber/issue/ENG-12',
author: { login: 'Ada', avatarUrl: 'https://avatars/1' },
linkedAt: 5,
});
expect(built).toEqual({
id: 'linear:ENG-12',
identifier: 'ENG-12',
title: 'Broken login',
url: 'https://linear.app/openchamber/issue/ENG-12',
kind: 'linear',
author: 'Ada',
authorAvatarUrl: 'https://avatars/1',
linkedAt: 5,
});
});
});
describe('getLinkedIssues', () => {
test('returns an empty list for a session with no metadata', () => {
expect(getLinkedIssues(undefined)).toEqual([]);
@@ -95,6 +119,17 @@ describe('getLinkedIssues', () => {
expect(getLinkedIssues(session)).toEqual([good]);
});
test('keeps Linear entries next to GitHub ones', () => {
const github = issue();
const linear = buildLinkedLinearIssue({
identifier: 'ENG-12',
title: 'Broken login',
url: 'https://linear.app/openchamber/issue/ENG-12',
linkedAt: 2,
});
expect(getLinkedIssues(sessionWith([github, linear]))).toEqual([github, linear]);
});
test('survives a non-array payload', () => {
expect(getLinkedIssues(sessionWith({ nope: true }))).toEqual([]);
});
@@ -143,3 +178,41 @@ describe('withLinkedIssue', () => {
expect((next.openchamber as { linked_issues: LinkedIssue[] }).linked_issues).toEqual([issue()]);
});
});
describe('canOpenLinearIssueInContextPanel', () => {
test('opens the rail when Linear is connected, the shell has a context panel, and a directory is known', () => {
expect(canOpenLinearIssueInContextPanel({
linearAvailable: true,
linearConnected: true,
inDedicatedMobileShell: false,
directory: '/repo',
})).toBe(true);
});
test('falls back when Linear is missing, disconnected, the mobile shell is open, or the directory is blank', () => {
expect(canOpenLinearIssueInContextPanel({
linearAvailable: false,
linearConnected: true,
inDedicatedMobileShell: false,
directory: '/repo',
})).toBe(false);
expect(canOpenLinearIssueInContextPanel({
linearAvailable: true,
linearConnected: false,
inDedicatedMobileShell: false,
directory: '/repo',
})).toBe(false);
expect(canOpenLinearIssueInContextPanel({
linearAvailable: true,
linearConnected: true,
inDedicatedMobileShell: true,
directory: '/repo',
})).toBe(false);
expect(canOpenLinearIssueInContextPanel({
linearAvailable: true,
linearConnected: true,
inDedicatedMobileShell: false,
directory: ' ',
})).toBe(false);
});
});
+69 -9
View File
@@ -2,20 +2,17 @@ import type { Session } from '@opencode-ai/sdk/v2';
import { getSessionMetadata, type SessionMetadataRecord } from './sessionReviewMetadata';
/**
* GitHub issues and pull requests a user has linked to a session.
* Issues and pull requests a user has linked to a session.
*
* Stored as a **snapshot**, not a reference: number, title, author and avatar
* only. Enough to render a row and open the thing, and nothing more the body,
* comments and state of an issue belong to GitHub, and mirroring them here
* would mean owning their staleness. The stored title can drift from the real
* one; that is the accepted cost of a storage that never needs refreshing.
* Stored as a **snapshot**, not a reference: identifier or number, title, author
* and avatar only. Enough to render a row and open the thing, and nothing more.
*
* Rides the same session-metadata channel as pinned messages
* (`contextObligatoryMessages`), so it inherits their persistence and sync for
* free.
*/
export type LinkedIssue = {
export type LinkedGitHubIssue = {
/** `owner/repo#number`, unique per session and stable across renames. */
id: string;
number: number;
@@ -27,10 +24,24 @@ export type LinkedIssue = {
linkedAt: number;
};
export type LinkedLinearIssue = {
/** `linear:{identifier}`, unique per session. */
id: string;
identifier: string;
title: string;
url: string;
kind: 'linear';
author?: string;
authorAvatarUrl?: string;
linkedAt: number;
};
export type LinkedIssue = LinkedGitHubIssue | LinkedLinearIssue;
const isRecord = (value: unknown): value is Record<string, unknown> =>
Boolean(value && typeof value === 'object' && !Array.isArray(value));
const isLinkedIssue = (value: unknown): value is LinkedIssue => (
const isLinkedGitHubIssue = (value: unknown): value is LinkedGitHubIssue => (
isRecord(value)
&& typeof value.id === 'string'
&& value.id.length > 0
@@ -43,9 +54,29 @@ const isLinkedIssue = (value: unknown): value is LinkedIssue => (
&& Number.isFinite(value.linkedAt)
);
const isLinkedLinearIssue = (value: unknown): value is LinkedLinearIssue => (
isRecord(value)
&& typeof value.id === 'string'
&& value.id.length > 0
&& typeof value.identifier === 'string'
&& value.identifier.length > 0
&& typeof value.title === 'string'
&& typeof value.url === 'string'
&& value.kind === 'linear'
&& typeof value.linkedAt === 'number'
&& Number.isFinite(value.linkedAt)
);
const isLinkedIssue = (value: unknown): value is LinkedIssue => (
isLinkedGitHubIssue(value) || isLinkedLinearIssue(value)
);
export const buildLinkedIssueId = (owner: string, repo: string, number: number): string =>
`${owner}/${repo}#${number}`;
const buildLinkedLinearIssueId = (identifier: string): string =>
`linear:${identifier}`;
/**
* Builds the stored snapshot from what an attach flow already has.
*
@@ -61,7 +92,7 @@ export const buildLinkedIssue = (input: {
kind: 'issue' | 'pull';
author?: { login?: string; avatarUrl?: string } | null;
linkedAt: number;
}): LinkedIssue => {
}): LinkedGitHubIssue => {
const match = /github\.com\/([^/]+)\/([^/]+)\//.exec(input.url);
const id = match
? buildLinkedIssueId(match[1], match[2], input.number)
@@ -79,6 +110,35 @@ export const buildLinkedIssue = (input: {
};
};
export const buildLinkedLinearIssue = (input: {
identifier: string;
title: string;
url: string;
author?: { login?: string; avatarUrl?: string } | null;
linkedAt: number;
}): LinkedLinearIssue => ({
id: buildLinkedLinearIssueId(input.identifier),
identifier: input.identifier,
title: input.title,
url: input.url,
kind: 'linear',
author: input.author?.login ?? undefined,
authorAvatarUrl: input.author?.avatarUrl ?? undefined,
linkedAt: input.linkedAt,
});
export const canOpenLinearIssueInContextPanel = (options: {
linearAvailable: boolean;
linearConnected: boolean;
inDedicatedMobileShell: boolean;
directory: string | null | undefined;
}): boolean => (
options.linearAvailable
&& options.linearConnected
&& !options.inDedicatedMobileShell
&& Boolean(options.directory?.trim())
);
export const getLinkedIssues = (session: Session | null | undefined): LinkedIssue[] => {
const openchamber = getSessionMetadata(session).openchamber;
if (!isRecord(openchamber) || !Array.isArray(openchamber.linked_issues)) return [];
+58 -1
View File
@@ -13,6 +13,8 @@ export type MagicPromptId =
| 'github.pr.review.instructions'
| 'github.issue.review.visible'
| 'github.issue.review.instructions'
| 'linear.issue.review.visible'
| 'linear.issue.review.instructions'
| 'github.pr.checks.review.visible'
| 'github.pr.checks.review.instructions'
| 'github.pr.comments.review.visible'
@@ -56,7 +58,7 @@ export interface MagicPromptDefinition {
id: MagicPromptId;
title: string;
description: string;
group: 'Git' | 'GitHub' | 'Planning' | 'Session';
group: 'Git' | 'GitHub' | 'Linear' | 'Planning' | 'Session';
template: string;
placeholders?: Array<{ key: string; description: string }>;
}
@@ -261,6 +263,61 @@ Question/Support:
- Answer/guidance (max 6 lines)
- Missing info (max 4)
Do not implement changes until I confirm; end with: "Next actions: <1 sentence>".`,
},
{
id: 'linear.issue.review.visible',
title: 'Linear Issue Review Visible Prompt',
group: 'Linear',
description: 'Visible user message when creating a session from a Linear issue.',
placeholders: [
{ key: 'identifier', description: 'Linear issue identifier, such as ENG-12.' },
],
template: 'Review this Linear issue {{identifier}} using the provided issue context',
},
{
id: 'linear.issue.review.instructions',
title: 'Linear Issue Review Instructions',
group: 'Linear',
description: 'Hidden instructions attached when generating a Linear issue review response.',
template: `Review this Linear issue using the provided issue context.
Process:
- First classify the issue type (bug / feature request / question/support / refactor / ops) and state it as: Type: <one label>.
- Gather any needed repository context (code, config, docs) to validate assumptions.
- After gathering, if anything is still unclear or cannot be verified, do not speculate state what's missing and ask targeted questions.
Mode selection by type:
- Bug / Question/Support / Ops: deliver the response directly using the matching template below. Do not bombard me with questions for straightforward diagnosis; use "Missing info" / "Repro/diagnostics needed" fields instead.
- Feature request / Refactor with substantive unknowns: this is effectively a planning session. Do not emit the Feature template on the first turn. Instead, ask me focused clarifying questions in batches of at most 3, one topic at a time (scope, constraints, tradeoffs, UX, etc.), wait for answers, drop questions that became irrelevant, and repeat until you have no more substantive questions. Only then emit the Feature template.
Output rules:
- Compact output; pick ONE template below and omit the others.
- No emojis. No code snippets. No fenced blocks.
- Short inline code identifiers allowed.
- Reference evidence with file paths and line ranges when applicable; if exact lines are not available, cite the file and say "approx" + why.
- Keep the entire response under ~300 words (applies to the final template output, not to clarifying-question turns).
Templates (choose one):
Bug:
- Summary (1-2 sentences)
- Likely cause (max 2)
- Repro/diagnostics needed (max 3)
- Fix approach (max 4 steps)
- Verification (max 3)
Feature:
- Summary (1-2 sentences)
- Requirements (max 4)
- Unknowns/questions (max 4)
- Proposed plan (max 5 steps)
- Verification (max 3)
Question/Support:
- Summary (1-2 sentences)
- Answer/guidance (max 6 lines)
- Missing info (max 4)
Do not implement changes until I confirm; end with: "Next actions: <1 sentence>".`,
},
{
@@ -114,6 +114,13 @@ describe('round-trip through part metadata', () => {
expect(readContextPart(part)).toEqual(payload);
});
test('linear references carry picker-built text and the identifier', () => {
const payload: ContextPartPayload = { kind: 'linear-issue', identifier: 'ENG-12', title: 'Login', url: 'https://linear.app/x/issue/ENG-12' };
const part = asPart(payload, 'Linear issue context (JSON)\n{}');
expect(part.text).toBe('Linear issue context (JSON)\n{}');
expect(readContextPart(part)).toEqual(payload);
});
test('non-text parts, missing metadata, and malformed payloads read as null', () => {
expect(readContextPart({ type: 'file', metadata: {} })).toBeNull();
expect(readContextPart({ type: 'text' })).toBeNull();
+19 -3
View File
@@ -96,6 +96,13 @@ type GitHubPrContext = {
url: string;
};
type LinearIssueContext = {
kind: 'linear-issue';
identifier: string;
title: string;
url: string;
};
export type ContextPartPayload =
| CodeCommentContext
| TerminalContextPayload
@@ -105,7 +112,8 @@ export type ContextPartPayload =
| FileQuoteContext
| ChatQuoteContext
| GitHubIssueContext
| GitHubPrContext;
| GitHubPrContext
| LinearIssueContext;
export type ContextPartMetadata = { [K in typeof CONTEXT_METADATA_KEY]: ContextPartPayload };
@@ -154,6 +162,7 @@ export function formatContextText(payload: ContextPartPayload): string {
return `Attached failed GitHub PR check (${payload.label}):\n\`\`\`\n${payload.output}\n\`\`\`${payload.text ? `\n\n${payload.text}` : ''}`;
case 'github-issue':
case 'github-pr':
case 'linear-issue':
// Linked issues/PRs carry server-fetched context text built by
// their pickers; there is no default text to derive here.
return '';
@@ -162,8 +171,9 @@ export function formatContextText(payload: ContextPartPayload): string {
/**
* Build the synthetic part for one context payload. `text` overrides the
* derived text; github-issue/github-pr payloads require it because their
* model-facing context is fetched by the picker, not derived from metadata.
* derived text; github-issue/github-pr/linear-issue payloads require it
* because their model-facing context is fetched by the picker, not derived
* from metadata.
*/
export function createContextPart(payload: ContextPartPayload, text?: string): ContextPart {
const resolvedText = text ?? formatContextText(payload);
@@ -297,6 +307,12 @@ const contextPayloadSchema = z.discriminatedUnion('kind', [
title: z.string(),
url: z.string(),
}),
z.object({
kind: z.literal('linear-issue'),
identifier: z.string().min(1),
title: z.string(),
url: z.string(),
}),
]);
/** The subset of a message part that context read-back inspects. */
@@ -0,0 +1,64 @@
import { beforeEach, describe, expect, test } from 'bun:test';
import type { Session } from '@opencode-ai/sdk/v2';
import { useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { openSessionFromRoute } from './openSessionFromRoute';
const SESSION_ID = 'ses_linear_open';
const PROJECT_DIR = '/projects/linear-from-url';
const OTHER_DIR = '/projects/linear-from-url-other';
const buildSession = (id: string, directory: string): Session => ({
id,
title: id,
directory,
time: { created: 1, updated: 2 },
} as Session);
describe('openSessionFromRoute', () => {
beforeEach(() => {
useSessionUIStore.getState().setCurrentSession(null);
useGlobalSessionsStore.setState({
activeSessions: [],
archivedSessions: [],
sessionsByDirectory: new Map(),
hasLoaded: true,
status: 'ready',
});
});
test('selects the routed session once the global list knows its directory', async () => {
useGlobalSessionsStore.setState({
activeSessions: [buildSession(SESSION_ID, PROJECT_DIR)],
archivedSessions: [],
hasLoaded: true,
status: 'ready',
});
await openSessionFromRoute(SESSION_ID);
expect(useSessionUIStore.getState().currentSessionId).toBe(SESSION_ID);
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(PROJECT_DIR);
});
test('replaces a guessed directory once the global list knows the owner', async () => {
const id = 'ses_linear_guessed';
useSessionUIStore.getState().setCurrentSession(id);
const guessed = useSessionUIStore.getState().currentSessionDirectory;
useGlobalSessionsStore.setState({
activeSessions: [buildSession(id, OTHER_DIR)],
archivedSessions: [],
hasLoaded: true,
status: 'ready',
});
await openSessionFromRoute(id);
expect(useSessionUIStore.getState().currentSessionId).toBe(id);
expect(useSessionUIStore.getState().currentSessionDirectory).toBe(OTHER_DIR);
expect(guessed).not.toBe(OTHER_DIR);
});
});
@@ -0,0 +1,33 @@
import { ensureGlobalSessionsLoaded, resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore';
import { useSessionUIStore } from '@/sync/session-ui-store';
/**
* Select a session named by `/?session=`. Cold loads often do not know the
* owning directory yet, so a first selection may guess the active project.
* After the global session list is available, re-select with that directory
* unless the user already moved to a different session.
*/
export async function openSessionFromRoute(sessionId: string): Promise<void> {
const id = sessionId.trim();
if (!id) return;
const initial = useSessionUIStore.getState();
if (initial.currentSessionId !== id) {
initial.setCurrentSession(id, initial.getDirectoryForSession(id));
}
const snapshot = await ensureGlobalSessionsLoaded().catch(() => null);
if (!snapshot) return;
const latest = useSessionUIStore.getState();
if (latest.currentSessionId !== id) return;
const session = [...snapshot.activeSessions, ...snapshot.archivedSessions]
.find((entry) => entry.id === id);
if (!session) return;
const directory = resolveGlobalSessionDirectory(session);
if (!directory || directory === latest.currentSessionDirectory) return;
latest.setCurrentSession(id, directory);
}
@@ -0,0 +1,20 @@
import { describe, expect, test } from 'bun:test';
import { parseRoute } from './parseRoute';
describe('parseRoute session', () => {
test('reads a session id including OpenCode underscores', () => {
const route = parseRoute(new URLSearchParams('session=ses_abc123'));
expect(route.sessionId).toBe('ses_abc123');
});
test('decodes a percent-encoded session id', () => {
const route = parseRoute(new URLSearchParams('session=ses%5Fabc123'));
expect(route.sessionId).toBe('ses_abc123');
});
test('ignores a blank session param', () => {
const route = parseRoute(new URLSearchParams('session='));
expect(route.sessionId).toBeNull();
});
});
+1 -1
View File
@@ -202,7 +202,7 @@ export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
{ slug: 'voice', title: 'Voice', group: 'general', kind: 'single', keywords: ['tts', 'speech', 'voice'], isAvailable: (ctx) => !ctx.isVSCode },
{ slug: 'tunnel', title: 'External Tunnel', group: 'projects', kind: 'single', keywords: ['tunnel', 'external', 'cloudflare', 'qr', 'remote', 'mobile', 'share'], isAvailable: (ctx) => !ctx.isVSCode },
{ slug: 'about', title: 'About', group: 'general', kind: 'single', keywords: ['about', 'version', 'updates', 'release', 'changelog'], isAvailable: (ctx) => ctx.isMobile && !ctx.isVSCode },
{ slug: 'integrations', title: 'Integrations', group: 'general', kind: 'single', keywords: ['integration', 'plugin', 'provider', 'oauth', 'claude', 'cursor', 'command code', 'connect', 'discord', 'telegram', 'messenger'] },
{ slug: 'integrations', title: 'Integrations', group: 'general', kind: 'single', keywords: ['integration', 'plugin', 'provider', 'oauth', 'claude', 'cursor', 'command code', 'connect', 'discord', 'telegram', 'messenger', 'linear'] },
] as const;
const LEGACY_SIDEBAR_SECTION_TO_SETTINGS_SLUG: Record<SidebarSection, SettingsPageSlug> = {
@@ -38,4 +38,30 @@ describe('settings search', () => {
expect(results.some((result) => result.id === 'integrations.third-party.opencode-cursor-oauth')).toBe(true);
});
test('finds Linear connect on the integrations page', () => {
const results = buildSettingsSearchResults({
query: 'linear',
runtimeCtx,
t,
getPageTitle: (page) => page,
});
expect(results.some((result) => result.id === 'integrations.linear')).toBe(true);
expect(results.some((result) => result.id === 'integrations.linear.add-workspace')).toBe(true);
expect(results.some((result) => result.id === 'integrations.linear.mapping')).toBe(true);
});
test('hides Linear connect in VS Code', () => {
const results = buildSettingsSearchResults({
query: 'linear',
runtimeCtx: { ...runtimeCtx, isVSCode: true },
t,
getPageTitle: (page) => page,
});
expect(results.some((result) => result.id === 'integrations.linear')).toBe(false);
expect(results.some((result) => result.id === 'integrations.linear.add-workspace')).toBe(false);
expect(results.some((result) => result.id === 'integrations.linear.mapping')).toBe(false);
});
});
+32
View File
@@ -984,6 +984,38 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
isAvailable: (ctx) => ctx.isWeb && !ctx.isDesktop && !ctx.isVSCode,
},
{
id: 'integrations.first-party',
page: 'integrations',
titleKey: 'settings.integrations.firstParty.title',
descriptionKey: 'settings.integrations.firstParty.info',
keywords: ['built-in', 'first-party', 'native', 'linear'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
id: 'integrations.linear',
page: 'integrations',
titleKey: 'settings.integrations.linear.title',
descriptionKey: 'settings.integrations.linear.description',
keywords: ['linear', 'issues', 'oauth', 'connect', 'workspace'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
id: 'integrations.linear.add-workspace',
page: 'integrations',
titleKey: 'settings.integrations.linear.actions.addWorkspace',
descriptionKey: 'settings.integrations.linear.description',
keywords: ['linear', 'workspace', 'add', 'connect', 'oauth'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
id: 'integrations.linear.mapping',
page: 'integrations',
titleKey: 'settings.integrations.linear.mapping.defaultProject',
descriptionKey: 'settings.integrations.linear.mapping.defaultProject.info',
keywords: ['linear', 'project', 'team', 'map', 'workspace', 'directory'],
isAvailable: (ctx) => !ctx.isVSCode,
},
{
id: 'integrations.third-party',
page: 'integrations',
+20 -5
View File
@@ -26,10 +26,10 @@ edge (`components/layout/ContextPanelRail.tsx`) and rendered by
(`useUIStore.contextRailHiddenSurfaces`, edited from the rail's trailing
configure button — `ContextRailSurfacesDialog`), drops the plan surface
unless plan mode is enabled,
drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, and hides
`has-content` surfaces until a tab of their mode exists. Both consumers use
it so the digit shown on a rail badge always maps to the same surface the
shortcut opens.
drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, hides
Linear unless a workspace is connected, and hides `has-content` surfaces
until a tab of their mode exists. Both consumers use it so the digit shown
on a rail badge always maps to the same surface the shortcut opens.
## Adding a surface
@@ -53,7 +53,22 @@ the `openContext*` actions in `useUIStore`.
positions). Chat tab records stay open, but only the active chat iframe is
mounted while the panel is open. A selected chat restores its state from
the session stores. A closed panel mounts no chat iframe.
Singleton surfaces (git, pr, notes, plan, context) remount on switch. These
Singleton surfaces (git, pr, linear, notes, plan, context) remount on switch. These
surfaces must restore their state from stores or snapshots.
- Runtime scope: desktop/web `MainLayout` only. VS Code and the dedicated
mobile shell have their own layouts and do not consume this registry.
Linear is a desktop/web singleton on this rail. VS Code and mobile omit it
(no this registry, and VS Code has no `RuntimeAPIs.linear`). The Linear
rail icon is hidden until a Linear workspace is connected. A persisted Linear
tab stays open across reload until auth has resolved; only a confirmed
disconnect closes the panel. The surface lists
issues with status (All, Backlog, To Do, In Progress, In Review, Done, Canceled, Duplicate), assignee, team, and priority filters, can switch
the current workspace, and keeps Start session in a footer on the issue card.
Those filters restore from `useUIStore` when the surface remounts. Non-default
status, assignee, team, priority, and search tint the filter icon `text-primary`,
same as the context rail; one control clears them. Workspace switch is not a
filter. Work-status Context sources
can open a specific issue here through `linearIssueFocus`. Below 520px
search and the filters other than status drop to icons; status keeps its label. The card
shows priority and labels. Changing filters keeps the previous list
until the next page arrives.
@@ -12,6 +12,7 @@ const baseOptions = {
isVSCode: false,
screenWidth: 1200,
tabs: [],
linearConnected: true,
} as const;
describe('getVisibleContextRailSurfaces', () => {
@@ -63,4 +64,17 @@ describe('getVisibleContextRailSurfaces', () => {
const surfaces = getVisibleContextRailSurfaces({ ...baseOptions, railOrder: ['git', 'context'] });
expect(surfaces.slice(0, 2).map((surface) => surface.id)).toEqual(['git', 'context']);
});
test('places Linear after Pull Request in the default order', () => {
const ids = getVisibleContextRailSurfaces(baseOptions).map((surface) => surface.id);
const pr = ids.indexOf('pr');
const linear = ids.indexOf('linear');
expect(pr).toBeGreaterThanOrEqual(0);
expect(linear).toBe(pr + 1);
});
test('hides Linear until a workspace is connected', () => {
expect(getVisibleContextRailSurfaces({ ...baseOptions, linearConnected: false }).some((s) => s.id === 'linear')).toBe(false);
expect(getVisibleContextRailSurfaces({ ...baseOptions, linearConnected: true }).some((s) => s.id === 'linear')).toBe(true);
});
});
+15
View File
@@ -6,6 +6,7 @@ export type ContextSurfaceId =
| 'editor'
| 'git'
| 'pr'
| 'linear'
| 'diff'
| 'walkthrough'
| 'terminal'
@@ -65,6 +66,15 @@ export const CONTEXT_SURFACES: readonly ContextSurfaceDescriptor[] = [
labelKey: 'contextPanel.mode.pr',
availability: 'always',
},
{
id: 'linear',
descriptionKey: 'contextRail.surface.linear.description',
defaultWidthFraction: 0.45,
mode: 'linear',
icon: 'linear',
labelKey: 'contextPanel.mode.linear',
availability: 'always',
},
{
id: 'diff',
descriptionKey: 'contextRail.surface.diff.description',
@@ -194,6 +204,8 @@ type VisibleRailSurfacesOptions = {
isVSCode: boolean;
screenWidth: number;
tabs: readonly { mode: ContextPanelMode }[];
/** Linear's rail icon stays off until a workspace is connected. */
linearConnected: boolean;
};
/**
@@ -225,6 +237,9 @@ export const getVisibleContextRailSurfaces = (options: VisibleRailSurfacesOption
if (surface.id === 'browser' && options.isVSCode) {
return false;
}
if (surface.id === 'linear' && !options.linearConnected) {
return false;
}
if (surface.availability === 'has-content') {
return options.tabs.some((tab) => tab.mode === surface.mode);
}