mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 18:47:40 +00:00
fix(i18n): localize GitHub project view errors and states (#34621)
* fix(i18n): localize GitHub project view errors and states Main now tags Orca-authored project-view failures with an optional reason code plus host, rate-limit numbers and diagnostic detail; the English message is unchanged for mobile, older clients and logs. The renderer maps type/reason to translated copy in the error state, picker, item dialog and mutation toasts, falling back to the English message for older hosts and GitHub's own text. Also wraps the layout labels, unsupported-layout, not-found/too-large copy, picker partial-failure summaries and the gh sign-in help. Closes #34589 * test: expect input_too_large reason on oversized project refs * fix: make project view error copy switches exhaustive --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -238,7 +238,8 @@ describe('resolveProjectRef', () => {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR
|
||||
message: GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR,
|
||||
reason: 'input_too_large'
|
||||
}
|
||||
})
|
||||
await expect(resolveProjectRef({ input })).resolves.not.toMatchObject({
|
||||
|
||||
@@ -48,7 +48,8 @@ export async function projectHostAuthenticationError(
|
||||
// Verify pasted hosts before sending ambient Enterprise credentials.
|
||||
return {
|
||||
type: 'auth_required',
|
||||
message: `GitHub CLI is not authenticated to ${selectedHost}. Run gh auth login --hostname ${selectedHost}.`
|
||||
message: `GitHub CLI is not authenticated to ${selectedHost}. Run gh auth login --hostname ${selectedHost}.`,
|
||||
host: selectedHost
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +80,10 @@ export function assertSlug(
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: `Invalid ${field}: "${String(value)}" is not a valid GitHub slug.`
|
||||
message: `Invalid ${field}: "${String(value)}" is not a valid GitHub slug.`,
|
||||
reason: 'invalid_slug',
|
||||
detail: field,
|
||||
value: String(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +99,9 @@ export function assertPositiveInt(
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: `Invalid ${field}: must be a positive integer.`
|
||||
message: `Invalid ${field}: must be a positive integer.`,
|
||||
reason: 'invalid_positive_integer',
|
||||
detail: field
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,7 +230,12 @@ export async function runRest<T>(
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'unknown', message: `Unexpected REST response: ${stderr.trim()}` }
|
||||
error: {
|
||||
type: 'unknown',
|
||||
message: `Unexpected REST response: ${stderr.trim()}`,
|
||||
reason: 'unexpected_rest_response',
|
||||
detail: stderr.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -42,7 +42,14 @@ export async function addIssueCommentBySlug(
|
||||
return { ok: false, error: number.error }
|
||||
}
|
||||
if (typeof args.body !== 'string' || !args.body.trim()) {
|
||||
return { ok: false, error: { type: 'validation_error', message: 'Comment body required.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: 'Comment body required.',
|
||||
reason: 'comment_body_required'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await runRest<RawIssueCommentResponse>(
|
||||
[
|
||||
@@ -73,7 +80,14 @@ export async function updateIssueCommentBySlug(
|
||||
return { ok: false, error: number.error }
|
||||
}
|
||||
if (typeof args.body !== 'string' || !args.body.trim()) {
|
||||
return { ok: false, error: { type: 'validation_error', message: 'Comment body required.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: 'Comment body required.',
|
||||
reason: 'comment_body_required'
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = await runRest<unknown>(
|
||||
[
|
||||
|
||||
@@ -33,7 +33,10 @@ export async function updateIssueTypeBySlug(
|
||||
}
|
||||
const issueId = lookup.data.repository?.issue?.id
|
||||
if (!issueId) {
|
||||
return { ok: false, error: { type: 'not_found', message: 'Issue not found.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'not_found', message: 'Issue not found.', reason: 'issue_not_found' }
|
||||
}
|
||||
}
|
||||
const query = args.issueTypeId
|
||||
? `mutation($issueId:ID!, $issueTypeId:ID!) {
|
||||
|
||||
@@ -27,7 +27,10 @@ export async function updateIssueBySlug(
|
||||
return { ok: false, error: number.error }
|
||||
}
|
||||
if (!args.updates || typeof args.updates !== 'object') {
|
||||
return { ok: false, error: { type: 'validation_error', message: 'Updates required.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'validation_error', message: 'Updates required.', reason: 'updates_required' }
|
||||
}
|
||||
}
|
||||
const duplicateError = validateDuplicateUpdate(args)
|
||||
if (duplicateError) {
|
||||
@@ -75,7 +78,8 @@ function validateDuplicateUpdate(args: UpdateIssueBySlugArgs): GitHubProjectMuta
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: 'Duplicate target is only valid when closing as duplicate.'
|
||||
message: 'Duplicate target is only valid when closing as duplicate.',
|
||||
reason: 'duplicate_target_not_allowed'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +88,8 @@ function validateDuplicateUpdate(args: UpdateIssueBySlugArgs): GitHubProjectMuta
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: 'Duplicate target issue number is required.'
|
||||
message: 'Duplicate target issue number is required.',
|
||||
reason: 'duplicate_target_required'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,8 @@ export function classifyProjectError(
|
||||
) {
|
||||
return {
|
||||
type: 'auth_required',
|
||||
message: `Sign in to GitHub to load project tasks. Run \`gh auth login --hostname ${selectedHost}\`.`
|
||||
message: `Sign in to GitHub to load project tasks. Run \`gh auth login --hostname ${selectedHost}\`.`,
|
||||
host: selectedHost
|
||||
}
|
||||
}
|
||||
if (
|
||||
@@ -93,7 +94,8 @@ export function classifyProjectError(
|
||||
) {
|
||||
return {
|
||||
type: 'scope_missing',
|
||||
message: `GitHub project access needs additional scopes. Run \`gh auth refresh --hostname ${selectedHost} -s project -s read:org -s repo\`.`
|
||||
message: `GitHub project access needs additional scopes. Run \`gh auth refresh --hostname ${selectedHost} -s project -s read:org -s repo\`.`,
|
||||
host: selectedHost
|
||||
}
|
||||
}
|
||||
if (s.includes('rate limit') || s.includes('api rate limit exceeded')) {
|
||||
@@ -119,13 +121,19 @@ export function classifyProjectError(
|
||||
return {
|
||||
type: 'not_found',
|
||||
message: 'Project or view not found.',
|
||||
reason: 'project_or_view_not_found',
|
||||
details: firstNotFound
|
||||
? { path: firstNotFound.path, code: firstNotFound.extensions?.code }
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
if (s.includes('http 422') || s.includes('validation failed')) {
|
||||
return { type: 'validation_error', message: `Invalid request — ${stderr.trim()}` }
|
||||
return {
|
||||
type: 'validation_error',
|
||||
message: `Invalid request — ${stderr.trim()}`,
|
||||
reason: 'request_invalid',
|
||||
detail: stderr.trim()
|
||||
}
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
const first = errors[0]
|
||||
@@ -144,7 +152,9 @@ export function classifyProjectError(
|
||||
const safe = firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine
|
||||
return {
|
||||
type: 'unknown',
|
||||
message: safe ? `GitHub request failed: ${safe}` : 'GitHub request failed.'
|
||||
message: safe ? `GitHub request failed: ${safe}` : 'GitHub request failed.',
|
||||
reason: 'request_failed',
|
||||
...(safe ? { detail: safe } : {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +162,12 @@ export function driftError(
|
||||
reason: string,
|
||||
details?: { path?: (string | number)[]; code?: string }
|
||||
): GitHubProjectViewError {
|
||||
return { type: 'schema_drift', message: `Could not read this project view: ${reason}.`, details }
|
||||
return {
|
||||
type: 'schema_drift',
|
||||
message: `Could not read this project view: ${reason}.`,
|
||||
detail: reason,
|
||||
details
|
||||
}
|
||||
}
|
||||
|
||||
// Shape pre-spawn breaker failures like classified rate limits; the guard already handles missing snapshots.
|
||||
@@ -165,6 +180,7 @@ export function rateLimitedError(blocked: {
|
||||
const mins = Math.ceil(resetIn / 60)
|
||||
return {
|
||||
type: 'rate_limited',
|
||||
message: `GitHub rate limit nearly exhausted (${blocked.remaining}/${blocked.limit} left). Resets in ~${mins}m.`
|
||||
message: `GitHub rate limit nearly exhausted (${blocked.remaining}/${blocked.limit} left). Resets in ~${mins}m.`,
|
||||
rateLimit: { remaining: blocked.remaining, limit: blocked.limit, resetMinutes: mins }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ export async function updateProjectItemFieldValue(
|
||||
args: UpdateProjectItemFieldArgs
|
||||
): Promise<GitHubProjectMutationResult> {
|
||||
if (!args.projectId || !args.itemId || !args.fieldId) {
|
||||
return { ok: false, error: { type: 'validation_error', message: 'Missing ids.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'validation_error', message: 'Missing ids.', reason: 'missing_ids' }
|
||||
}
|
||||
}
|
||||
let valueFragment: string
|
||||
let valueVariable: { type: string; val: string | number }
|
||||
@@ -88,7 +91,10 @@ export async function clearProjectItemFieldValue(
|
||||
args: ClearProjectItemFieldArgs
|
||||
): Promise<GitHubProjectMutationResult> {
|
||||
if (!args.projectId || !args.itemId || !args.fieldId) {
|
||||
return { ok: false, error: { type: 'validation_error', message: 'Missing ids.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'validation_error', message: 'Missing ids.', reason: 'missing_ids' }
|
||||
}
|
||||
}
|
||||
const result = await runGraphql<unknown>(
|
||||
`mutation($projectId:ID!, $itemId:ID!, $fieldId:ID!) {
|
||||
|
||||
@@ -137,7 +137,10 @@ export async function fetchProjectViewsPage(args: {
|
||||
const top = res.data[root]
|
||||
const project = top?.projectV2 ?? null
|
||||
if (!project || typeof project.id !== 'string') {
|
||||
return { ok: false, error: { type: 'not_found', message: 'Project not found.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'not_found', message: 'Project not found.', reason: 'project_not_found' }
|
||||
}
|
||||
}
|
||||
const pageInfo = project.views?.pageInfo
|
||||
const views = (project.views?.nodes ?? []).filter((v): v is RawProjectView => v !== null)
|
||||
|
||||
@@ -159,7 +159,11 @@ export async function fetchItemsPageWithRaw(args: {
|
||||
if (!page) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'not_found', message: 'Project or view not found.' },
|
||||
error: {
|
||||
type: 'not_found',
|
||||
message: 'Project or view not found.',
|
||||
reason: 'project_or_view_not_found'
|
||||
},
|
||||
rawErrors: [],
|
||||
stderr
|
||||
}
|
||||
|
||||
@@ -19,7 +19,14 @@ export async function listProjectViews(
|
||||
return { ok: false, error: numCheck.error }
|
||||
}
|
||||
if (args.ownerType !== 'organization' && args.ownerType !== 'user') {
|
||||
return { ok: false, error: { type: 'validation_error', message: 'Invalid ownerType.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: 'Invalid ownerType.',
|
||||
reason: 'invalid_owner_type'
|
||||
}
|
||||
}
|
||||
}
|
||||
const summaries: GitHubProjectViewSummary[] = []
|
||||
let cursor: string | null = null
|
||||
|
||||
@@ -111,12 +111,18 @@ async function resolveOwnerType(
|
||||
}
|
||||
const top = res.data[root]
|
||||
if (!top) {
|
||||
return { ok: false, error: { type: 'not_found', message: 'Owner not found.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'not_found', message: 'Owner not found.', reason: 'owner_not_found' }
|
||||
}
|
||||
}
|
||||
if (num) {
|
||||
const p = top.projectV2
|
||||
if (!p || typeof p.id !== 'string') {
|
||||
return { ok: false, error: { type: 'not_found', message: 'Project not found.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'not_found', message: 'Project not found.', reason: 'project_not_found' }
|
||||
}
|
||||
}
|
||||
return { ok: true, title: p.title ?? '' }
|
||||
}
|
||||
@@ -153,7 +159,11 @@ async function resolveOwnerType(
|
||||
rememberOwnerType(owner, null, host)
|
||||
return {
|
||||
ok: false,
|
||||
error: lastError ?? { type: 'not_found', message: 'Owner not found.' }
|
||||
error: lastError ?? {
|
||||
type: 'not_found',
|
||||
message: 'Owner not found.',
|
||||
reason: 'owner_not_found'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,13 +174,17 @@ export async function resolveProjectRef(
|
||||
if (!input) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'validation_error', message: 'Input required.' }
|
||||
error: { type: 'validation_error', message: 'Input required.', reason: 'input_required' }
|
||||
}
|
||||
}
|
||||
if (isGitHubProjectRefInputTooLarge(input)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'validation_error', message: GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR }
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: GITHUB_PROJECT_REF_INPUT_TOO_LARGE_ERROR,
|
||||
reason: 'input_too_large'
|
||||
}
|
||||
}
|
||||
}
|
||||
const parsed = parseProjectPaste(input, args.host)
|
||||
@@ -179,7 +193,8 @@ export async function resolveProjectRef(
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: 'Could not parse input. Expected a GitHub project URL or `owner/number`.'
|
||||
message: 'Could not parse input. Expected a GitHub project URL or `owner/number`.',
|
||||
reason: 'input_unparseable'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,7 +221,10 @@ export async function resolveProjectRef(
|
||||
}
|
||||
const p = res.data[root]?.projectV2
|
||||
if (!p || typeof p.id !== 'string') {
|
||||
return { ok: false, error: { type: 'not_found', message: 'Project not found.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'not_found', message: 'Project not found.', reason: 'project_not_found' }
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
|
||||
@@ -28,7 +28,11 @@ export async function getProjectViewTable(
|
||||
if (args.ownerType !== 'organization' && args.ownerType !== 'user') {
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'validation_error', message: 'Invalid ownerType.' }
|
||||
error: {
|
||||
type: 'validation_error',
|
||||
message: 'Invalid ownerType.',
|
||||
reason: 'invalid_owner_type'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +83,10 @@ export async function getProjectViewTable(
|
||||
}
|
||||
}
|
||||
if (!project) {
|
||||
return { ok: false, error: { type: 'not_found', message: 'Project not found.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'not_found', message: 'Project not found.', reason: 'project_not_found' }
|
||||
}
|
||||
}
|
||||
const noSelector =
|
||||
args.viewId === undefined && args.viewNumber === undefined && args.viewName === undefined
|
||||
@@ -91,7 +98,14 @@ export async function getProjectViewTable(
|
||||
null
|
||||
}
|
||||
if (!selectedRaw) {
|
||||
return { ok: false, error: { type: 'not_found', message: 'Could not find the selected view.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
type: 'not_found',
|
||||
message: 'Could not find the selected view.',
|
||||
reason: 'view_not_found'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let extraFields: RawProjectV2Field[] = []
|
||||
|
||||
@@ -14,7 +14,10 @@ export async function updatePullRequestBySlug(
|
||||
return { ok: false, error: number.error }
|
||||
}
|
||||
if (!args.updates || typeof args.updates !== 'object') {
|
||||
return { ok: false, error: { type: 'validation_error', message: 'Updates required.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'validation_error', message: 'Updates required.', reason: 'updates_required' }
|
||||
}
|
||||
}
|
||||
const patchArgs: string[] = [
|
||||
'-X',
|
||||
|
||||
@@ -45,7 +45,10 @@ export async function getWorkItemDetailsBySlug(
|
||||
return { ok: false, error: number.error }
|
||||
}
|
||||
if (args.type !== 'issue' && args.type !== 'pr') {
|
||||
return { ok: false, error: { type: 'validation_error', message: 'Invalid type.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'validation_error', message: 'Invalid type.', reason: 'invalid_item_type' }
|
||||
}
|
||||
}
|
||||
const contentFragment = args.type === 'issue' ? ISSUE_FRAGMENT : PULL_REQUEST_FRAGMENT
|
||||
const result = await runGraphql<{
|
||||
@@ -66,7 +69,10 @@ export async function getWorkItemDetailsBySlug(
|
||||
const raw =
|
||||
args.type === 'issue' ? result.data.repository?.issue : result.data.repository?.pullRequest
|
||||
if (!raw) {
|
||||
return { ok: false, error: { type: 'not_found', message: 'Item not found.' } }
|
||||
return {
|
||||
ok: false,
|
||||
error: { type: 'not_found', message: 'Item not found.', reason: 'item_not_found' }
|
||||
}
|
||||
}
|
||||
return { ok: true, details: mapWorkItemDetails(raw, args) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user