fix(i18n): substitute {providerName} & sibling placeholder tokens instead of concatenating (#3759) (#3792)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
sanil-23
2026-06-22 10:58:01 +05:30
committed by GitHub
co-authored by Steven Enamakel
parent c8d71b7325
commit 7c54fd7c7d
8 changed files with 86 additions and 9 deletions
+25
View File
@@ -436,7 +436,27 @@ jobs:
image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0
env:
CARGO_INCREMENTAL: "0"
# The instrumented Tauri shell binary (whole tauri/zbus/CEF graph) is
# dominated by DWARF debug info, which exhausts the runner disk and
# SIGBUSes the linker (collect2: ld terminated with signal 7) during the
# final link. Drop debug info — llvm-cov reads coverage from the embedded
# __llvm_covmap/__llvm_covfun sections, NOT from DWARF, so coverage
# numbers are unaffected. Mirrors rust-core-coverage.
CARGO_PROFILE_DEV_DEBUG: "0"
steps:
- name: Free disk space
run: |
# Coverage-instrumented builds produce huge binaries that exhaust the
# runner's ~14GB free disk during linking (SIGBUS). Inside a container,
# host paths are bind-mounted:
# /opt/hostedtoolcache → /__t
# /home/runner/work → /__w
# Deleting /__t frees ~8GB on the same partition the build uses.
# Mirrors rust-core-coverage.
rm -rf /__t/* || true
echo "Disk after cleanup:"
df -h /__w || true
- name: Checkout code
uses: actions/checkout@v5
with:
@@ -471,6 +491,11 @@ jobs:
- name: Run cargo llvm-cov for Tauri shell
run: bash scripts/ci-cancel-aware.sh cargo llvm-cov --manifest-path app/src-tauri/Cargo.toml --lcov --output-path lcov-tauri.info
env:
# Serialize codegen/link to cap peak memory + disk during the
# instrumented Tauri-shell link (SIGBUS otherwise). Mirrors
# rust-core-coverage.
CARGO_BUILD_JOBS: "1"
- name: Save CEF binary distribution cache
if: always() && steps.cef-cache.outputs.cache-hit != 'true'
+6 -2
View File
@@ -226,7 +226,9 @@ const WebviewHost = ({ accountId, provider }: WebviewHostProps) => {
/>
<span
className={`text-xs font-medium tracking-wide ${isLoading ? '' : 'text-stone-500 dark:text-neutral-400'}`}>
{isLoading ? `${t('accounts.webviewHost.loading')} ${providerName}...` : providerName}
{isLoading
? t('accounts.webviewHost.loading').replace('{providerName}', providerName)
: providerName}
</span>
{isLoading ? (
<div
@@ -251,7 +253,9 @@ const WebviewHost = ({ accountId, provider }: WebviewHostProps) => {
aria-live="polite"
aria-label={t('accounts.webviewHost.loadTimeout')}>
<div className="max-w-sm space-y-1">
<p className="text-sm font-semibold text-stone-800 dark:text-neutral-100">{`${providerName} ${t('accounts.webviewHost.takingLonger')}`}</p>
<p className="text-sm font-semibold text-stone-800 dark:text-neutral-100">
{t('accounts.webviewHost.takingLonger').replace('{providerName}', providerName)}
</p>
<p className="text-xs text-stone-600 dark:text-neutral-300">
{t('accounts.webviewHost.timeoutHint')}
</p>
@@ -83,6 +83,25 @@ describe('WebviewHost — issue #1233 loading UX', () => {
expect(screen.queryByTestId(`webview-loading-${ACCOUNT_ID}`)).not.toBeInTheDocument();
});
// Issue #3759: the loading/timeout copy must SUBSTITUTE the provider name
// into the `{providerName}` slot, not concatenate it — otherwise the raw
// placeholder token leaks to screen ("Loading {providerName}... Slack...").
it('substitutes the provider name into the loading copy (issue #3759)', () => {
seedAccount('loading');
renderHost();
const placeholder = screen.getByTestId(`webview-placeholder-${ACCOUNT_ID}`);
expect(placeholder).toHaveTextContent('Loading Slack...');
expect(placeholder.textContent).not.toContain('{providerName}');
});
it('substitutes the provider name into the timeout copy (issue #3759)', () => {
seedAccount('timeout');
renderHost();
const timeout = screen.getByTestId(`webview-timeout-${ACCOUNT_ID}`);
expect(timeout).toHaveTextContent('Slack is taking longer than expected.');
expect(timeout.textContent).not.toContain('{providerName}');
});
it('renders the phase hint after 5s of loading and escalates after 10s', () => {
seedAccount('loading');
renderHost();
@@ -716,3 +716,30 @@ describe('<ComposioConnectModal> — WhatsApp WABA id parity (#2127)', () => {
});
});
});
describe('ComposioConnectModal — connection-failed copy (issue #3759)', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('substitutes the status into the failure message when polling sees a FAILED connection', async () => {
// A PENDING connection resumes polling on mount; the first poll observes a
// FAILED connection and must SUBSTITUTE {status} into the error copy
// (issue #3759) — not concatenate/leak the literal placeholder.
vi.mocked(composioApi.listConnections).mockResolvedValue({
connections: [{ id: 'ca_err', toolkit: 'gmail', status: 'FAILED' }],
});
render(
<ComposioConnectModal
toolkit={mockToolkit}
connections={[{ id: 'ca_pending', toolkit: 'gmail', status: 'PENDING' }]}
onClose={() => {}}
/>
);
expect(await screen.findByText('Connection failed (status: FAILED).')).toBeInTheDocument();
// Regression guard: the raw placeholder must never reach the screen.
expect(screen.queryByText(/\{status\}/)).not.toBeInTheDocument();
});
});
@@ -298,7 +298,9 @@ export default function ComposioConnectModal({
if (state === 'error') {
stopPolling();
setPhase('error');
setError(`${t('composio.connect.connectionFailed')} (status: ${hit.status}).`);
setError(
t('composio.connect.connectionFailed').replace('{status}', String(hit.status))
);
return;
}
if (state === 'expired') {
@@ -539,7 +541,7 @@ export default function ComposioConnectModal({
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
setPhase('error');
setError(`${t('composio.connect.disconnectFailed')}: ${msg}`);
setError(t('composio.connect.disconnectFailed').replace('{msg}', msg));
setClearMemoryOnDisconnect(false);
}
},
@@ -527,7 +527,7 @@ export default function IntelligenceTasksTab() {
const title =
thread?.title && thread.title.trim().length > 0
? thread.title
: `${t('intelligence.tasks.threadPrefix')} ${shortId(threadId)}`;
: t('intelligence.tasks.threadPrefix').replace('{thread}', shortId(threadId));
boardEntries.push({ threadId, title, board, live: Boolean(liveBoard) });
}
+2 -2
View File
@@ -3375,7 +3375,7 @@ const en: TranslationMap = {
'composio.connect.dynamicsOrgNameHint':
'For example, "myorg" for myorg.crm.dynamics.com. Enter the short org name only, not the full URL.',
'composio.connect.dynamicsOrgNameLabel': 'Dynamics 365 Organization Name',
'composio.connect.connectionFailed': 'Connection failed (status: ${hit.status}).',
'composio.connect.connectionFailed': 'Connection failed (status: {status}).',
'composio.connect.disconnectFailed': 'Disconnect failed: {msg}',
'composio.connect.disconnecting': 'Disconnecting…',
'composio.connect.idleDescription': 'Connect your',
@@ -3594,7 +3594,7 @@ const en: TranslationMap = {
'intelligence.tasks.failedToLoad': 'Failed to load',
'intelligence.tasks.live': 'live',
'intelligence.tasks.loadingBoards': 'Loading task boards…',
'intelligence.tasks.threadPrefix': 'Thread {id}',
'intelligence.tasks.threadPrefix': 'Thread {thread}',
'intelligence.tasks.subtitle': 'Your tasks and agent task boards across the workspace.',
'intelligence.tasks.newTask': 'New task',
'intelligence.tasks.personalBoardTitle': 'Agent Tasks',
+2 -2
View File
@@ -2709,7 +2709,7 @@ const messages: TranslationMap = {
'composio.connect.dynamicsOrgNameHint':
'例如,myorg.crm.dynamics.com 的组织名称为 "myorg"。仅输入简短的组织名称,而不是完整 URL。',
'composio.connect.dynamicsOrgNameLabel': 'Dynamics 365 组织名称',
'composio.connect.connectionFailed': '连接失败。',
'composio.connect.connectionFailed': '连接失败(状态:{status}。',
'composio.connect.disconnectFailed': '断开连接失败:{msg}',
'composio.connect.disconnecting': '断开中…',
'composio.connect.idleDescription': '连接你的',
@@ -2899,7 +2899,7 @@ const messages: TranslationMap = {
'intelligence.tasks.failedToLoad': '加载失败',
'intelligence.tasks.live': '实时',
'intelligence.tasks.loadingBoards': '正在加载任务看板…',
'intelligence.tasks.threadPrefix': '对话',
'intelligence.tasks.threadPrefix': '对话 {thread}',
'intelligence.tasks.subtitle': '整个工作区中的你的任务和智能体任务看板。',
'intelligence.tasks.newTask': '新建任务',
'intelligence.tasks.personalBoardTitle': '智能体任务',