fix(i18n): localize the integration card status pills (#12208)

Every card under Settings -> Integrations built its status pill from bare
literals inside the JSX call, so the pill stayed English beside a translated
description. audit-localization-coverage.mjs inspects JSX attributes and object
properties, not conditional expressions handed to a prop, so the gate stayed
green while the strings shipped untranslated.

The three token-configured cards share tokenProviderStatusLabel(); Gitea passes
optional: true because it works read-only without a token, which is why its
unconfigured state reads Optional setup rather than Not configured.

Co-authored-by: Evgenii <kumiro@me.com>
This commit is contained in:
Evgenii
2026-08-04 03:56:33 -07:00
committed by Neil
parent 439a8c46cf
commit 4e8f136052
6 changed files with 128 additions and 36 deletions
@@ -99,7 +99,14 @@ export function JiraIntegrationCard(): React.JSX.Element {
}
checking={checking}
statusTone={connected ? 'connected' : 'attention'}
statusLabel={connected ? 'Connected' : 'Not connected'}
statusLabel={
connected
? translate('auto.components.settings.jira.integration.card.statusConnected', 'Connected')
: translate(
'auto.components.settings.jira.integration.card.statusNotConnected',
'Not connected'
)
}
actions={
!checking ? (
<Button
@@ -91,7 +91,17 @@ export function LinearIntegrationCard(): React.JSX.Element {
}
checking={checking}
statusTone={connected ? 'connected' : 'attention'}
statusLabel={connected ? 'Connected' : 'Not connected'}
statusLabel={
connected
? translate(
'auto.components.settings.task.tracker.integration.cards.statusConnected',
'Connected'
)
: translate(
'auto.components.settings.task.tracker.integration.cards.statusNotConnected',
'Not connected'
)
}
actions={
!checking ? (
<Button
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'
import { IntegrationCardDetails, IntegrationCardShell } from './integration-card-shell'
import { usePreflightCardStatuses } from './source-control-preflight-card-status'
import { translate } from '@/i18n/i18n'
import { tokenProviderStatusLabel } from './token-source-control-status'
export function BitbucketIntegrationCard(): React.JSX.Element {
const { statuses, unavailable, refresh } = usePreflightCardStatuses('bitbucket')
@@ -32,15 +33,7 @@ export function BitbucketIntegrationCard(): React.JSX.Element {
}
checking={status === 'checking'}
statusTone={connected ? 'connected' : 'attention'}
statusLabel={
connected
? 'Connected'
: status === 'unavailable'
? 'Unavailable'
: status === 'not-configured'
? 'Not configured'
: 'Auth failed'
}
statusLabel={tokenProviderStatusLabel({ configured: connected, status })}
>
{status !== 'checking' && !connected ? (
<IntegrationCardDetails>
@@ -154,17 +147,11 @@ export function AzureDevOpsIntegrationCard(): React.JSX.Element {
}
checking={status === 'checking'}
statusTone={configured ? 'connected' : 'attention'}
statusLabel={
configured
? statuses.azureDevOpsAccount
? 'Connected'
: 'Configured'
: status === 'unavailable'
? 'Unavailable'
: status === 'not-configured'
? 'Not configured'
: 'Auth failed'
}
statusLabel={tokenProviderStatusLabel({
configured,
hasAccount: Boolean(statuses.azureDevOpsAccount),
status
})}
>
{status !== 'checking' && !configured ? (
<IntegrationCardDetails>
@@ -283,17 +270,12 @@ export function GiteaIntegrationCard(): React.JSX.Element {
}
checking={status === 'checking'}
statusTone={configured ? 'connected' : 'attention'}
statusLabel={
configured
? statuses.giteaAccount
? 'Connected'
: 'Configured'
: status === 'unavailable'
? 'Unavailable'
: status === 'not-configured'
? 'Optional setup'
: 'Auth failed'
}
statusLabel={tokenProviderStatusLabel({
configured,
hasAccount: Boolean(statuses.giteaAccount),
status,
optional: true
})}
>
{status !== 'checking' && !configured ? (
<IntegrationCardDetails>
@@ -0,0 +1,39 @@
import { translate } from '@/i18n/i18n'
import type { AzureDevOpsStatus, BitbucketStatus, GiteaStatus } from './integrations-pane-status'
/**
* Card status as the pane computes it: the provider's own state, or
* `'unavailable'` when the runtime cannot report on it at all.
*/
export type TokenProviderStatus = BitbucketStatus | AzureDevOpsStatus | GiteaStatus | 'unavailable'
const NS = 'auto.components.settings.token.source.control.integration.cards'
/**
* Status copy for the token-configured source-control cards (Bitbucket, Azure
* DevOps, Gitea). They share the same states, so the labels live in one place.
*/
export function tokenProviderStatusLabel(input: {
/** Credentials present for the provider. */
configured: boolean
/** An account was resolved with those credentials. */
hasAccount?: boolean
status: TokenProviderStatus
/** Gitea works read-only without a token, so its unconfigured state is softer. */
optional?: boolean
}): string {
if (input.configured) {
return input.hasAccount === false
? translate(`${NS}.statusConfigured`, 'Configured')
: translate(`${NS}.statusConnected`, 'Connected')
}
if (input.status === 'unavailable') {
return translate(`${NS}.statusUnavailable`, 'Unavailable')
}
if (input.status === 'not-configured') {
return input.optional
? translate(`${NS}.statusOptionalSetup`, 'Optional setup')
: translate(`${NS}.statusNotConfigured`, 'Not configured')
}
return translate(`${NS}.statusAuthFailed`, 'Auth failed')
}
@@ -0,0 +1,44 @@
/**
* Integration cards build their status pill inside the JSX call, which the
* coverage audit does not inspect — it looks at JSX attributes and object
* properties, not conditional expressions passed to a prop. These assertions
* pin the catalog contract so a card that goes back to a bare literal fails here.
*/
import { describe, expect, it } from 'vitest'
import en from './locales/en.json'
function lookup(key: string): string | undefined {
const value = key
.split('.')
.reduce<unknown>(
(node, part) =>
node && typeof node === 'object' ? (node as Record<string, unknown>)[part] : undefined,
en as unknown
)
return typeof value === 'string' ? value : undefined
}
const REQUIRED_KEYS: Record<string, string> = {
// Linear and other task trackers
'auto.components.settings.task.tracker.integration.cards.statusConnected': 'Connected',
'auto.components.settings.task.tracker.integration.cards.statusNotConnected': 'Not connected',
// Jira
'auto.components.settings.jira.integration.card.statusConnected': 'Connected',
'auto.components.settings.jira.integration.card.statusNotConnected': 'Not connected',
// Bitbucket, Azure DevOps, Gitea
'auto.components.settings.token.source.control.integration.cards.statusConnected': 'Connected',
'auto.components.settings.token.source.control.integration.cards.statusConfigured': 'Configured',
'auto.components.settings.token.source.control.integration.cards.statusUnavailable':
'Unavailable',
'auto.components.settings.token.source.control.integration.cards.statusNotConfigured':
'Not configured',
'auto.components.settings.token.source.control.integration.cards.statusOptionalSetup':
'Optional setup',
'auto.components.settings.token.source.control.integration.cards.statusAuthFailed': 'Auth failed'
}
describe('integration card status labels', () => {
it.each(Object.entries(REQUIRED_KEYS))('keeps %s in the English catalog', (key, expected) => {
expect(lookup(key)).toBe(expected)
})
})
+13 -3
View File
@@ -8489,7 +8489,9 @@
"d5c5b47bb9": "update",
"fb854902d9": "connecting",
"9a9f8d4910": "Connect Jira Cloud to browse, create, and link issues.",
"74f3063026": "{{value0}} site{{value1}} connected"
"74f3063026": "{{value0}} site{{value1}} connected",
"statusConnected": "Connected",
"statusNotConnected": "Not connected"
}
}
},
@@ -9312,7 +9314,9 @@
"disconnect_all": "Disconnect",
"account_scope_prefix": "Account scope",
"2d60ec7921": "Connect a Jira Cloud site with an API token, or a self-hosted Jira with a personal access token or username and password. Credentials are sent to the selected remote runtime and stored there with runtime-supported encryption.",
"977e360b71": "Connect a Jira Cloud site with an API token, or a self-hosted Jira with a personal access token or username and password. Credentials are stored locally and encrypted when local runtime storage supports it."
"977e360b71": "Connect a Jira Cloud site with an API token, or a self-hosted Jira with a personal access token or username and password. Credentials are stored locally and encrypted when local runtime storage supports it.",
"statusConnected": "Connected",
"statusNotConnected": "Not connected"
}
}
}
@@ -9353,7 +9357,13 @@
"63a7f47392": "ORCA_BITBUCKET_EMAIL",
"24ac1c69dc": "Bitbucket status is not available in this runtime yet.",
"a924e8dcd1": "Pull requests and build statuses via Bitbucket Cloud API tokens.",
"0fa5629dad": "Pull requests and build statuses"
"0fa5629dad": "Pull requests and build statuses",
"statusConnected": "Connected",
"statusUnavailable": "Unavailable",
"statusNotConfigured": "Not configured",
"statusAuthFailed": "Auth failed",
"statusConfigured": "Configured",
"statusOptionalSetup": "Optional setup"
}
}
}