From c646c8809f33d6eac445573b2b4feb2768fa123a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 15:38:18 +0200 Subject: [PATCH 1/6] feat(mobile): keep agent composers editable during live dictation --- .../chat-composer-input-row.mounted.test.tsx | 29 +++ .../agents/chat-composer-input-row.tsx | 82 ++++--- .../agents/chat-composer-input-state.test.ts | 6 +- .../agents/chat-composer-input-state.ts | 9 +- .../src/components/agents/chat-composer.tsx | 46 +++- .../agents/new-session-prompt-state.test.ts | 6 +- .../agents/new-session-prompt-state.ts | 12 +- .../components/agents/new-session-prompt.tsx | 46 +++- apps/mobile/src/i18n/locales/en.json | 1 + .../lib/voice-input/voice-input-draft.test.ts | 200 +++++++++++++++++- .../src/lib/voice-input/voice-input-draft.ts | 136 +++++++++++- 11 files changed, 515 insertions(+), 58 deletions(-) diff --git a/apps/mobile/src/components/agents/chat-composer-input-row.mounted.test.tsx b/apps/mobile/src/components/agents/chat-composer-input-row.mounted.test.tsx index 2b52e6a5dd..faab7c2341 100644 --- a/apps/mobile/src/components/agents/chat-composer-input-row.mounted.test.tsx +++ b/apps/mobile/src/components/agents/chat-composer-input-row.mounted.test.tsx @@ -11,6 +11,12 @@ vi.mock('react-native', () => ({ TextInput: 'TextInput', View: 'View', })); +vi.mock('react-native-reanimated', () => ({ + default: { View: 'Animated.View' }, + FadeIn: { duration: vi.fn(() => ({})) }, + FadeOut: { duration: vi.fn(() => ({})) }, + useReducedMotion: () => false, +})); vi.mock('@/components/ui/icons', () => ({ ArrowUp: 'ArrowUp', Paperclip: 'Paperclip', @@ -31,6 +37,7 @@ type RenderProps = { hasSendableContent?: boolean; inputEditable: boolean; isStreaming?: boolean; + voiceInputAvailable?: boolean; }; function makeProps(overrides: Partial = {}) { @@ -69,6 +76,13 @@ function findTextInput(root: TestRenderer.ReactTestInstance): TestRenderer.React return root.find(node => typeof node.type === 'string' && (node.type as string) === 'TextInput'); } +function findAllByType( + root: TestRenderer.ReactTestInstance, + type: string +): TestRenderer.ReactTestInstance[] { + return root.findAll(node => typeof node.type === 'string' && (node.type as string) === type); +} + function findByAccessibilityLabel( root: TestRenderer.ReactTestInstance, label: string @@ -144,4 +158,19 @@ describe('ChatComposerInputRow mounted — iOS writing-tools lock', () => { renderer.unmount(); }); + + it('keeps the microphone mounted beside Stop while streaming', async () => { + const renderer = await renderRow({ + inputEditable: true, + isStreaming: true, + canSend: false, + hasSendableContent: false, + voiceInputAvailable: true, + }); + + expect(findAllByType(renderer.root, 'VoiceInputButton')).toHaveLength(1); + expect(findByAccessibilityLabel(renderer.root, 'Stop generating')).not.toBeNull(); + + renderer.unmount(); + }); }); diff --git a/apps/mobile/src/components/agents/chat-composer-input-row.tsx b/apps/mobile/src/components/agents/chat-composer-input-row.tsx index 9b878c3949..e399e79f1c 100644 --- a/apps/mobile/src/components/agents/chat-composer-input-row.tsx +++ b/apps/mobile/src/components/agents/chat-composer-input-row.tsx @@ -1,6 +1,7 @@ import { ArrowUp, Paperclip, Square } from '@/components/ui/icons'; import { CLOUD_AGENT_PROMPT_MAX_LENGTH } from '@kilocode/cloud-agent-sdk/limits'; import { type RefObject } from 'react'; +import Animated, { FadeIn, FadeOut, useReducedMotion } from 'react-native-reanimated'; import { useTranslation } from 'react-i18next'; import { ActivityIndicator, @@ -87,6 +88,7 @@ export function ChatComposerInputRow({ }: Readonly) { const colors = useThemeColors(); const { t } = useTranslation(); + const reducedMotion = useReducedMotion(); const inputScrollable = shouldEnableComposerInputScroll(measureHeight, maxInputHeight); return ( @@ -138,7 +140,7 @@ export function ChatComposerInputRow({ /> - {!isStreaming && voiceInputAvailable ? ( + {voiceInputAvailable ? ( - - + + + + ) : ( - - {isSending ? ( - - ) : ( - - )} - + + {isSending ? ( + + ) : ( + + )} + + )} ); diff --git a/apps/mobile/src/components/agents/chat-composer-input-state.test.ts b/apps/mobile/src/components/agents/chat-composer-input-state.test.ts index cbf0701577..96214e70bf 100644 --- a/apps/mobile/src/components/agents/chat-composer-input-state.test.ts +++ b/apps/mobile/src/components/agents/chat-composer-input-state.test.ts @@ -244,7 +244,7 @@ describe('resolveChatComposerControlState', () => { expect(state.paperclipDisabled).toBe(true); }); - it('disables the paperclip and input while this owner is voice active', () => { + it('disables the paperclip but keeps the input editable while this owner is voice active', () => { const state = resolveChatComposerControlState({ attachmentsCount: 0, sendableAttachmentsCount: 0, @@ -258,8 +258,8 @@ describe('resolveChatComposerControlState', () => { }); expect(state.paperclipDisabled).toBe(true); - expect(state.inputEditable).toBe(false); - expect(state.inputAccessibilityDisabled).toBe(true); + expect(state.inputEditable).toBe(true); + expect(state.inputAccessibilityDisabled).toBe(false); }); it('leaves voice enabled (only toolbar gates it) when the composer is otherwise ready', () => { diff --git a/apps/mobile/src/components/agents/chat-composer-input-state.ts b/apps/mobile/src/components/agents/chat-composer-input-state.ts index 4027e01790..1b22812188 100644 --- a/apps/mobile/src/components/agents/chat-composer-input-state.ts +++ b/apps/mobile/src/components/agents/chat-composer-input-state.ts @@ -36,8 +36,9 @@ type ChatComposerControlState = { * the rules in one place lets the component stay a thin presenter and makes * every state — happy, blocked, and listening — testable without rendering * the composer. Voice input integrates here too: an active voice session - * makes the input read-only and locks the attachment picker while speech is - * being recognized. + * locks the attachment picker while speech is being recognized, but it keeps + * the input editable so dictation can insert at the caret (a user edit during + * dictation aborts the session in the selection-aware draft path). */ export function resolveChatComposerControlState( input: ChatComposerControlInput @@ -63,7 +64,9 @@ export function resolveChatComposerControlState( const voiceDisabled = toolbarDisabled; const paperclipDisabled = toolbarDisabled || voiceInputActive || attachmentsCount >= attachmentMax; - const inputEditable = !toolbarDisabled && !voiceInputActive; + // Voice activity no longer makes the input read-only: dictation inserts at + // the caret, so the user can keep editing (an edit aborts the session). + const inputEditable = !toolbarDisabled; const showToolbar = isFocused || hasText || attachmentsCount > 0 || voiceInputActive; const hasSendableContent = hasText || sendableAttachmentsCount > 0; return { diff --git a/apps/mobile/src/components/agents/chat-composer.tsx b/apps/mobile/src/components/agents/chat-composer.tsx index 4d9dbf5c6a..7cb7249fe5 100644 --- a/apps/mobile/src/components/agents/chat-composer.tsx +++ b/apps/mobile/src/components/agents/chat-composer.tsx @@ -18,6 +18,7 @@ import { useState, } from 'react'; import { + AccessibilityInfo, Alert, AppState, type GestureResponderEvent, @@ -101,7 +102,10 @@ import { } from '@/lib/composer-auto-send'; import { createSubmitLock, type SubmitLock } from '@/lib/submit-lock'; import { useVoiceInput } from '@/lib/voice-input/use-voice-input'; -import { applyVoiceDraftToInput } from '@/lib/voice-input/voice-input-draft'; +import { + applyVoiceDraftAtSelection, + type VoiceInputSelection, +} from '@/lib/voice-input/voice-input-draft'; import { settleVoiceInputBeforeSubmit } from '@/lib/voice-input/voice-input-submit'; const TEXT_INPUT_MAX_LINES = 5; @@ -239,6 +243,18 @@ export function ChatComposer({ // Last caret the input reported. Paste inserts here so the button behaves // like the platform paste. const selectionRef = useRef(null); + // Selection-aware dictation state: the caret captured at session start, the + // draft the last speech result produced, and the abort trigger. A user edit + // (including an IME edit, which fires onChangeText) diverges the live draft + // from the expected draft, so the next speech result aborts instead of + // inserting into the edit. + const voiceBaseDraftRef = useRef(''); + const voiceBaseSelectionRef = useRef(null); + const voiceExpectedDraftRef = useRef(''); + // RN 0.86 exposes no IME composition event, so this stays false; the draft + // divergence above is what aborts dictation when an IME session edits text. + const isComposingRef = useRef(false); + const abortVoiceInputRef = useRef<(() => Promise) | null>(null); const inputFocusedRef = useRef(false); const restoreFocusOnActiveRef = useRef(false); const restoreFocusTimeoutRef = useRef | null>(null); @@ -546,16 +562,38 @@ export function ChatComposer({ const voiceInput = useVoiceInput({ disabled: voiceDisabled, - getDraft: () => textRef.current, + getDraft: () => { + // The controller calls getDraft exactly once, at session start, to + // snapshot the base draft. Capture the caret at the same instant so the + // selection-aware insert path knows where to splice the transcript. + voiceBaseDraftRef.current = textRef.current; + voiceBaseSelectionRef.current = selectionRef.current; + voiceExpectedDraftRef.current = textRef.current; + return textRef.current; + }, onDraftChange: draft => { - applyVoiceDraftToInput({ + const result = applyVoiceDraftAtSelection({ + baseDraft: voiceBaseDraftRef.current, + baseSelection: voiceBaseSelectionRef.current, + currentDraft: textRef.current, + expectedDraft: voiceExpectedDraftRef.current, + mergedDraft: draft, + isComposing: isComposingRef.current, input: inputRef.current, - draft, maxLength: CLOUD_AGENT_PROMPT_MAX_LENGTH, onChangeText: handleChangeText, }); + if (result.kind === 'aborted') { + // The user edited the live speech range or an IME session is composing: + // keep their text, stop recognition, and announce the stop once. + AccessibilityInfo.announceForAccessibility(i18n.t('voiceInput.listeningStopped')); + void abortVoiceInputRef.current?.(); + return; + } + voiceExpectedDraftRef.current = result.draft; }, }); + abortVoiceInputRef.current = voiceInput.abort; const control = resolveChatComposerControlState({ attachmentsCount: upload.attachments.length, diff --git a/apps/mobile/src/components/agents/new-session-prompt-state.test.ts b/apps/mobile/src/components/agents/new-session-prompt-state.test.ts index aeffd94048..c139d89ab6 100644 --- a/apps/mobile/src/components/agents/new-session-prompt-state.test.ts +++ b/apps/mobile/src/components/agents/new-session-prompt-state.test.ts @@ -75,7 +75,7 @@ describe('resolveNewSessionPromptControlState', () => { expect(state.paperclipDisabled).toBe(true); }); - it('disables the text input and paperclip while this owner is voice active', () => { + it('disables the paperclip but keeps the prompt editable while this owner is voice active', () => { const state = resolveNewSessionPromptControlState({ attachmentsCount: 0, attachmentMax: 5, @@ -84,8 +84,8 @@ describe('resolveNewSessionPromptControlState', () => { voiceInputActive: true, }); - expect(state.inputEditable).toBe(false); - expect(state.inputAccessibilityDisabled).toBe(true); + expect(state.inputEditable).toBe(true); + expect(state.inputAccessibilityDisabled).toBe(false); expect(state.paperclipDisabled).toBe(true); expect(state.voiceDisabled).toBe(false); expect(state.hasPrompt).toBe(true); diff --git a/apps/mobile/src/components/agents/new-session-prompt-state.ts b/apps/mobile/src/components/agents/new-session-prompt-state.ts index d834bac60f..d6ac872121 100644 --- a/apps/mobile/src/components/agents/new-session-prompt-state.ts +++ b/apps/mobile/src/components/agents/new-session-prompt-state.ts @@ -41,10 +41,10 @@ export function resolveNewSessionPromptForCreate(rawPrompt: string): string | nu * Pure projection of the new-session prompt row's gating state. The * component file stays a thin presenter and every state — happy, in-flight, * voice-active — is testable without rendering React Native. Voice input - * integrates here too: an active voice session makes the prompt read-only - * and locks the attachment picker while speech is being recognized, but it - * does not by itself disable the create button (Create only fires after the - * user presses "Start session"). + * integrates here too: an active voice session locks the attachment picker + * while speech is being recognized, but it keeps the prompt editable so + * dictation can insert at the caret. It does not by itself disable the + * create button (Create only fires after the user presses "Start session"). */ export function resolveNewSessionPromptControlState( input: NewSessionPromptControlInput @@ -54,7 +54,9 @@ export function resolveNewSessionPromptControlState( const createDisabled = isCreating; const voiceDisabled = isCreating; const paperclipDisabled = isCreating || voiceInputActive || attachmentsCount >= attachmentMax; - const inputEditable = !isCreating && !voiceInputActive; + // Voice activity no longer makes the prompt read-only: dictation inserts at + // the caret, so the user can keep editing (an edit aborts the session). + const inputEditable = !isCreating; return { createDisabled, hasPrompt, diff --git a/apps/mobile/src/components/agents/new-session-prompt.tsx b/apps/mobile/src/components/agents/new-session-prompt.tsx index ab27f3fa22..6c961c8b15 100644 --- a/apps/mobile/src/components/agents/new-session-prompt.tsx +++ b/apps/mobile/src/components/agents/new-session-prompt.tsx @@ -1,6 +1,7 @@ import { CLOUD_AGENT_PROMPT_MAX_LENGTH } from '@kilocode/cloud-agent-sdk/limits'; import { useCallback, useEffect, useRef, useState } from 'react'; import { + AccessibilityInfo, type LayoutChangeEvent, Platform, Pressable, @@ -28,7 +29,10 @@ import { QueryError } from '@/components/query-error'; import { useThemeColors } from '@/lib/hooks/use-theme-colors'; import { useSharePrefill } from '@/lib/share-prefill'; import { cn } from '@/lib/utils'; -import { applyVoiceDraftToInput } from '@/lib/voice-input/voice-input-draft'; +import { + applyVoiceDraftAtSelection, + type VoiceInputSelection, +} from '@/lib/voice-input/voice-input-draft'; import { useVoiceInput } from '@/lib/voice-input/use-voice-input'; import { VoiceInputButton, VoiceInputStatus } from '@/components/voice-input-control'; import { describeClassificationFailure } from '@/lib/agent-attachments/validate'; @@ -101,6 +105,18 @@ export function NewSessionPrompt({ // Last caret the input reported. Paste inserts here so the button behaves // like the platform paste. const promptSelectionRef = useRef(null); + // Selection-aware dictation state: the caret captured at session start, the + // draft the last speech result produced, and the abort trigger. A user edit + // (including an IME edit, which fires onChangeText) diverges the live draft + // from the expected draft, so the next speech result aborts instead of + // inserting into the edit. + const voiceBaseDraftRef = useRef(''); + const voiceBaseSelectionRef = useRef(null); + const voiceExpectedDraftRef = useRef(''); + // RN 0.86 exposes no IME composition event, so this stays false; the draft + // divergence above is what aborts dictation when an IME session edits text. + const isComposingRef = useRef(false); + const abortVoiceInputRef = useRef<(() => Promise) | null>(null); const [promptInputWidth, setPromptInputWidth] = useState(0); const promptMeasure = useTextHeight({ minHeight: PROMPT_INPUT_MIN_HEIGHT, @@ -135,16 +151,38 @@ export function NewSessionPrompt({ const voiceInput = useVoiceInput({ disabled: isCreating, - getDraft: () => promptRef.current, + getDraft: () => { + // The controller calls getDraft exactly once, at session start, to + // snapshot the base draft. Capture the caret at the same instant so the + // selection-aware insert path knows where to splice the transcript. + voiceBaseDraftRef.current = promptRef.current; + voiceBaseSelectionRef.current = promptSelectionRef.current; + voiceExpectedDraftRef.current = promptRef.current; + return promptRef.current; + }, onDraftChange: draft => { - applyVoiceDraftToInput({ + const result = applyVoiceDraftAtSelection({ + baseDraft: voiceBaseDraftRef.current, + baseSelection: voiceBaseSelectionRef.current, + currentDraft: promptRef.current, + expectedDraft: voiceExpectedDraftRef.current, + mergedDraft: draft, + isComposing: isComposingRef.current, input: promptInputRef.current, - draft, maxLength: PROMPT_INPUT_MAX_CHARS, onChangeText: handlePromptChange, }); + if (result.kind === 'aborted') { + // The user edited the live speech range or an IME session is composing: + // keep their text, stop recognition, and announce the stop once. + AccessibilityInfo.announceForAccessibility(t('voiceInput.listeningStopped')); + void abortVoiceInputRef.current?.(); + return; + } + voiceExpectedDraftRef.current = result.draft; }, }); + abortVoiceInputRef.current = voiceInput.abort; useEffect(() => { voiceInputSettlerRef.current = voiceInput.settleBeforeSubmit; diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index c2db100f09..3f95ecbfab 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -2773,6 +2773,7 @@ }, "voiceInput": { "listening": "Listening...", + "listeningStopped": "Listening stopped.", "staysOff": "Speech stays off until you allow online transcription.", "onlineTitle": "Speech is processed online", "onlineMessage": "This device cannot transcribe offline. Your recording is sent to {{provider}} to turn it into text. Kilo does not store the audio.", diff --git a/apps/mobile/src/lib/voice-input/voice-input-draft.test.ts b/apps/mobile/src/lib/voice-input/voice-input-draft.test.ts index 46f3a22c04..70373d6c7e 100644 --- a/apps/mobile/src/lib/voice-input/voice-input-draft.test.ts +++ b/apps/mobile/src/lib/voice-input/voice-input-draft.test.ts @@ -1,6 +1,13 @@ import { describe, expect, it, vi } from 'vitest'; -import { applyVoiceDraftToInput } from './voice-input-draft'; +import { + applyVoiceDraftAtSelection, + applyVoiceDraftToInput, + resolveVoiceInsertion, + resolveVoiceTranscriptDelta, + type VoiceInputSelection, +} from './voice-input-draft'; +import { appendVoiceTranscript } from './voice-input-state'; type NativeCall = { text: string }; type ChangeCall = string; @@ -118,3 +125,194 @@ describe('applyVoiceDraftToInput', () => { expect(onChangeText).toHaveBeenCalledTimes(1); }); }); + +type SelectionNativeCall = { text: string; selection?: VoiceInputSelection }; + +function makeSelectionInput() { + const calls: SelectionNativeCall[] = []; + const input = { + setNativeProps(props: { text: string; selection?: VoiceInputSelection }): void { + calls.push({ text: props.text, selection: props.selection }); + }, + }; + return { input, calls }; +} + +describe('resolveVoiceTranscriptDelta', () => { + it('recovers the trimmed transcript appended by appendVoiceTranscript for every base', () => { + const cases: { base: string; transcript: string }[] = [ + { base: '', transcript: 'hello' }, + { base: 'hello', transcript: 'world' }, + { base: 'hello ', transcript: 'world' }, + { base: 'hello\n', transcript: 'world' }, + { base: 'hello\t', transcript: 'world' }, + { base: 'hello', transcript: ' world again' }, + { base: 'hello', transcript: '' }, + ]; + + for (const { base, transcript } of cases) { + const merged = appendVoiceTranscript(base, transcript); + expect(resolveVoiceTranscriptDelta(base, merged)).toBe(transcript.trimStart()); + } + }); + + it('returns an empty delta when the merged draft no longer starts with the base', () => { + expect(resolveVoiceTranscriptDelta('hello', 'different world')).toBe(''); + }); +}); + +describe('resolveVoiceInsertion', () => { + it('inserts at the caret and lands the caret after the inserted text', () => { + const result = resolveVoiceInsertion('hello world', { start: 5, end: 5 }, 'there', undefined); + + expect(result.draft).toBe('hello there world'); + expect(result.selection).toEqual({ start: 11, end: 11 }); + }); + + it('replaces the selected range', () => { + const result = resolveVoiceInsertion('hello world', { start: 5, end: 11 }, 'there', undefined); + + expect(result.draft).toBe('hello there'); + expect(result.selection).toEqual({ start: 11, end: 11 }); + }); + + it('inserts at the draft end when no selection was reported', () => { + const result = resolveVoiceInsertion('hello', null, 'world', undefined); + + expect(result.draft).toBe('hello world'); + expect(result.selection).toEqual({ start: 11, end: 11 }); + }); + + it('truncates the transcript, not the surrounding text, at maxLength', () => { + const result = resolveVoiceInsertion('hello world', { start: 5, end: 5 }, 'there', 14); + + expect(result.draft).toBe('hello th world'); + expect(result.selection).toEqual({ start: 8, end: 8 }); + }); +}); + +describe('applyVoiceDraftAtSelection', () => { + it('inserts finalized speech at the caret instead of replacing the draft', () => { + const { input, calls } = makeSelectionInput(); + const changeCalls: string[] = []; + const mergedDraft = appendVoiceTranscript('hello world', 'there'); + + const result = applyVoiceDraftAtSelection({ + baseDraft: 'hello world', + baseSelection: { start: 5, end: 5 }, + currentDraft: 'hello world', + expectedDraft: 'hello world', + mergedDraft, + isComposing: false, + input, + onChangeText: draft => { + changeCalls.push(draft); + }, + }); + + expect(result).toEqual({ + kind: 'inserted', + draft: 'hello there world', + selection: { start: 11, end: 11 }, + }); + expect(calls).toEqual([{ text: 'hello there world', selection: { start: 11, end: 11 } }]); + expect(changeCalls).toEqual(['hello there world']); + }); + + it('replaces the prior interim on the next result (live range stays anchored at the caret)', () => { + const { input } = makeSelectionInput(); + const baseDraft = 'say hello'; + const firstMerged = appendVoiceTranscript(baseDraft, 'good'); + const secondMerged = appendVoiceTranscript(baseDraft, 'good morning'); + + const first = applyVoiceDraftAtSelection({ + baseDraft, + baseSelection: { start: 4, end: 4 }, + currentDraft: baseDraft, + expectedDraft: baseDraft, + mergedDraft: firstMerged, + isComposing: false, + input, + onChangeText: () => undefined, + }); + const second = applyVoiceDraftAtSelection({ + baseDraft, + baseSelection: { start: 4, end: 4 }, + currentDraft: first.kind === 'inserted' ? first.draft : baseDraft, + expectedDraft: first.kind === 'inserted' ? first.draft : baseDraft, + mergedDraft: secondMerged, + isComposing: false, + input, + onChangeText: () => undefined, + }); + + expect(second).toEqual({ + kind: 'inserted', + draft: 'say good morning hello', + selection: { start: 16, end: 16 }, + }); + }); + + it('aborts and leaves the user text alone when the live draft diverged from the expected draft', () => { + const { input, calls } = makeSelectionInput(); + const onChangeText = vi.fn<(draft: string) => void>(); + const mergedDraft = appendVoiceTranscript('hello world', 'there'); + + const result = applyVoiceDraftAtSelection({ + baseDraft: 'hello world', + baseSelection: { start: 5, end: 5 }, + currentDraft: 'hello edited world', + expectedDraft: 'hello world', + mergedDraft, + isComposing: false, + input, + onChangeText, + }); + + expect(result).toEqual({ kind: 'aborted' }); + expect(calls).toEqual([]); + expect(onChangeText).not.toHaveBeenCalled(); + }); + + it('aborts and skips the insert when an IME composition is active', () => { + const { input, calls } = makeSelectionInput(); + const onChangeText = vi.fn<(draft: string) => void>(); + const mergedDraft = appendVoiceTranscript('hello world', 'there'); + + const result = applyVoiceDraftAtSelection({ + baseDraft: 'hello world', + baseSelection: { start: 5, end: 5 }, + currentDraft: 'hello world', + expectedDraft: 'hello world', + mergedDraft, + isComposing: true, + input, + onChangeText, + }); + + expect(result).toEqual({ kind: 'aborted' }); + expect(calls).toEqual([]); + expect(onChangeText).not.toHaveBeenCalled(); + }); + + it('still reports the inserted draft when the input ref is null', () => { + const mergedDraft = appendVoiceTranscript('hello', 'world'); + + const result = applyVoiceDraftAtSelection({ + baseDraft: 'hello', + baseSelection: null, + currentDraft: 'hello', + expectedDraft: 'hello', + mergedDraft, + isComposing: false, + input: null, + onChangeText: () => undefined, + }); + + expect(result).toEqual({ + kind: 'inserted', + draft: 'hello world', + selection: { start: 11, end: 11 }, + }); + }); +}); diff --git a/apps/mobile/src/lib/voice-input/voice-input-draft.ts b/apps/mobile/src/lib/voice-input/voice-input-draft.ts index 062f1fa6ee..d8737f5c43 100644 --- a/apps/mobile/src/lib/voice-input/voice-input-draft.ts +++ b/apps/mobile/src/lib/voice-input/voice-input-draft.ts @@ -1,5 +1,7 @@ +export type VoiceInputSelection = { start: number; end: number }; + type VoiceInputTextInput = { - setNativeProps(props: { text: string }): void; + setNativeProps(props: { text: string; selection?: VoiceInputSelection }): void; }; type ApplyVoiceDraftOptions = { @@ -37,3 +39,135 @@ export function applyVoiceDraftToInput({ input?.setNativeProps({ text: next }); onChangeText(next); } + +/** + * Recovers the speech text the controller merged into `mergedDraft`. The + * listeners build the merged draft with `appendVoiceTranscript(baseDraft, + * transcript)`, which appends the transcript (leading whitespace trimmed) and + * inserts exactly one space when `baseDraft` does not already end in + * whitespace. This is the inverse of that join: it returns the trimmed + * transcript, or `''` when the merge could not be explained (the base draft + * changed under the caller). + */ +export function resolveVoiceTranscriptDelta(baseDraft: string, mergedDraft: string): string { + if (baseDraft.length === 0) { + return mergedDraft.trimStart(); + } + if (!mergedDraft.startsWith(baseDraft)) { + return ''; + } + const remainder = mergedDraft.slice(baseDraft.length); + const lastChar = baseDraft.at(-1); + const endsWithWhitespace = + lastChar === ' ' || lastChar === '\n' || lastChar === '\t' || lastChar === '\r'; + if (endsWithWhitespace) { + return remainder; + } + return remainder.startsWith(' ') ? remainder.slice(1) : remainder; +} + +function clampSelection(value: number, max: number): number { + return Number.isFinite(value) ? Math.min(Math.max(Math.floor(value), 0), max) : max; +} + +function isWhitespace(char: string | undefined): boolean { + return char === ' ' || char === '\n' || char === '\t' || char === '\r'; +} + +/** + * Splices `transcript` into `baseDraft` at the selection captured when the + * voice session started. The selected range is replaced (an empty selection + * inserts at the caret) and a single space is added on each side where the + * transcript would otherwise touch a non-whitespace character, mirroring how + * the default full-replace path joins speech. The draft never exceeds + * `maxLength` (the transcript is truncated, never the surrounding text), and + * the returned caret sits just after the inserted text. A null selection + * inserts at the draft's end. + */ +export function resolveVoiceInsertion( + baseDraft: string, + baseSelection: VoiceInputSelection | null, + transcript: string, + maxLength: number | undefined +): { draft: string; selection: VoiceInputSelection } { + const fallback = { start: baseDraft.length, end: baseDraft.length }; + const reported = baseSelection ?? fallback; + const start = clampSelection(reported.start, baseDraft.length); + const end = clampSelection(reported.end, baseDraft.length); + const insertStart = Math.min(start, end); + const insertEnd = Math.max(start, end); + const prefix = baseDraft.slice(0, insertStart); + const suffix = baseDraft.slice(insertEnd); + const leading = prefix.length > 0 && !isWhitespace(prefix.at(-1)) ? ' ' : ''; + const trailing = suffix.length > 0 && !isWhitespace(suffix.at(0)) ? ' ' : ''; + const separators = leading.length + trailing.length; + const capacity = + maxLength === undefined + ? Number.POSITIVE_INFINITY + : Math.max(0, Math.floor(maxLength) - prefix.length - suffix.length); + const room = + capacity === Number.POSITIVE_INFINITY ? capacity : Math.max(0, capacity - separators); + const inserted = room > 0 ? transcript.slice(0, room) : ''; + // An empty insert never pads the draft with orphan separators. + const attachedLeading = inserted.length > 0 ? leading : ''; + const attachedTrailing = inserted.length > 0 ? trailing : ''; + const caretOffset = prefix.length + attachedLeading.length + inserted.length; + return { + draft: prefix + attachedLeading + inserted + attachedTrailing + suffix, + selection: { start: caretOffset, end: caretOffset }, + }; +} + +export type ApplyVoiceDraftAtSelectionOptions = { + /** Draft captured when the voice session started (the controller's base). */ + baseDraft: string; + /** Caret (or replaced range) captured when the voice session started. */ + baseSelection: VoiceInputSelection | null; + /** The live text currently in the input. */ + currentDraft: string; + /** The last draft this helper produced; `baseDraft` on the first result. */ + expectedDraft: string; + /** Full merged draft the controller produced for this result. */ + mergedDraft: string; + /** True while an IME composition is active; inserts are skipped. */ + isComposing: boolean; + input: VoiceInputTextInput | null; + maxLength?: number; + onChangeText: (draft: string) => void; +}; + +export type ApplyVoiceDraftAtSelectionResult = + | { kind: 'inserted'; draft: string; selection: VoiceInputSelection } + | { kind: 'aborted' }; + +/** + * Selection-aware dictation path used by the agent composers: interim and + * final speech splice into the caret captured at session start instead of + * replacing the whole draft. If an IME composition is active, or the live + * draft no longer matches the draft this helper last produced (the user + * edited the speech range), the input is left untouched and `aborted` is + * returned so the caller can stop recognition and keep the user's text. + */ +export function applyVoiceDraftAtSelection({ + baseDraft, + baseSelection, + currentDraft, + expectedDraft, + mergedDraft, + isComposing, + input, + maxLength, + onChangeText, +}: ApplyVoiceDraftAtSelectionOptions): ApplyVoiceDraftAtSelectionResult { + if (isComposing) { + return { kind: 'aborted' }; + } + if (currentDraft !== expectedDraft) { + return { kind: 'aborted' }; + } + const transcript = resolveVoiceTranscriptDelta(baseDraft, mergedDraft); + const next = resolveVoiceInsertion(baseDraft, baseSelection, transcript, maxLength); + input?.setNativeProps({ text: next.draft, selection: next.selection }); + onChangeText(next.draft); + return { kind: 'inserted', draft: next.draft, selection: next.selection }; +} From 9552aebb04586f763082b5ac02f2a0880077b505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 19:42:32 +0200 Subject: [PATCH 2/6] refactor(mobile): take voice insertion options as one object --- .../lib/voice-input/voice-input-draft.test.ts | 28 ++++++++++++++++--- .../src/lib/voice-input/voice-input-draft.ts | 27 +++++++++++++----- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/apps/mobile/src/lib/voice-input/voice-input-draft.test.ts b/apps/mobile/src/lib/voice-input/voice-input-draft.test.ts index 70373d6c7e..2cc6262a72 100644 --- a/apps/mobile/src/lib/voice-input/voice-input-draft.test.ts +++ b/apps/mobile/src/lib/voice-input/voice-input-draft.test.ts @@ -163,28 +163,48 @@ describe('resolveVoiceTranscriptDelta', () => { describe('resolveVoiceInsertion', () => { it('inserts at the caret and lands the caret after the inserted text', () => { - const result = resolveVoiceInsertion('hello world', { start: 5, end: 5 }, 'there', undefined); + const result = resolveVoiceInsertion({ + baseDraft: 'hello world', + baseSelection: { start: 5, end: 5 }, + transcript: 'there', + maxLength: undefined, + }); expect(result.draft).toBe('hello there world'); expect(result.selection).toEqual({ start: 11, end: 11 }); }); it('replaces the selected range', () => { - const result = resolveVoiceInsertion('hello world', { start: 5, end: 11 }, 'there', undefined); + const result = resolveVoiceInsertion({ + baseDraft: 'hello world', + baseSelection: { start: 5, end: 11 }, + transcript: 'there', + maxLength: undefined, + }); expect(result.draft).toBe('hello there'); expect(result.selection).toEqual({ start: 11, end: 11 }); }); it('inserts at the draft end when no selection was reported', () => { - const result = resolveVoiceInsertion('hello', null, 'world', undefined); + const result = resolveVoiceInsertion({ + baseDraft: 'hello', + baseSelection: null, + transcript: 'world', + maxLength: undefined, + }); expect(result.draft).toBe('hello world'); expect(result.selection).toEqual({ start: 11, end: 11 }); }); it('truncates the transcript, not the surrounding text, at maxLength', () => { - const result = resolveVoiceInsertion('hello world', { start: 5, end: 5 }, 'there', 14); + const result = resolveVoiceInsertion({ + baseDraft: 'hello world', + baseSelection: { start: 5, end: 5 }, + transcript: 'there', + maxLength: 14, + }); expect(result.draft).toBe('hello th world'); expect(result.selection).toEqual({ start: 8, end: 8 }); diff --git a/apps/mobile/src/lib/voice-input/voice-input-draft.ts b/apps/mobile/src/lib/voice-input/voice-input-draft.ts index d8737f5c43..0f2bb3ad42 100644 --- a/apps/mobile/src/lib/voice-input/voice-input-draft.ts +++ b/apps/mobile/src/lib/voice-input/voice-input-draft.ts @@ -74,6 +74,19 @@ function isWhitespace(char: string | undefined): boolean { return char === ' ' || char === '\n' || char === '\t' || char === '\r'; } +/** Inputs for `resolveVoiceInsertion`. */ +export type ResolveVoiceInsertionOptions = { + baseDraft: string; + baseSelection: VoiceInputSelection | null; + transcript: string; + maxLength?: number; +}; + +export type ResolvedVoiceInsertion = { + draft: string; + selection: VoiceInputSelection; +}; + /** * Splices `transcript` into `baseDraft` at the selection captured when the * voice session started. The selected range is replaced (an empty selection @@ -84,12 +97,12 @@ function isWhitespace(char: string | undefined): boolean { * the returned caret sits just after the inserted text. A null selection * inserts at the draft's end. */ -export function resolveVoiceInsertion( - baseDraft: string, - baseSelection: VoiceInputSelection | null, - transcript: string, - maxLength: number | undefined -): { draft: string; selection: VoiceInputSelection } { +export function resolveVoiceInsertion({ + baseDraft, + baseSelection, + transcript, + maxLength, +}: ResolveVoiceInsertionOptions): ResolvedVoiceInsertion { const fallback = { start: baseDraft.length, end: baseDraft.length }; const reported = baseSelection ?? fallback; const start = clampSelection(reported.start, baseDraft.length); @@ -166,7 +179,7 @@ export function applyVoiceDraftAtSelection({ return { kind: 'aborted' }; } const transcript = resolveVoiceTranscriptDelta(baseDraft, mergedDraft); - const next = resolveVoiceInsertion(baseDraft, baseSelection, transcript, maxLength); + const next = resolveVoiceInsertion({ baseDraft, baseSelection, transcript, maxLength }); input?.setNativeProps({ text: next.draft, selection: next.selection }); onChangeText(next.draft); return { kind: 'inserted', draft: next.draft, selection: next.selection }; From 95839c30940fde0f6261c6933085e7019f49239e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 22:18:02 +0200 Subject: [PATCH 3/6] fix(mobile): gate send on failed attachments and widen mic target --- .../chat-composer-input-row.mounted.test.tsx | 12 +++++++ .../agents/chat-composer-input-row.tsx | 2 +- .../agents/chat-composer-input-state.test.ts | 32 +++++++++++++++++++ .../agents/chat-composer-input-state.ts | 9 ++++-- .../src/components/agents/chat-composer.tsx | 1 + .../src/components/voice-input-control.tsx | 7 +++- 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/components/agents/chat-composer-input-row.mounted.test.tsx b/apps/mobile/src/components/agents/chat-composer-input-row.mounted.test.tsx index faab7c2341..de2cccf1d8 100644 --- a/apps/mobile/src/components/agents/chat-composer-input-row.mounted.test.tsx +++ b/apps/mobile/src/components/agents/chat-composer-input-row.mounted.test.tsx @@ -173,4 +173,16 @@ describe('ChatComposerInputRow mounted — iOS writing-tools lock', () => { renderer.unmount(); }); + + it('renders the mic at the lg size so it reaches the 48dp Android target', async () => { + const renderer = await renderRow({ + inputEditable: true, + voiceInputAvailable: true, + }); + + const [mic] = findAllByType(renderer.root, 'VoiceInputButton'); + expect(mic?.props.size).toBe('lg'); + + renderer.unmount(); + }); }); diff --git a/apps/mobile/src/components/agents/chat-composer-input-row.tsx b/apps/mobile/src/components/agents/chat-composer-input-row.tsx index e399e79f1c..5f87a34766 100644 --- a/apps/mobile/src/components/agents/chat-composer-input-row.tsx +++ b/apps/mobile/src/components/agents/chat-composer-input-row.tsx @@ -144,7 +144,7 @@ export function ChatComposerInputRow({ diff --git a/apps/mobile/src/components/agents/chat-composer-input-state.test.ts b/apps/mobile/src/components/agents/chat-composer-input-state.test.ts index 96214e70bf..9f77a7566a 100644 --- a/apps/mobile/src/components/agents/chat-composer-input-state.test.ts +++ b/apps/mobile/src/components/agents/chat-composer-input-state.test.ts @@ -13,6 +13,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -42,6 +43,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: override.isSending, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -64,6 +66,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -85,6 +88,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -105,6 +109,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -124,6 +129,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -143,6 +149,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -162,6 +169,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -179,6 +187,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: true, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -188,6 +197,24 @@ describe('resolveChatComposerControlState', () => { expect(state.inputEditable).toBe(true); }); + it('gates send on a failed attachment chip while sendable content remains', () => { + const state = resolveChatComposerControlState({ + attachmentsCount: 1, + sendableAttachmentsCount: 1, + attachmentMax: 5, + disabled: false, + hasText: true, + isFocused: false, + isSending: false, + isUploading: false, + hasFailedAttachments: true, + voiceInputActive: false, + }); + + expect(state.canSend).toBe(false); + expect(state.hasSendableContent).toBe(true); + }); + it('keeps the toolbar visible when focused, has text, has attachments, or voice is active', () => { const base = { attachmentsCount: 0, @@ -198,6 +225,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }; @@ -222,6 +250,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -238,6 +267,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: true, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); @@ -254,6 +284,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: true, }); @@ -272,6 +303,7 @@ describe('resolveChatComposerControlState', () => { isFocused: false, isSending: false, isUploading: false, + hasFailedAttachments: false, voiceInputActive: false, }); diff --git a/apps/mobile/src/components/agents/chat-composer-input-state.ts b/apps/mobile/src/components/agents/chat-composer-input-state.ts index 1b22812188..4dc1844a9c 100644 --- a/apps/mobile/src/components/agents/chat-composer-input-state.ts +++ b/apps/mobile/src/components/agents/chat-composer-input-state.ts @@ -9,6 +9,8 @@ type ChatComposerControlInput = { isSending: boolean; /** True while an attachment upload is in flight; blocks send until it settles. */ isUploading: boolean; + /** True when at least one attachment chip is terminally failed; gates send. */ + hasFailedAttachments: boolean; voiceInputActive: boolean; }; @@ -38,7 +40,9 @@ type ChatComposerControlState = { * the composer. Voice input integrates here too: an active voice session * locks the attachment picker while speech is being recognized, but it keeps * the input editable so dictation can insert at the caret (a user edit during - * dictation aborts the session in the selection-aware draft path). + * dictation aborts the session in the selection-aware draft path). A + * terminally failed attachment chip gates send (`hasFailedAttachments`), so a + * failed upload renders Send disabled instead of toasting on press. */ export function resolveChatComposerControlState( input: ChatComposerControlInput @@ -52,6 +56,7 @@ export function resolveChatComposerControlState( isFocused, isSending, isUploading, + hasFailedAttachments, voiceInputActive, } = input; // Streaming is intentionally NOT a composer gate. The user must be able to @@ -70,7 +75,7 @@ export function resolveChatComposerControlState( const showToolbar = isFocused || hasText || attachmentsCount > 0 || voiceInputActive; const hasSendableContent = hasText || sendableAttachmentsCount > 0; return { - canSend: hasSendableContent && !disabled && !isSending && !isUploading, + canSend: hasSendableContent && !disabled && !isSending && !isUploading && !hasFailedAttachments, hasSendableContent, inputAccessibilityDisabled: !inputEditable, inputEditable, diff --git a/apps/mobile/src/components/agents/chat-composer.tsx b/apps/mobile/src/components/agents/chat-composer.tsx index 9edfc9bfa1..df77ee17ac 100644 --- a/apps/mobile/src/components/agents/chat-composer.tsx +++ b/apps/mobile/src/components/agents/chat-composer.tsx @@ -607,6 +607,7 @@ export function ChatComposer({ isFocused, isSending, isUploading: upload.isUploading, + hasFailedAttachments: upload.hasFailedAttachments, voiceInputActive: voiceInput.isActive, }); diff --git a/apps/mobile/src/components/voice-input-control.tsx b/apps/mobile/src/components/voice-input-control.tsx index b3283f618b..3ec5ff7ab5 100644 --- a/apps/mobile/src/components/voice-input-control.tsx +++ b/apps/mobile/src/components/voice-input-control.tsx @@ -8,7 +8,7 @@ import { cn } from '@/lib/utils'; import { type VoiceInputStatus } from '@/lib/voice-input/voice-input-state'; import { resolveVoiceInputControlState } from '@/lib/voice-input/voice-input-view-state'; -type VoiceInputButtonSize = 'sm' | 'md'; +type VoiceInputButtonSize = 'sm' | 'md' | 'lg'; type VoiceInputButtonProps = { disabled: boolean; @@ -19,6 +19,7 @@ type VoiceInputButtonProps = { // Visual class and hitSlop travel as a coupled pair so the effective touch // target stays >=44pt (visual size + 2 * hitSlop per side) at every size. +// `lg` reaches the 48dp Android minimum for the composer input row. const SIZE_STYLES = { sm: { className: 'h-8 w-8 rounded-full', @@ -28,6 +29,10 @@ const SIZE_STYLES = { className: 'h-9 w-9 rounded-full', hitSlop: { top: 4, bottom: 4, left: 4, right: 4 }, }, + lg: { + className: 'h-10 w-10 rounded-full', + hitSlop: { top: 4, bottom: 4, left: 4, right: 4 }, + }, } satisfies Record< VoiceInputButtonSize, { From 49dfa87d38fe8d4e1f796bcea428031cec69b451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Thu, 27 Aug 2026 22:45:12 +0200 Subject: [PATCH 4/6] fix(mobile): widen new-session mic to 48dp --- apps/mobile/src/components/agents/new-session-prompt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/components/agents/new-session-prompt.tsx b/apps/mobile/src/components/agents/new-session-prompt.tsx index aae4a1a325..8ef3925910 100644 --- a/apps/mobile/src/components/agents/new-session-prompt.tsx +++ b/apps/mobile/src/components/agents/new-session-prompt.tsx @@ -315,7 +315,7 @@ export function NewSessionPrompt({ {voiceInput.available ? ( From c2664e95c68c70da089acb97a487f91b82e37993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 28 Aug 2026 04:46:06 +0200 Subject: [PATCH 5/6] refactor(mobile): extract new-session prompt controls --- .../agents/new-session-prompt-controls.tsx | 73 ++++++++++++++++++ .../new-session-prompt-initial-prompt.test.ts | 75 +++++++++++++------ .../components/agents/new-session-prompt.tsx | 55 ++------------ 3 files changed, 132 insertions(+), 71 deletions(-) create mode 100644 apps/mobile/src/components/agents/new-session-prompt-controls.tsx diff --git a/apps/mobile/src/components/agents/new-session-prompt-controls.tsx b/apps/mobile/src/components/agents/new-session-prompt-controls.tsx new file mode 100644 index 0000000000..7a00c6aaac --- /dev/null +++ b/apps/mobile/src/components/agents/new-session-prompt-controls.tsx @@ -0,0 +1,73 @@ +import { Pressable, View } from 'react-native'; +import { useTranslation } from 'react-i18next'; + +import { ComposerPasteButton } from '@/components/agents/composer-paste-button'; +import { type resolveNewSessionPromptControlState } from '@/components/agents/new-session-prompt-state'; +import { Paperclip } from '@/components/ui/icons'; +import { VoiceInputButton, VoiceInputStatus } from '@/components/voice-input-control'; +import { useThemeColors } from '@/lib/hooks/use-theme-colors'; +import { cn } from '@/lib/utils'; +import { type useVoiceInput } from '@/lib/voice-input/use-voice-input'; + +type NewSessionPromptControlsProps = { + control: ReturnType; + voiceInput: ReturnType; + onAddAttachment: () => void; + pasteClipboard: () => void; +}; + +export function NewSessionPromptControls({ + control, + voiceInput, + onAddAttachment, + pasteClipboard, +}: Readonly) { + const colors = useThemeColors(); + const { t } = useTranslation(); + const paperclipDisabled = control.paperclipDisabled; + + function handlePaperclipPress() { + onAddAttachment(); + } + + function handleVoiceToggle() { + void voiceInput.toggle(); + } + + return ( + + + + + + {/* Follows the input, not the paperclip: a full attachment list + still allows a text paste. */} + + + {voiceInput.available ? ( + + + + ) : null} + {voiceInput.available ? ( + + ) : null} + + ); +} diff --git a/apps/mobile/src/components/agents/new-session-prompt-initial-prompt.test.ts b/apps/mobile/src/components/agents/new-session-prompt-initial-prompt.test.ts index af78d8e3c6..77b01f76fd 100644 --- a/apps/mobile/src/components/agents/new-session-prompt-initial-prompt.test.ts +++ b/apps/mobile/src/components/agents/new-session-prompt-initial-prompt.test.ts @@ -1,8 +1,10 @@ import * as React from 'react'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { type AgentMode } from '@/components/agents/mode-selector'; import { ComposerPasteButton } from '@/components/agents/composer-paste-button'; +import { NewSessionPromptControls as renderPromptControls } from '@/components/agents/new-session-prompt-controls'; +import { VoiceInputButton, VoiceInputStatus } from '@/components/voice-input-control'; import '@/i18n'; import type * as ReactI18next from 'react-i18next'; @@ -87,18 +89,7 @@ vi.mock('@/components/agents/use-text-height', () => ({ }), })); -const controlState = vi.hoisted(() => ({ inputEditable: true })); - -vi.mock('@/components/agents/new-session-prompt-state', () => ({ - resolveNewSessionPromptControlState: () => ({ - createDisabled: false, - hasPrompt: false, - inputAccessibilityDisabled: false, - inputEditable: controlState.inputEditable, - paperclipDisabled: false, - voiceDisabled: false, - }), -})); +const voiceInputAvailable = vi.hoisted(() => ({ current: false })); vi.mock('@/components/query-error', () => ({ QueryError: () => null, @@ -138,7 +129,7 @@ vi.mock('@/lib/share-prefill', () => ({ vi.mock('@/lib/voice-input/use-voice-input', () => ({ useVoiceInput: () => ({ abort: vi.fn(), - available: false, + available: voiceInputAvailable.current, isActive: false, settleBeforeSubmit: vi.fn(), status: 'idle' as const, @@ -164,6 +155,12 @@ function findElementByType(node: Node, target: ElementType): Record), + target + ); + } for (const child of Array.isArray(children) ? children : [children]) { const found = findElementByType(child as Node, target); if (found) { @@ -202,6 +199,10 @@ function defaultProps() { } describe('NewSessionPrompt initialPrompt seed', () => { + beforeEach(() => { + voiceInputAvailable.current = false; + }); + it('does not invoke onChangeText when initialPrompt seeds the uncontrolled input on mount', async () => { const { NewSessionPrompt } = await import('./new-session-prompt'); @@ -288,15 +289,10 @@ describe('NewSessionPrompt initialPrompt seed', () => { const { NewSessionPrompt } = await import('./new-session-prompt'); onChangeTextMock.mockClear(); - controlState.inputEditable = false; - try { - // eslint-disable-next-line new-cap -- plain function call, matching repo test convention - NewSessionPrompt({ ...defaultProps(), initialPrompt: 'fix the bug' }); - - clipboardHintOptions.current?.addText?.('really '); - } finally { - controlState.inputEditable = true; - } + // eslint-disable-next-line new-cap -- plain function call, matching repo test convention + NewSessionPrompt({ ...defaultProps(), initialPrompt: 'fix the bug', isCreating: true }); + + clipboardHintOptions.current?.addText?.('really '); expect(onChangeTextMock).not.toHaveBeenCalled(); }); @@ -313,4 +309,37 @@ describe('NewSessionPrompt initialPrompt seed', () => { expect(props.onPress).toBe(pasteClipboardImageMock); expect(props.disabled).toBe(false); }); + + it.each([ + { isCreating: false, attachmentMax: 5, voiceAvailable: false, paperclipDisabled: false }, + { isCreating: false, attachmentMax: 0, voiceAvailable: true, paperclipDisabled: true }, + { isCreating: true, attachmentMax: 5, voiceAvailable: true, paperclipDisabled: true }, + ])('preserves control availability and disabled states for %j', async options => { + const { NewSessionPrompt: renderPrompt } = await import('./new-session-prompt'); + const { isCreating, attachmentMax, voiceAvailable, paperclipDisabled } = options; + voiceInputAvailable.current = voiceAvailable; + const element = renderPrompt({ ...defaultProps(), isCreating, attachmentMax }); + + expect(findElementByType(element, 'Pressable')).toMatchObject({ + disabled: paperclipDisabled, + accessibilityState: { disabled: paperclipDisabled }, + hitSlop: { top: 8, bottom: 8, left: 8, right: 8 }, + }); + expect(findElementByType(element, ComposerPasteButton)).toMatchObject({ disabled: isCreating }); + expect(findElementByType(element, VoiceInputButton)).toEqual( + voiceAvailable ? expect.objectContaining({ disabled: isCreating, size: 'lg' }) : null + ); + expect(findElementByType(element, VoiceInputStatus)).toEqual( + voiceAvailable ? { status: 'idle' } : null + ); + }); + + it('keeps the extracted controls unmounted for clone entry', async () => { + const { NewSessionPrompt: renderPrompt } = await import('./new-session-prompt'); + voiceInputAvailable.current = true; + const element = renderPrompt({ ...defaultProps(), isCloneEntry: true }); + + expect(findElementByType(element, 'TextInput')).toBeNull(); + expect(findElementByType(element, renderPromptControls)).toBeNull(); + }); }); diff --git a/apps/mobile/src/components/agents/new-session-prompt.tsx b/apps/mobile/src/components/agents/new-session-prompt.tsx index 8405238c96..275f4e7daa 100644 --- a/apps/mobile/src/components/agents/new-session-prompt.tsx +++ b/apps/mobile/src/components/agents/new-session-prompt.tsx @@ -4,19 +4,16 @@ import { AccessibilityInfo, type LayoutChangeEvent, Platform, - Pressable, TextInput as RNTextInput, type TextInput, type TextInputSelectionChangeEvent, type TextStyle, View, } from 'react-native'; -import { Paperclip } from '@/components/ui/icons'; import { toast } from 'sonner-native'; import { useTranslation } from 'react-i18next'; import { AttachmentPreviewStrip } from '@/components/agents/attachment-preview-strip'; -import { ComposerPasteButton } from '@/components/agents/composer-paste-button'; import { type ComposerSelection, pasteTextIntoComposer, @@ -25,6 +22,7 @@ import { ChatToolbar } from '@/components/agents/chat-toolbar'; import { useTextHeight } from '@/components/agents/use-text-height'; import { resolveNewSessionPromptControlState } from '@/components/agents/new-session-prompt-state'; import { NewSessionPromptClone } from '@/components/agents/new-session-prompt-clone'; +import { NewSessionPromptControls } from '@/components/agents/new-session-prompt-controls'; import { type NewSessionPromptProps } from '@/components/agents/new-session-prompt-types'; import { QueryError } from '@/components/query-error'; import { useThemeColors } from '@/lib/hooks/use-theme-colors'; @@ -35,7 +33,6 @@ import { type VoiceInputSelection, } from '@/lib/voice-input/voice-input-draft'; import { useVoiceInput } from '@/lib/voice-input/use-voice-input'; -import { VoiceInputButton, VoiceInputStatus } from '@/components/voice-input-control'; import { describeClassificationFailure } from '@/lib/agent-attachments/validate'; import { AGENT_ATTACHMENT_MAX_BYTES } from '@/lib/agent-attachments/constants'; import { @@ -203,8 +200,6 @@ export function NewSessionPrompt({ voiceInputActive: voiceInput.isActive, }); - const paperclipDisabled = control.paperclipDisabled; - const { paste: pasteClipboard } = useClipboardPaste({ addFile: async file => { await onPrefillAttachments([file]); @@ -242,14 +237,6 @@ export function NewSessionPrompt({ promptSelectionRef.current = event.nativeEvent.selection; } - function handlePaperclipPress() { - onAddAttachment(); - } - - function handleVoiceToggle() { - void voiceInput.toggle(); - } - // Clone entry has no composer: render the models error or the toolbar as a // standalone block, never an empty rounded composer card. The input, the // attachment strip, the paperclip, the paste button, and voice are unmounted. @@ -312,40 +299,12 @@ export function NewSessionPrompt({ // arrival hides the attachment strip and the Start button. autoFocus={shareId === undefined || shareId === ''} /> - - - - - - {/* Follows the input, not the paperclip: a full attachment list - still allows a text paste. */} - - - {voiceInput.available ? ( - - - - ) : null} - {voiceInput.available ? ( - - ) : null} - + {isModelsError && modelOptions.length === 0 ? ( Date: Fri, 28 Aug 2026 07:43:11 +0200 Subject: [PATCH 6/6] feat(i18n): translate dictation stop status --- apps/mobile/src/i18n/locales/af.json | 3 ++- apps/mobile/src/i18n/locales/am.json | 3 ++- apps/mobile/src/i18n/locales/ar.json | 3 ++- apps/mobile/src/i18n/locales/az.json | 3 ++- apps/mobile/src/i18n/locales/be.json | 3 ++- apps/mobile/src/i18n/locales/bg.json | 3 ++- apps/mobile/src/i18n/locales/bn.json | 3 ++- apps/mobile/src/i18n/locales/bs.json | 3 ++- apps/mobile/src/i18n/locales/ca.json | 3 ++- apps/mobile/src/i18n/locales/ckb.json | 3 ++- apps/mobile/src/i18n/locales/cs.json | 3 ++- apps/mobile/src/i18n/locales/cy.json | 3 ++- apps/mobile/src/i18n/locales/da.json | 3 ++- apps/mobile/src/i18n/locales/de.json | 3 ++- apps/mobile/src/i18n/locales/el.json | 3 ++- apps/mobile/src/i18n/locales/es.json | 3 ++- apps/mobile/src/i18n/locales/et.json | 3 ++- apps/mobile/src/i18n/locales/eu.json | 3 ++- apps/mobile/src/i18n/locales/fa.json | 3 ++- apps/mobile/src/i18n/locales/fi.json | 3 ++- apps/mobile/src/i18n/locales/fil.json | 3 ++- apps/mobile/src/i18n/locales/fr.json | 3 ++- apps/mobile/src/i18n/locales/ga.json | 3 ++- apps/mobile/src/i18n/locales/gl.json | 3 ++- apps/mobile/src/i18n/locales/gu.json | 3 ++- apps/mobile/src/i18n/locales/ha.json | 3 ++- apps/mobile/src/i18n/locales/he.json | 3 ++- apps/mobile/src/i18n/locales/hi.json | 3 ++- apps/mobile/src/i18n/locales/hr.json | 3 ++- apps/mobile/src/i18n/locales/ht.json | 3 ++- apps/mobile/src/i18n/locales/hu.json | 3 ++- apps/mobile/src/i18n/locales/hy.json | 3 ++- apps/mobile/src/i18n/locales/id.json | 3 ++- apps/mobile/src/i18n/locales/ig.json | 3 ++- apps/mobile/src/i18n/locales/is.json | 3 ++- apps/mobile/src/i18n/locales/it.json | 3 ++- apps/mobile/src/i18n/locales/ja.json | 3 ++- apps/mobile/src/i18n/locales/ka.json | 3 ++- apps/mobile/src/i18n/locales/kk.json | 3 ++- apps/mobile/src/i18n/locales/km.json | 3 ++- apps/mobile/src/i18n/locales/kn.json | 3 ++- apps/mobile/src/i18n/locales/ko.json | 3 ++- apps/mobile/src/i18n/locales/lo.json | 3 ++- apps/mobile/src/i18n/locales/lt.json | 3 ++- apps/mobile/src/i18n/locales/lv.json | 3 ++- apps/mobile/src/i18n/locales/mg.json | 3 ++- apps/mobile/src/i18n/locales/mi.json | 3 ++- apps/mobile/src/i18n/locales/mk.json | 3 ++- apps/mobile/src/i18n/locales/ml.json | 3 ++- apps/mobile/src/i18n/locales/mn.json | 3 ++- apps/mobile/src/i18n/locales/mr.json | 3 ++- apps/mobile/src/i18n/locales/ms.json | 3 ++- apps/mobile/src/i18n/locales/mt.json | 3 ++- apps/mobile/src/i18n/locales/my.json | 3 ++- apps/mobile/src/i18n/locales/nb.json | 3 ++- apps/mobile/src/i18n/locales/ne.json | 3 ++- apps/mobile/src/i18n/locales/nl.json | 3 ++- apps/mobile/src/i18n/locales/om.json | 3 ++- apps/mobile/src/i18n/locales/or.json | 3 ++- apps/mobile/src/i18n/locales/pa.json | 3 ++- apps/mobile/src/i18n/locales/pl.json | 3 ++- apps/mobile/src/i18n/locales/ps.json | 3 ++- apps/mobile/src/i18n/locales/pt-BR.json | 3 ++- apps/mobile/src/i18n/locales/pt.json | 3 ++- apps/mobile/src/i18n/locales/ro.json | 3 ++- apps/mobile/src/i18n/locales/ru.json | 3 ++- apps/mobile/src/i18n/locales/si.json | 3 ++- apps/mobile/src/i18n/locales/sk.json | 3 ++- apps/mobile/src/i18n/locales/sl.json | 3 ++- apps/mobile/src/i18n/locales/so.json | 3 ++- apps/mobile/src/i18n/locales/sq.json | 3 ++- apps/mobile/src/i18n/locales/sr.json | 3 ++- apps/mobile/src/i18n/locales/sv.json | 3 ++- apps/mobile/src/i18n/locales/sw.json | 3 ++- apps/mobile/src/i18n/locales/ta.json | 3 ++- apps/mobile/src/i18n/locales/te.json | 3 ++- apps/mobile/src/i18n/locales/th.json | 3 ++- apps/mobile/src/i18n/locales/tr.json | 3 ++- apps/mobile/src/i18n/locales/uk.json | 3 ++- apps/mobile/src/i18n/locales/ur.json | 3 ++- apps/mobile/src/i18n/locales/uz.json | 3 ++- apps/mobile/src/i18n/locales/vi.json | 3 ++- apps/mobile/src/i18n/locales/yo.json | 3 ++- apps/mobile/src/i18n/locales/zh-Hans.json | 3 ++- apps/mobile/src/i18n/locales/zh-Hant.json | 3 ++- apps/mobile/src/i18n/locales/zu.json | 3 ++- 86 files changed, 172 insertions(+), 86 deletions(-) diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 2c7a7d2b68..30dc538a8c 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -2808,7 +2808,8 @@ "busy": "Steminvoer is besig. Probeer weer.", "stopped": "Steminvoer het gestop. Tik die mikrofoon om weer te probeer.", "unavailableDevice": "Steminvoer is nie op hierdie toestel beskikbaar nie.", - "unavailableLanguage": "Steminvoer is nie vir hierdie toesteltaal beskikbaar nie." + "unavailableLanguage": "Steminvoer is nie vir hierdie toesteltaal beskikbaar nie.", + "listeningStopped": "Luister het gestop." }, "share": { "title": "Deel na Kilo", diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 2ff795f0b3..6d271016b0 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -2808,7 +2808,8 @@ "busy": "የድምፅ ግብዓት ስራ ላይ ነው። እንደገና ይሞክሩ።", "stopped": "የድምፅ ግብዓት ቆሟል። እንደገና ለመሞከር ማይክሮፎኑን ይንኩ።", "unavailableDevice": "የድምፅ ግብዓት በዚህ መሣሪያ ላይ አይገኝም።", - "unavailableLanguage": "የድምፅ ግብዓት ለዚህ የመሣሪያ ቋንቋ አይገኝም።" + "unavailableLanguage": "የድምፅ ግብዓት ለዚህ የመሣሪያ ቋንቋ አይገኝም።", + "listeningStopped": "ማዳመጥ ቆሟል።" }, "share": { "title": "ወደ Kilo ያጋሩ", diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index 4ca67b7d25..80bdc7e17b 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -2406,7 +2406,8 @@ "busy": "الإدخال الصوتي مشغول. حاول مرة أخرى.", "stopped": "توقف الإدخال الصوتي. اضغط على الميكروفون للمحاولة مرة أخرى.", "unavailableDevice": "الإدخال الصوتي غير متاح على هذا الجهاز.", - "unavailableLanguage": "الإدخال الصوتي غير متاح للغة الجهاز هذه." + "unavailableLanguage": "الإدخال الصوتي غير متاح للغة الجهاز هذه.", + "listeningStopped": "توقف الاستماع." }, "share": { "title": "مشاركة إلى Kilo", diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 145fb7633b..ab8a2dbbe6 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -2808,7 +2808,8 @@ "busy": "Səs girişi məşğuldur. Yenidən cəhd edin.", "stopped": "Səs girişi dayandı. Yenidən cəhd etmək üçün mikrofona toxunun.", "unavailableDevice": "Səs girişi bu cihazda mövcud deyil.", - "unavailableLanguage": "Səs girişi bu cihaz dili üçün mövcud deyil." + "unavailableLanguage": "Səs girişi bu cihaz dili üçün mövcud deyil.", + "listeningStopped": "Dinləmə dayandı." }, "share": { "title": "Kilo-ya paylaş", diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 48b573d09f..ca56905734 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -2848,7 +2848,8 @@ "busy": "Галасавы ўвод заняты. Паспрабуйце яшчэ раз.", "stopped": "Галасавы ўвод спынены. Дакраніцеся да мікрафона, каб паспрабаваць зноў.", "unavailableDevice": "Галасавы ўвод недаступны на гэтай прыладзе.", - "unavailableLanguage": "Галасавы ўвод недаступны для мовы гэтай прылады." + "unavailableLanguage": "Галасавы ўвод недаступны для мовы гэтай прылады.", + "listeningStopped": "Праслухоўванне спынена." }, "share": { "title": "Падзяліцца з Kilo", diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index 6d593c1e19..1a408c359a 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -2808,7 +2808,8 @@ "busy": "Гласовото въвеждане е заето. Опитайте отново.", "stopped": "Гласовото въвеждане е спряно. Докоснете микрофона, за да опитате отново.", "unavailableDevice": "Гласовото въвеждане не е налично на това устройство.", - "unavailableLanguage": "Гласовото въвеждане не е налично за езика на това устройство." + "unavailableLanguage": "Гласовото въвеждане не е налично за езика на това устройство.", + "listeningStopped": "Слушането е спряно." }, "share": { "title": "Споделете в Kilo", diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index 2a7373f6f5..90df9e74f2 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -2808,7 +2808,8 @@ "busy": "ভয়েস ইনপুট ব্যস্ত। আবার চেষ্টা করুন।", "stopped": "ভয়েস ইনপুট বন্ধ হয়েছে। আবার চেষ্টা করতে মাইক্রোফোনে আলতো চাপুন।", "unavailableDevice": "এই ডিভাইসে ভয়েস ইনপুট উপলব্ধ নয়।", - "unavailableLanguage": "এই ডিভাইসের ভাষার জন্য ভয়েস ইনপুট উপলব্ধ নয়।" + "unavailableLanguage": "এই ডিভাইসের ভাষার জন্য ভয়েস ইনপুট উপলব্ধ নয়।", + "listeningStopped": "শোনা বন্ধ হয়েছে।" }, "share": { "title": "Kilo-তে শেয়ার করুন", diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index 68a2404d81..4436b4da6e 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -2828,7 +2828,8 @@ "busy": "Glasovni unos je zauzet. Pokušajte ponovo.", "stopped": "Glasovni unos je zaustavljen. Dodirnite mikrofon za ponovni pokušaj.", "unavailableDevice": "Glasovni unos nije dostupan na ovom uređaju.", - "unavailableLanguage": "Glasovni unos nije dostupan za jezik ovog uređaja." + "unavailableLanguage": "Glasovni unos nije dostupan za jezik ovog uređaja.", + "listeningStopped": "Slušanje je zaustavljeno." }, "share": { "title": "Podijeli u Kilo", diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 0bf2937402..bfc623adff 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -2828,7 +2828,8 @@ "busy": "L'entrada de veu està ocupada. Torna-ho a provar.", "stopped": "L'entrada de veu s'ha aturat. Toca el micròfon per tornar-ho a provar.", "unavailableDevice": "L'entrada de veu no està disponible en aquest dispositiu.", - "unavailableLanguage": "L'entrada de veu no està disponible per a l'idioma d'aquest dispositiu." + "unavailableLanguage": "L'entrada de veu no està disponible per a l'idioma d'aquest dispositiu.", + "listeningStopped": "S'ha aturat l'escolta." }, "share": { "title": "Comparteix a Kilo", diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index 8cf18bf7da..c7fc036660 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -2808,7 +2808,8 @@ "busy": "دەنگی تێخستن سەرقاڵە. دووبارە هەوڵبدەرەوە.", "stopped": "دەنگی تێخستن وەستا. بۆ دووبارە هەوڵدانەوە لە مایکرۆفۆنەکە بدە.", "unavailableDevice": "دەنگی تێخستن لەسەر ئەم ئامێرە بەردەست نییە.", - "unavailableLanguage": "دەنگی تێخستن بۆ زمانی ئەم ئامێرە بەردەست نییە." + "unavailableLanguage": "دەنگی تێخستن بۆ زمانی ئەم ئامێرە بەردەست نییە.", + "listeningStopped": "گوێگرتن وەستا." }, "share": { "title": "هاوبەشکردن بۆ Kilo", diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 3672a327be..aec854112e 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -2848,7 +2848,8 @@ "busy": "Hlasový vstup je zaneprázdněn. Zkuste to znovu.", "stopped": "Hlasový vstup se zastavil. Klepnutím na mikrofon to zkusíte znovu.", "unavailableDevice": "Hlasový vstup není na tomto zařízení k dispozici.", - "unavailableLanguage": "Hlasový vstup není k dispozici pro jazyk tohoto zařízení." + "unavailableLanguage": "Hlasový vstup není k dispozici pro jazyk tohoto zařízení.", + "listeningStopped": "Poslech zastaven." }, "share": { "title": "Sdílet do Kilo", diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index f9ac3b8652..d08fe76772 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -2888,7 +2888,8 @@ "busy": "Mae mewnbwn llais yn brysur. Rhowch gynnig arall arni.", "stopped": "Stopiodd mewnbwn llais. Tapiwch y meic i roi cynnig arall arni.", "unavailableDevice": "Nid yw mewnbwn llais ar gael ar y ddyfais hon.", - "unavailableLanguage": "Nid yw mewnbwn llais ar gael ar gyfer iaith y ddyfais hon." + "unavailableLanguage": "Nid yw mewnbwn llais ar gael ar gyfer iaith y ddyfais hon.", + "listeningStopped": "Stopiodd y gwrando." }, "share": { "title": "Rhannu i Kilo", diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index e4010d0b43..e0e2d62376 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -2808,7 +2808,8 @@ "busy": "Taleinput er optaget. Prøv igen.", "stopped": "Taleinput stoppede. Tryk på mikrofonen for at prøve igen.", "unavailableDevice": "Taleinput er ikke tilgængeligt på denne enhed.", - "unavailableLanguage": "Taleinput er ikke tilgængeligt for dette enhedssprog." + "unavailableLanguage": "Taleinput er ikke tilgængeligt for dette enhedssprog.", + "listeningStopped": "Lytning stoppet." }, "share": { "title": "Del til Kilo", diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index 5e9a1317f1..5db681c473 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -2354,7 +2354,8 @@ "busy": "Spracheingabe ist beschäftigt. Versuche es erneut.", "stopped": "Die Spracheingabe wurde gestoppt. Tippe auf das Mikrofon, um es erneut zu versuchen.", "unavailableDevice": "Spracheingabe ist auf diesem Gerät nicht verfügbar.", - "unavailableLanguage": "Spracheingabe ist für diese Gerätesprache nicht verfügbar." + "unavailableLanguage": "Spracheingabe ist für diese Gerätesprache nicht verfügbar.", + "listeningStopped": "Zuhören beendet." }, "share": { "title": "An Kilo teilen", diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index ec5955f22f..c5bb5def25 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -2808,7 +2808,8 @@ "busy": "Η φωνητική είσοδος είναι απασχολημένη. Δοκιμάστε ξανά.", "stopped": "Η φωνητική είσοδος σταμάτησε. Πατήστε το μικρόφωνο για να δοκιμάσετε ξανά.", "unavailableDevice": "Η φωνητική είσοδος δεν είναι διαθέσιμη σε αυτή τη συσκευή.", - "unavailableLanguage": "Η φωνητική είσοδος δεν είναι διαθέσιμη για τη γλώσσα αυτής της συσκευής." + "unavailableLanguage": "Η φωνητική είσοδος δεν είναι διαθέσιμη για τη γλώσσα αυτής της συσκευής.", + "listeningStopped": "Η ακρόαση σταμάτησε." }, "share": { "title": "Κοινή χρήση στο Kilo", diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 2d7fb24401..4380f94e50 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -2367,7 +2367,8 @@ "busy": "La entrada de voz está ocupada. Inténtalo de nuevo.", "stopped": "La entrada de voz se detuvo. Toca el micrófono para intentarlo de nuevo.", "unavailableDevice": "La entrada de voz no está disponible en este dispositivo.", - "unavailableLanguage": "La entrada de voz no está disponible para el idioma de este dispositivo." + "unavailableLanguage": "La entrada de voz no está disponible para el idioma de este dispositivo.", + "listeningStopped": "Escucha detenida." }, "share": { "title": "Compartir en Kilo", diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index f9b5e22bb5..572770f34e 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -2808,7 +2808,8 @@ "busy": "Häälsisestus on hõivatud. Proovi uuesti.", "stopped": "Häälsisestus peatus. Uuesti proovimiseks puuduta mikrofoni.", "unavailableDevice": "Häälsisestus pole sellel seadmel saadaval.", - "unavailableLanguage": "Häälsisestus pole selle seadme keele puhul saadaval." + "unavailableLanguage": "Häälsisestus pole selle seadme keele puhul saadaval.", + "listeningStopped": "Kuulamine peatus." }, "share": { "title": "Jaga Kilosse", diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index fb6cf2e308..ecbcbf7788 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -2808,7 +2808,8 @@ "busy": "Ahots-sarrera lanpetuta dago. Saiatu berriro.", "stopped": "Ahots-sarrera gelditu da. Sakatu mikrofonoa berriro saiatzeko.", "unavailableDevice": "Ahots-sarrera ez dago eskuragarri gailu honetan.", - "unavailableLanguage": "Ahots-sarrera ez dago eskuragarri gailu honen hizkuntzarako." + "unavailableLanguage": "Ahots-sarrera ez dago eskuragarri gailu honen hizkuntzarako.", + "listeningStopped": "Entzutea gelditu da." }, "share": { "title": "Partekatu Kilo-n", diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 348a7aa08b..c4d17830eb 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -2808,7 +2808,8 @@ "busy": "ورودی صوتی مشغول است. دوباره تلاش کنید.", "stopped": "ورودی صوتی متوقف شد. برای تلاش مجدد روی میکروفون ضربه بزنید.", "unavailableDevice": "ورودی صوتی در این دستگاه در دسترس نیست.", - "unavailableLanguage": "ورودی صوتی برای زبان این دستگاه در دسترس نیست." + "unavailableLanguage": "ورودی صوتی برای زبان این دستگاه در دسترس نیست.", + "listeningStopped": "گوش دادن متوقف شد." }, "share": { "title": "اشتراک در Kilo", diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index bb35c40b92..caed2dee6e 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -2808,7 +2808,8 @@ "busy": "Puheentunnistus on varattu. Yritä uudelleen.", "stopped": "Puheentunnistus pysähtyi. Napauta mikrofonia yrittääksesi uudelleen.", "unavailableDevice": "Puheentunnistus ei ole käytettävissä tässä laitteessa.", - "unavailableLanguage": "Puheentunnistus ei ole käytettävissä tälle laitekielelle." + "unavailableLanguage": "Puheentunnistus ei ole käytettävissä tälle laitekielelle.", + "listeningStopped": "Kuuntelu pysähtyi." }, "share": { "title": "Jaa Kiloon", diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index 3c09ac0e5b..c95c78289b 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -2808,7 +2808,8 @@ "busy": "Abala ang voice input. Subukan muli.", "stopped": "Huminto ang voice input. Tap ang mikropono para subukan muli.", "unavailableDevice": "Hindi available ang voice input sa device na ito.", - "unavailableLanguage": "Hindi available ang voice input para sa wikang ito ng device." + "unavailableLanguage": "Hindi available ang voice input para sa wikang ito ng device.", + "listeningStopped": "Huminto ang pakikinig." }, "share": { "title": "I-share sa Kilo", diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index a7e7b83b77..12d249cc47 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -2295,7 +2295,8 @@ "busy": "La saisie vocale est occupée. Réessayez.", "stopped": "La saisie vocale s'est arrêtée. Touchez le microphone pour réessayer.", "unavailableDevice": "La saisie vocale n'est pas disponible sur cet appareil.", - "unavailableLanguage": "La saisie vocale n'est pas disponible pour la langue de cet appareil." + "unavailableLanguage": "La saisie vocale n'est pas disponible pour la langue de cet appareil.", + "listeningStopped": "Écoute arrêtée." }, "share": { "title": "Partager vers Kilo", diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index f774864607..4f8fc3df82 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -2868,7 +2868,8 @@ "busy": "Tá an t-ionchur gutha gnóthach. Bain triail as arís.", "stopped": "Stop an t-ionchur gutha. Tapáil an micreafón chun triail arís a bhaint.", "unavailableDevice": "Níl ionchur gutha ar fáil ar an ngléas seo.", - "unavailableLanguage": "Níl ionchur gutha ar fáil don teanga gléis seo." + "unavailableLanguage": "Níl ionchur gutha ar fáil don teanga gléis seo.", + "listeningStopped": "Stop an éisteacht." }, "share": { "title": "Roinn le Kilo", diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index 3a7237dbc7..ef460146ac 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -2808,7 +2808,8 @@ "busy": "A entrada de voz está ocupada. Proba de novo.", "stopped": "A entrada de voz detívose. Toca o micrófono para volver tentalo.", "unavailableDevice": "A entrada de voz non está dispoñible neste dispositivo.", - "unavailableLanguage": "A entrada de voz non está dispoñible para o idioma deste dispositivo." + "unavailableLanguage": "A entrada de voz non está dispoñible para o idioma deste dispositivo.", + "listeningStopped": "Detívose a escoita." }, "share": { "title": "Compartir en Kilo", diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index 25e6c1e25c..4657d1c4fa 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -2808,7 +2808,8 @@ "busy": "વૉઇસ ઇનપુટ વ્યસ્ત છે. ફરી પ્રયત્ન કરો.", "stopped": "વૉઇસ ઇનપુટ બંધ થયું. ફરી પ્રયત્ન કરવા માઇક્રોફોન ટેપ કરો.", "unavailableDevice": "આ ઉપકરણ પર વૉઇસ ઇનપુટ ઉપલબ્ધ નથી.", - "unavailableLanguage": "આ ઉપકરણની ભાષા માટે વૉઇસ ઇનપુટ ઉપલબ્ધ નથી." + "unavailableLanguage": "આ ઉપકરણની ભાષા માટે વૉઇસ ઇનપુટ ઉપલબ્ધ નથી.", + "listeningStopped": "સાંભળવાનું બંધ થયું." }, "share": { "title": "Kilo પર શેર કરો", diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 95e3c1d3bf..1170bd7569 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -2808,7 +2808,8 @@ "busy": "Shigar da murya tana aiki. Sake gwadawa.", "stopped": "Shigar da murya ta tsaya. Danna makirufo don sake gwadawa.", "unavailableDevice": "Shigar da murya ba ta samuwa akan wannan na'ura.", - "unavailableLanguage": "Shigar da murya ba ta samuwa don harshen wannan na'ura." + "unavailableLanguage": "Shigar da murya ba ta samuwa don harshen wannan na'ura.", + "listeningStopped": "An dakatar da sauraro." }, "share": { "title": "Raba zuwa Kilo", diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index 57f6c7ef74..00b44f36d4 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -2367,7 +2367,8 @@ "busy": "קלט קולי עסוק. נסה שוב.", "stopped": "קלט קולי הופסק. הקש על המיקרופון כדי לנסות שוב.", "unavailableDevice": "קלט קולי אינו זמין במכשיר זה.", - "unavailableLanguage": "קלט קולי אינו זמין עבור שפת המכשיר הזו." + "unavailableLanguage": "קלט קולי אינו זמין עבור שפת המכשיר הזו.", + "listeningStopped": "ההאזנה הופסקה." }, "share": { "title": "שתף ל-Kilo", diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index 9c7f0b1e51..2815e52feb 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -2354,7 +2354,8 @@ "busy": "वॉइस इनपुट व्यस्त है। फिर से प्रयास करें।", "stopped": "वॉइस इनपुट रुक गया। फिर से प्रयास करने के लिए माइक्रोफ़ोन पर टैप करें।", "unavailableDevice": "इस डिवाइस पर वॉइस इनपुट उपलब्ध नहीं है।", - "unavailableLanguage": "इस डिवाइस की भाषा के लिए वॉइस इनपुट उपलब्ध नहीं है।" + "unavailableLanguage": "इस डिवाइस की भाषा के लिए वॉइस इनपुट उपलब्ध नहीं है।", + "listeningStopped": "सुनना बंद हो गया।" }, "share": { "title": "Kilo पर साझा करें", diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index a8f0e47d0d..ae50ddba29 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -2828,7 +2828,8 @@ "busy": "Glasovni unos je zauzet. Pokušajte ponovno.", "stopped": "Glasovni unos je zaustavljen. Dodirnite mikrofon za ponovni pokušaj.", "unavailableDevice": "Glasovni unos nije dostupan na ovom uređaju.", - "unavailableLanguage": "Glasovni unos nije dostupan za jezik ovog uređaja." + "unavailableLanguage": "Glasovni unos nije dostupan za jezik ovog uređaja.", + "listeningStopped": "Slušanje je zaustavljeno." }, "share": { "title": "Podijeli u Kilo", diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index eed3cfb71f..2829259b05 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -2808,7 +2808,8 @@ "busy": "Antre vwa okipe. Eseye ankò.", "stopped": "Antre vwa sispann. Tape mikwofòn nan pou eseye ankò.", "unavailableDevice": "Antre vwa pa disponib sou aparèy sa a.", - "unavailableLanguage": "Antre vwa pa disponib pou lang aparèy sa a." + "unavailableLanguage": "Antre vwa pa disponib pou lang aparèy sa a.", + "listeningStopped": "Koute a sispann." }, "share": { "title": "Pataje nan Kilo", diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index c8f4ebf93d..bddea81371 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -2808,7 +2808,8 @@ "busy": "A hangbevitel foglalt. Próbálja újra.", "stopped": "A hangbevitel leállt. A mikrofon megérintésével próbálja újra.", "unavailableDevice": "A hangbevitel nem érhető el ezen az eszközön.", - "unavailableLanguage": "A hangbevitel nem érhető el az eszköz nyelvéhez." + "unavailableLanguage": "A hangbevitel nem érhető el az eszköz nyelvéhez.", + "listeningStopped": "A figyelés leállt." }, "share": { "title": "Megosztás a Kilo-val", diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index 848c7aa6f2..050688736a 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -2808,7 +2808,8 @@ "busy": "Ձայնային մուտքագրումը զբաղված է: Կրկին փորձեք:", "stopped": "Ձայնային մուտքագրումը դադարեցված է: Հպեք խոսափողին՝ կրկին փորձելու համար:", "unavailableDevice": "Ձայնային մուտքագրումը հասանելի չէ այս սարքում:", - "unavailableLanguage": "Ձայնային մուտքագրումը հասանելի չէ այս սարքի լեզվի համար:" + "unavailableLanguage": "Ձայնային մուտքագրումը հասանելի չէ այս սարքի լեզվի համար:", + "listeningStopped": "Լսումը դադարեցված է։" }, "share": { "title": "Տարածել Kilo-ում", diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index f99243080e..9af271c2d5 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -2354,7 +2354,8 @@ "busy": "Masukan suara sedang sibuk. Coba lagi.", "stopped": "Masukan suara berhenti. Ketuk mikrofon untuk mencoba lagi.", "unavailableDevice": "Masukan suara tidak tersedia di perangkat ini.", - "unavailableLanguage": "Masukan suara tidak tersedia untuk bahasa perangkat ini." + "unavailableLanguage": "Masukan suara tidak tersedia untuk bahasa perangkat ini.", + "listeningStopped": "Mendengarkan berhenti." }, "share": { "title": "Bagikan ke Kilo", diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index 4ba0221280..d4ec33271c 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -2808,7 +2808,8 @@ "busy": "Ntinye olu na-arụ ọrụ. Nwaa ọzọ.", "stopped": "Ntinye olu kwụsịrị. Pịa igwe okwu ka ị nwaa ọzọ.", "unavailableDevice": "Ntinye olu adịghị na ngwaọrụ a.", - "unavailableLanguage": "Ntinye olu adịghị maka asụsụ ngwaọrụ a." + "unavailableLanguage": "Ntinye olu adịghị maka asụsụ ngwaọrụ a.", + "listeningStopped": "Ịge ntị kwụsịrị." }, "share": { "title": "Kekọrịta na Kilo", diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index 10af4293bf..e4ccfa975f 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -2808,7 +2808,8 @@ "busy": "Raddinnsláttur er upptekinn. Reyndu aftur.", "stopped": "Raddinnsláttur stöðvaðist. Bankaðu á hljóðnemann til að reyna aftur.", "unavailableDevice": "Raddinnsláttur er ekki í boði á þessu tæki.", - "unavailableLanguage": "Raddinnsláttur er ekki í boði fyrir tækjatungumálið." + "unavailableLanguage": "Raddinnsláttur er ekki í boði fyrir tækjatungumálið.", + "listeningStopped": "Hlustun stöðvaðist." }, "share": { "title": "Deila í Kilo", diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 1cef234a61..69fda127dd 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -2367,7 +2367,8 @@ "busy": "L'input vocale è occupato. Riprova.", "stopped": "L'input vocale si è interrotto. Tocca il microfono per riprovare.", "unavailableDevice": "L'input vocale non è disponibile su questo dispositivo.", - "unavailableLanguage": "L'input vocale non è disponibile per la lingua del dispositivo." + "unavailableLanguage": "L'input vocale non è disponibile per la lingua del dispositivo.", + "listeningStopped": "Ascolto interrotto." }, "share": { "title": "Condividi su Kilo", diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 7dfb34821d..846a07490c 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -2354,7 +2354,8 @@ "busy": "音声入力は処理中です。もう一度お試しください。", "stopped": "音声入力が停止しました。マイクをタップして、もう一度お試しください。", "unavailableDevice": "このデバイスでは音声入力は利用できません。", - "unavailableLanguage": "このデバイスの言語では音声入力は利用できません。" + "unavailableLanguage": "このデバイスの言語では音声入力は利用できません。", + "listeningStopped": "聞き取りを停止しました。" }, "share": { "title": "Kiloに共有", diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 20552121d3..1455184a48 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -2808,7 +2808,8 @@ "busy": "ხმოვანი შეყვანა დაკავებულია. სცადეთ კიდევ.", "stopped": "ხმოვანი შეყვანა შეჩერდა. შეეხეთ მიკროფონს ხელახლა საცდელად.", "unavailableDevice": "ამ მოწყობილობაზე ხმოვანი შეყვანა მიუწვდომელია.", - "unavailableLanguage": "ხმოვანი შეყვანა მიუწვდომელია ამ მოწყობილობის ენისთვის." + "unavailableLanguage": "ხმოვანი შეყვანა მიუწვდომელია ამ მოწყობილობის ენისთვის.", + "listeningStopped": "მოსმენა შეჩერდა." }, "share": { "title": "Kilo-ში გაზიარება", diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index 3add5e4988..fe419187aa 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -2808,7 +2808,8 @@ "busy": "Дауыстық енгізу бос емес. Қайта көріңіз.", "stopped": "Дауыстық енгізу тоқтатылды. Микрофонды түртіп, қайта көріңіз.", "unavailableDevice": "Бұл құрылғыда дауыстық енгізу қолжетімсіз.", - "unavailableLanguage": "Бұл құрылғы тілі үшін дауыстық енгізу қолжетімсіз." + "unavailableLanguage": "Бұл құрылғы тілі үшін дауыстық енгізу қолжетімсіз.", + "listeningStopped": "Тыңдау тоқтатылды." }, "share": { "title": "Kilo-ға бөлісу", diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 62afc5a90f..6141194567 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -2808,7 +2808,8 @@ "busy": "ការបញ្ចូលសំឡេងកំពុងរវល់។ ព្យាយាមម្តងទៀត។", "stopped": "ការបញ្ចូលសំឡេងបានឈប់។ ប៉ះមីក្រូហ្វូន ដើម្បីព្យាយាមម្តងទៀត។", "unavailableDevice": "ការបញ្ចូលសំឡេងមិនអាចប្រើបាននៅលើឧបករណ៍នេះទេ។", - "unavailableLanguage": "ការបញ្ចូលសំឡេងមិនអាចប្រើបានសម្រាប់ភាសាឧបករណ៍នេះទេ។" + "unavailableLanguage": "ការបញ្ចូលសំឡេងមិនអាចប្រើបានសម្រាប់ភាសាឧបករណ៍នេះទេ។", + "listeningStopped": "ការស្តាប់បានឈប់។" }, "share": { "title": "ចែករំលែកទៅ Kilo", diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 0912bb6d21..79bd3a1023 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -2808,7 +2808,8 @@ "busy": "ಧ್ವನಿ ಇನ್ಪುಟ್ ಕಾರ್ಯನಿರತವಾಗಿದೆ. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.", "stopped": "ಧ್ವನಿ ಇನ್ಪುಟ್ ನಿಂತಿದೆ. ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಲು ಮೈಕ್ರೋಫೋನ್ ಟ್ಯಾಪ್ ಮಾಡಿ.", "unavailableDevice": "ಈ ಸಾಧನದಲ್ಲಿ ಧ್ವನಿ ಇನ್ಪುಟ್ ಲಭ್ಯವಿಲ್ಲ.", - "unavailableLanguage": "ಈ ಸಾಧನ ಭಾಷೆಗೆ ಧ್ವನಿ ಇನ್ಪುಟ್ ಲಭ್ಯವಿಲ್ಲ." + "unavailableLanguage": "ಈ ಸಾಧನ ಭಾಷೆಗೆ ಧ್ವನಿ ಇನ್ಪುಟ್ ಲಭ್ಯವಿಲ್ಲ.", + "listeningStopped": "ಆಲಿಸುವುದು ನಿಂತಿದೆ." }, "share": { "title": "Kilo ಗೆ ಹಂಚಿಕೊಳ್ಳಿ", diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 7d15ec1b5b..82c75402f7 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -2354,7 +2354,8 @@ "busy": "음성 입력이 사용 중입니다. 다시 시도하세요.", "stopped": "음성 입력이 중지되었습니다. 마이크를 탭하여 다시 시도하세요.", "unavailableDevice": "이 기기에서는 음성 입력을 사용할 수 없습니다.", - "unavailableLanguage": "이 기기 언어에서는 음성 입력을 사용할 수 없습니다." + "unavailableLanguage": "이 기기 언어에서는 음성 입력을 사용할 수 없습니다.", + "listeningStopped": "듣기가 중지되었습니다." }, "share": { "title": "Kilo로 공유", diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index 3aba9f337b..0c851b5c08 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -2808,7 +2808,8 @@ "busy": "ການປ້ອນສຽງກຳລັງຫຍຸ້ງ. ລອງອີກຄັ້ງ.", "stopped": "ການປ້ອນສຽງຢຸດແລ້ວ. ແຕະໄມໂຄຣໂຟນເພື່ອລອງອີກຄັ້ງ.", "unavailableDevice": "ການປ້ອນສຽງບໍ່ມີໃນອຸປະກອນນີ້.", - "unavailableLanguage": "ການປ້ອນສຽງບໍ່ມີສຳລັບພາສາຂອງອຸປະກອນນີ້." + "unavailableLanguage": "ການປ້ອນສຽງບໍ່ມີສຳລັບພາສາຂອງອຸປະກອນນີ້.", + "listeningStopped": "ຢຸດການຟັງແລ້ວ." }, "share": { "title": "ແບ່ງປັນໄປ Kilo", diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 57fb4191b6..81e9424c2d 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -2848,7 +2848,8 @@ "busy": "Balso įvestis užimta. Bandykite dar kartą.", "stopped": "Balso įvestis sustabdyta. Bakstelėkite mikrofoną, kad bandytumėte dar kartą.", "unavailableDevice": "Balso įvestis šiame įrenginyje nepasiekiama.", - "unavailableLanguage": "Balso įvestis nepasiekiama šios įrenginio kalbos." + "unavailableLanguage": "Balso įvestis nepasiekiama šios įrenginio kalbos.", + "listeningStopped": "Klausymasis sustabdytas." }, "share": { "title": "Dalintis į Kilo", diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index 44ca097a3b..d4257924a2 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -2828,7 +2828,8 @@ "busy": "Balss ievade ir aizņemta. Mēģiniet vēlreiz.", "stopped": "Balss ievade tika apturēta. Pieskarieties mikrofonam, lai mēģinātu vēlreiz.", "unavailableDevice": "Balss ievade šajā ierīcē nav pieejama.", - "unavailableLanguage": "Balss ievade nav pieejama šai ierīces valodai." + "unavailableLanguage": "Balss ievade nav pieejama šai ierīces valodai.", + "listeningStopped": "Klausīšanās apturēta." }, "share": { "title": "Kopīgot Kilo", diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index faf95e45f1..1841b55a56 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -2808,7 +2808,8 @@ "busy": "Saika miasa ny fidirana amin'ny feo. Andramana indray.", "stopped": "Nijanona ny fidirana amin'ny feo. Tsindrio ny mikrô mba andramana indray.", "unavailableDevice": "Tsy misy ny fidirana amin'ny feo amin'ity fitaovana ity.", - "unavailableLanguage": "Tsy misy ny fidirana amin'ny feo ho an'ny fitenin'ity fitaovana ity." + "unavailableLanguage": "Tsy misy ny fidirana amin'ny feo ho an'ny fitenin'ity fitaovana ity.", + "listeningStopped": "Nijanona ny fihainoana." }, "share": { "title": "Zarao amin'ny Kilo", diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index b2013164c8..3eee173363 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -2808,7 +2808,8 @@ "busy": "Kei te pukumahi te tāuru reo. Whakamātau anō.", "stopped": "Kua mutu te tāuru reo. Pāwhiritia te hopuoro kia whakamātau anō.", "unavailableDevice": "Kāore e wātea ana te tāuru reo i tēnei pūrere.", - "unavailableLanguage": "Kāore e wātea ana te tāuru reo mō tēnei reo o te pūrere." + "unavailableLanguage": "Kāore e wātea ana te tāuru reo mō tēnei reo o te pūrere.", + "listeningStopped": "Kua mutu te whakarongo." }, "share": { "title": "Tuari ki Kilo", diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index 78cf0695d1..2cb26610a9 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -2808,7 +2808,8 @@ "busy": "Гласовниот внес е зафатен. Обидете се повторно.", "stopped": "Гласовниот внес е запрен. Допрете го микрофонот за да се обидете повторно.", "unavailableDevice": "Гласовниот внес не е достапен на овој уред.", - "unavailableLanguage": "Гласовниот внес не е достапен за јазикот на овој уред." + "unavailableLanguage": "Гласовниот внес не е достапен за јазикот на овој уред.", + "listeningStopped": "Слушањето е запрено." }, "share": { "title": "Сподели во Kilo", diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 9befacde53..0afd97baf0 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -2808,7 +2808,8 @@ "busy": "ശബ്ദ ഇൻപുട്ട് തിരക്കിലാണ്. വീണ്ടും ശ്രമിക്കുക.", "stopped": "ശബ്ദ ഇൻപുട്ട് നിർത്തി. വീണ്ടും ശ്രമിക്കാൻ മൈക്രോഫോണിൽ ടാപ്പ് ചെയ്യുക.", "unavailableDevice": "ഈ ഉപകരണത്തിൽ ശബ്ദ ഇൻപുട്ട് ലഭ്യമല്ല.", - "unavailableLanguage": "ഈ ഉപകരണ ഭാഷയ്ക്ക് ശബ്ദ ഇൻപുട്ട് ലഭ്യമല്ല." + "unavailableLanguage": "ഈ ഉപകരണ ഭാഷയ്ക്ക് ശബ്ദ ഇൻപുട്ട് ലഭ്യമല്ല.", + "listeningStopped": "കേൾക്കുന്നത് നിർത്തി." }, "share": { "title": "Kilo-യിലേക്ക് പങ്കിടുക", diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index 9df29ad6f0..7e24411d15 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -2808,7 +2808,8 @@ "busy": "Дуу оруулалт завгүй байна. Дахин оролдоно уу.", "stopped": "Дуу оруулалт зогссон. Дахин оролдохын тулд микрофоныг товшино уу.", "unavailableDevice": "Энэ төхөөрөмж дээр дуу оруулалт боломжгүй.", - "unavailableLanguage": "Энэ төхөөрөмжийн хэлэнд дуу оруулалт боломжгүй." + "unavailableLanguage": "Энэ төхөөрөмжийн хэлэнд дуу оруулалт боломжгүй.", + "listeningStopped": "Сонсох ажиллагаа зогссон." }, "share": { "title": "Kilo руу хуваалцах", diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index 4dc393462a..471f169279 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -2808,7 +2808,8 @@ "busy": "व्हॉइस इनपुट व्यस्त आहे. पुन्हा प्रयत्न करा.", "stopped": "व्हॉइस इनपुट थांबले. पुन्हा प्रयत्न करण्यासाठी मायक्रोफोनवर टॅप करा.", "unavailableDevice": "या डिव्हाइसवर व्हॉइस इनपुट उपलब्ध नाही.", - "unavailableLanguage": "या डिव्हाइसच्या भाषेसाठी व्हॉइस इनपुट उपलब्ध नाही." + "unavailableLanguage": "या डिव्हाइसच्या भाषेसाठी व्हॉइस इनपुट उपलब्ध नाही.", + "listeningStopped": "ऐकणे थांबले." }, "share": { "title": "Kilo ला शेअर करा", diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 66bf72446d..c59066d7cb 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -2808,7 +2808,8 @@ "busy": "Input suara sibuk. Cuba lagi.", "stopped": "Input suara dihentikan. Ketik mikrofon untuk mencuba lagi.", "unavailableDevice": "Input suara tidak tersedia pada peranti ini.", - "unavailableLanguage": "Input suara tidak tersedia untuk bahasa peranti ini." + "unavailableLanguage": "Input suara tidak tersedia untuk bahasa peranti ini.", + "listeningStopped": "Pendengaran dihentikan." }, "share": { "title": "Kongsi ke Kilo", diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index cf76a49b62..b5d108c7ba 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -2868,7 +2868,8 @@ "busy": "Id-dħul bil-vuċi huwa okkupat. Erġa' pprova.", "stopped": "Id-dħul bil-vuċi waqaf. Tektek il-mikrofonu biex terġa' tipprova.", "unavailableDevice": "Id-dħul bil-vuċi mhux disponibbli fuq dan it-tagħmir.", - "unavailableLanguage": "Id-dħul bil-vuċi mhux disponibbli għal din il-lingwa tal-apparat." + "unavailableLanguage": "Id-dħul bil-vuċi mhux disponibbli għal din il-lingwa tal-apparat.", + "listeningStopped": "Is-smigħ waqaf." }, "share": { "title": "Aqsam ma' Kilo", diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index fbf77eafe3..66c17e89ef 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -2808,7 +2808,8 @@ "busy": "အသံထည့်သွင်းမှု အလုပ်ရှုပ်နေသည်။ ထပ်စမ်းကြည့်ပါ။", "stopped": "အသံထည့်သွင်းမှု ရပ်သွားသည်။ ထပ်စမ်းရန် မိုက်ခရိုဖုန်းကို နှိပ်ပါ။", "unavailableDevice": "ဤစက်တွင် အသံထည့်သွင်းမှု မရနိုင်ပါ။", - "unavailableLanguage": "ဤစက်၏ ဘာသာစကားအတွက် အသံထည့်သွင်းမှု မရနိုင်ပါ။" + "unavailableLanguage": "ဤစက်၏ ဘာသာစကားအတွက် အသံထည့်သွင်းမှု မရနိုင်ပါ။", + "listeningStopped": "နားထောင်ခြင်း ရပ်သွားသည်။" }, "share": { "title": "Kilo သို့ မျှဝေရန်", diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index d809b7c8fb..eef7c3bb58 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -2808,7 +2808,8 @@ "busy": "Taleinntasting er opptatt. Prøv igjen.", "stopped": "Taleinntasting stoppet. Trykk på mikrofonen for å prøve igjen.", "unavailableDevice": "Taleinntasting er ikke tilgjengelig på denne enheten.", - "unavailableLanguage": "Taleinntasting er ikke tilgjengelig for dette enhetsspråket." + "unavailableLanguage": "Taleinntasting er ikke tilgjengelig for dette enhetsspråket.", + "listeningStopped": "Lyttingen er stoppet." }, "share": { "title": "Del til Kilo", diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index 4e6b92bf0f..28a17024a8 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -2808,7 +2808,8 @@ "busy": "भ्वाइस इनपुट व्यस्त छ। फेरि प्रयास गर्नुहोस्।", "stopped": "भ्वाइस इनपुट रोकियो। फेरि प्रयास गर्न माइक्रोफोनमा ट्याप गर्नुहोस्।", "unavailableDevice": "यो यन्त्रमा भ्वाइस इनपुट उपलब्ध छैन।", - "unavailableLanguage": "यो यन्त्रको भाषाका लागि भ्वाइस इनपुट उपलब्ध छैन।" + "unavailableLanguage": "यो यन्त्रको भाषाका लागि भ्वाइस इनपुट उपलब्ध छैन।", + "listeningStopped": "सुन्न रोकियो।" }, "share": { "title": "Kilo मा साझेदारी गर्नुहोस्", diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index f21b3df9ff..4e34137278 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -2354,7 +2354,8 @@ "busy": "Spraakinvoer is bezet. Probeer het opnieuw.", "stopped": "Spraakinvoer gestopt. Tik op de microfoon om opnieuw te proberen.", "unavailableDevice": "Spraakinvoer is niet beschikbaar op dit apparaat.", - "unavailableLanguage": "Spraakinvoer is niet beschikbaar voor de taal van dit apparaat." + "unavailableLanguage": "Spraakinvoer is niet beschikbaar voor de taal van dit apparaat.", + "listeningStopped": "Luisteren gestopt." }, "share": { "title": "Delen naar Kilo", diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index df05df929c..bc5da32d98 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -2808,7 +2808,8 @@ "busy": "Galchuun sagalee qabanaa jira. Irra deebi'ii yaali.", "stopped": "Galchuun sagalee dhaabbateera. Irra deebi'uuf maayikirofoonii tuqi.", "unavailableDevice": "Galchuun sagalee meeshaa kana irratti hin jiru.", - "unavailableLanguage": "Galchuun sagalee afaan meeshaa kanaaf hin jiru." + "unavailableLanguage": "Galchuun sagalee afaan meeshaa kanaaf hin jiru.", + "listeningStopped": "Dhaggeeffachuun dhaabbateera." }, "share": { "title": "Kilo wajjin qoodi", diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index 3d1e5483f3..6d1aa03a2f 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -2808,7 +2808,8 @@ "busy": "ଭଏସ୍ ଇନ୍ପୁଟ୍ ବ୍ୟସ୍ତ ଅଛି। ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ।", "stopped": "ଭଏସ୍ ଇନ୍ପୁଟ୍ ବନ୍ଦ ହେଲା। ପୁଣି ଚେଷ୍ଟା କରିବାକୁ ମାଇକ୍ରୋଫୋନ୍ ଟାପ୍ କରନ୍ତୁ।", "unavailableDevice": "ଏହି ଡିଭାଇସ୍ ରେ ଭଏସ୍ ଇନ୍ପୁଟ୍ ଉପଲବ୍ଧ ନାହିଁ।", - "unavailableLanguage": "ଏହି ଡିଭାଇସ୍ ଭାଷା ପାଇଁ ଭଏସ୍ ଇନ୍ପୁଟ୍ ଉପଲବ୍ଧ ନାହିଁ।" + "unavailableLanguage": "ଏହି ଡିଭାଇସ୍ ଭାଷା ପାଇଁ ଭଏସ୍ ଇନ୍ପୁଟ୍ ଉପଲବ୍ଧ ନାହିଁ।", + "listeningStopped": "ଶୁଣିବା ବନ୍ଦ ହେଲା।" }, "share": { "title": "Kilo କୁ ଅଂଶୀଦାର କରନ୍ତୁ", diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 9230f95b58..9ae74efe57 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -2808,7 +2808,8 @@ "busy": "ਵੌਇਸ ਇਨਪੁਟ ਰੁੱਝਿਆ ਹੋਇਆ ਹੈ। ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", "stopped": "ਵੌਇਸ ਇਨਪੁਟ ਰੁਕ ਗਿਆ। ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰਨ ਲਈ ਮਾਈਕ੍ਰੋਫ਼ੋਨ 'ਤੇ ਟੈਪ ਕਰੋ।", "unavailableDevice": "ਇਸ ਡਿਵਾਈਸ 'ਤੇ ਵੌਇਸ ਇਨਪੁਟ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।", - "unavailableLanguage": "ਇਸ ਡਿਵਾਈਸ ਦੀ ਭਾਸ਼ਾ ਲਈ ਵੌਇਸ ਇਨਪੁਟ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।" + "unavailableLanguage": "ਇਸ ਡਿਵਾਈਸ ਦੀ ਭਾਸ਼ਾ ਲਈ ਵੌਇਸ ਇਨਪੁਟ ਉਪਲਬਧ ਨਹੀਂ ਹੈ।", + "listeningStopped": "ਸੁਣਨਾ ਰੁਕ ਗਿਆ।" }, "share": { "title": "Kilo ਨੂੰ ਸਾਂਝਾ ਕਰੋ", diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index b7f10e0f89..6cdc7c8bb4 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -2380,7 +2380,8 @@ "busy": "Wprowadzanie głosowe jest zajęte. Spróbuj ponownie.", "stopped": "Wprowadzanie głosowe zostało zatrzymane. Dotknij mikrofonu, aby spróbować ponownie.", "unavailableDevice": "Wprowadzanie głosowe nie jest dostępne na tym urządzeniu.", - "unavailableLanguage": "Wprowadzanie głosowe nie jest dostępne dla języka tego urządzenia." + "unavailableLanguage": "Wprowadzanie głosowe nie jest dostępne dla języka tego urządzenia.", + "listeningStopped": "Słuchanie zatrzymane." }, "share": { "title": "Udostępnij do Kilo", diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 003be69de2..b7e9b0d2aa 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -2808,7 +2808,8 @@ "busy": "د غږ معلومات بوخت دی. بیا هڅه وکړئ.", "stopped": "د غږ معلومات ودرول شو. د بیا هڅې لپاره مایکروفون ټک کړئ.", "unavailableDevice": "د غږ معلومات په دې وسیله شتون نلري.", - "unavailableLanguage": "د غږ معلومات د دې وسیلې ژبې لپاره شتون نلري." + "unavailableLanguage": "د غږ معلومات د دې وسیلې ژبې لپاره شتون نلري.", + "listeningStopped": "اورېدل ودرېدل." }, "share": { "title": "Kilo ته شریک کړئ", diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index 8cdd4d37bd..6f3948b54f 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -2367,7 +2367,8 @@ "busy": "A entrada de voz está ocupada. Tente novamente.", "stopped": "A entrada de voz foi interrompida. Toque no microfone para tentar novamente.", "unavailableDevice": "A entrada de voz não está disponível neste dispositivo.", - "unavailableLanguage": "A entrada de voz não está disponível para o idioma deste dispositivo." + "unavailableLanguage": "A entrada de voz não está disponível para o idioma deste dispositivo.", + "listeningStopped": "Escuta interrompida." }, "share": { "title": "Compartilhar no Kilo", diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index e07ffa8a03..9ba477fcd5 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -2828,7 +2828,8 @@ "busy": "A entrada de voz está ocupada. Tente novamente.", "stopped": "A entrada de voz foi interrompida. Toque no microfone para tentar novamente.", "unavailableDevice": "A entrada de voz não está disponível neste dispositivo.", - "unavailableLanguage": "A entrada de voz não está disponível para o idioma deste dispositivo." + "unavailableLanguage": "A entrada de voz não está disponível para o idioma deste dispositivo.", + "listeningStopped": "Escuta interrompida." }, "share": { "title": "Partilhar no Kilo", diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index 19874f2c34..fdce33874a 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -2828,7 +2828,8 @@ "busy": "Introducerea vocală este ocupată. Încearcă din nou.", "stopped": "Introducerea vocală s-a oprit. Atinge microfonul pentru a încerca din nou.", "unavailableDevice": "Introducerea vocală nu este disponibilă pe acest dispozitiv.", - "unavailableLanguage": "Introducerea vocală nu este disponibilă pentru limba acestui dispozitiv." + "unavailableLanguage": "Introducerea vocală nu este disponibilă pentru limba acestui dispozitiv.", + "listeningStopped": "Ascultarea s-a oprit." }, "share": { "title": "Distribuie către Kilo", diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 3628ab2096..fa417d4e0e 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -2380,7 +2380,8 @@ "busy": "Голосовой ввод занят. Попробуйте снова.", "stopped": "Голосовой ввод остановлен. Нажмите на микрофон, чтобы попробовать снова.", "unavailableDevice": "Голосовой ввод недоступен на этом устройстве.", - "unavailableLanguage": "Голосовой ввод недоступен для языка этого устройства." + "unavailableLanguage": "Голосовой ввод недоступен для языка этого устройства.", + "listeningStopped": "Прослушивание остановлено." }, "share": { "title": "Поделиться в Kilo", diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index bb75e71b54..5fbff715a5 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -2808,7 +2808,8 @@ "busy": "හඬ ආදානය කාර්යබහුලයි. නැවත උත්සාහ කරන්න.", "stopped": "හඬ ආදානය නතර විය. නැවත උත්සාහ කිරීමට මයික්‍රෆෝනය තට්ටු කරන්න.", "unavailableDevice": "මෙම උපාංගයේ හඬ ආදානය ලබා ගත නොහැක.", - "unavailableLanguage": "මෙම උපාංග භාෂාව සඳහා හඬ ආදානය ලබා ගත නොහැක." + "unavailableLanguage": "මෙම උපාංග භාෂාව සඳහා හඬ ආදානය ලබා ගත නොහැක.", + "listeningStopped": "සවන් දීම නතර විය." }, "share": { "title": "Kilo වෙත බෙදාගන්න", diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index 61504f5d7a..c24c6ab10c 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -2848,7 +2848,8 @@ "busy": "Hlasový vstup je zaneprázdnený. Skúste to znova.", "stopped": "Hlasový vstup sa zastavil. Klepnutím na mikrofón to skúste znova.", "unavailableDevice": "Hlasový vstup nie je na tomto zariadení dostupný.", - "unavailableLanguage": "Hlasový vstup nie je dostupný pre jazyk tohto zariadenia." + "unavailableLanguage": "Hlasový vstup nie je dostupný pre jazyk tohto zariadenia.", + "listeningStopped": "Počúvanie sa zastavilo." }, "share": { "title": "Zdieľať do Kilo", diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index 059e9f3be5..fadac2890f 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -2848,7 +2848,8 @@ "busy": "Glasovni vnos je zaseden. Poskusite znova.", "stopped": "Glasovni vnos je bil ustavljen. Dotaknite se mikrofona za ponovni poskus.", "unavailableDevice": "Glasovni vnos na tej napravi ni na voljo.", - "unavailableLanguage": "Glasovni vnos ni na voljo za ta jezik naprave." + "unavailableLanguage": "Glasovni vnos ni na voljo za ta jezik naprave.", + "listeningStopped": "Poslušanje je ustavljeno." }, "share": { "title": "Deli s Kilo", diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index ee5281f76b..8972426c1f 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -2808,7 +2808,8 @@ "busy": "Gelinta codka waa mashquul. Mar kale isku day.", "stopped": "Gelinta codku way joogsatay. Taabo makarafoonka si aad mar kale isku daydo.", "unavailableDevice": "Gelinta codka aaladdan kuma heli karto.", - "unavailableLanguage": "Gelinta codka looma heli karo luqaddan aaladda." + "unavailableLanguage": "Gelinta codka looma heli karo luqaddan aaladda.", + "listeningStopped": "Dhageysigu wuu joogsaday." }, "share": { "title": "Wadaag Kilo", diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index 6a8be213f8..02db74e18c 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -2808,7 +2808,8 @@ "busy": "Futja me zë është e zënë. Provo përsëri.", "stopped": "Futja me zë u ndal. Prekni mikrofonin për të provuar përsëri.", "unavailableDevice": "Futja me zë nuk është e disponueshme në këtë pajisje.", - "unavailableLanguage": "Futja me zë nuk është e disponueshme për gjuhën e kësaj pajisjeje." + "unavailableLanguage": "Futja me zë nuk është e disponueshme për gjuhën e kësaj pajisjeje.", + "listeningStopped": "Dëgjimi u ndal." }, "share": { "title": "Ndaj te Kilo", diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index 28a398ec03..0981f409fe 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -2828,7 +2828,8 @@ "busy": "Glasovni unos je zauzet. Pokušajte ponovo.", "stopped": "Glasovni unos je zaustavljen. Dodirnite mikrofon da pokušate ponovo.", "unavailableDevice": "Glasovni unos nije dostupan na ovom uređaju.", - "unavailableLanguage": "Glasovni unos nije dostupan za jezik ovog uređaja." + "unavailableLanguage": "Glasovni unos nije dostupan za jezik ovog uređaja.", + "listeningStopped": "Slušanje je zaustavljeno." }, "share": { "title": "Podeli u Kilo", diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index 8eb7dbda79..70f4d92023 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -2808,7 +2808,8 @@ "busy": "Röstinmatningen är upptagen. Försök igen.", "stopped": "Röstinmatningen stoppades. Tryck på mikrofonen för att försöka igen.", "unavailableDevice": "Röstinmatning är inte tillgänglig på den här enheten.", - "unavailableLanguage": "Röstinmatning är inte tillgänglig för den här enhetens språk." + "unavailableLanguage": "Röstinmatning är inte tillgänglig för den här enhetens språk.", + "listeningStopped": "Lyssningen stoppades." }, "share": { "title": "Dela till Kilo", diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index 403da103cb..97f45844ef 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -2808,7 +2808,8 @@ "busy": "Ingizo la sauti linashughulika. Jaribu tena.", "stopped": "Ingizo la sauti limesimama. Gusa kipaza sauti kujaribu tena.", "unavailableDevice": "Ingizo la sauti halipatikani kwenye kifaa hiki.", - "unavailableLanguage": "Ingizo la sauti halipatikani kwa lugha hii ya kifaa." + "unavailableLanguage": "Ingizo la sauti halipatikani kwa lugha hii ya kifaa.", + "listeningStopped": "Usikilizaji umesimama." }, "share": { "title": "Shiriki kwa Kilo", diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index f11ec05f0d..7eb033f306 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -2808,7 +2808,8 @@ "busy": "குரல் உள்ளீடு பரபரப்பாக உள்ளது. மீண்டும் முயற்சிக்கவும்.", "stopped": "குரல் உள்ளீடு நிறுத்தப்பட்டது. மீண்டும் முயற்சிக்க மைக்ரோஃபோனைத் தட்டவும்.", "unavailableDevice": "இந்த சாதனத்தில் குரல் உள்ளீடு கிடைக்கவில்லை.", - "unavailableLanguage": "இந்த சாதன மொழிக்கு குரல் உள்ளீடு கிடைக்கவில்லை." + "unavailableLanguage": "இந்த சாதன மொழிக்கு குரல் உள்ளீடு கிடைக்கவில்லை.", + "listeningStopped": "கேட்பது நிறுத்தப்பட்டது." }, "share": { "title": "Kiloவுக்கு பகிர்", diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 22d003e2da..caf955bf39 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -2808,7 +2808,8 @@ "busy": "వాయిస్ ఇన్పుట్ బిజీగా ఉంది. మళ్లీ ప్రయత్నించండి.", "stopped": "వాయిస్ ఇన్పుట్ ఆగిపోయింది. మళ్లీ ప్రయత్నించడానికి మైక్రోఫోన్ను తాప్ చేయండి.", "unavailableDevice": "ఈ పరికరంలో వాయిస్ ఇన్పుట్ అందుబాటులో లేదు.", - "unavailableLanguage": "ఈ పరికర భాషకు వాయిస్ ఇన్పుట్ అందుబాటులో లేదు." + "unavailableLanguage": "ఈ పరికర భాషకు వాయిస్ ఇన్పుట్ అందుబాటులో లేదు.", + "listeningStopped": "వినడం ఆగిపోయింది." }, "share": { "title": "Kiloకి భాగస్వామ్యం చేయండి", diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index 8aa0110d0c..3bbdbe63c3 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -2808,7 +2808,8 @@ "busy": "การป้อนเสียงไม่ว่าง ลองอีกครั้ง", "stopped": "การป้อนเสียงหยุดแล้ว แตะไมโครโฟนเพื่อลองอีกครั้ง", "unavailableDevice": "การป้อนเสียงไม่พร้อมใช้งานบนอุปกรณ์นี้", - "unavailableLanguage": "การป้อนเสียงไม่พร้อมใช้งานสำหรับภาษาของอุปกรณ์นี้" + "unavailableLanguage": "การป้อนเสียงไม่พร้อมใช้งานสำหรับภาษาของอุปกรณ์นี้", + "listeningStopped": "หยุดฟังแล้ว" }, "share": { "title": "แชร์ไปยัง Kilo", diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index fc63993b82..f33cce9789 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -2354,7 +2354,8 @@ "busy": "Sesli giriş meşgul. Tekrar deneyin.", "stopped": "Sesli giriş durdu. Tekrar denemek için mikrofona dokunun.", "unavailableDevice": "Sesli giriş bu cihazda kullanılamıyor.", - "unavailableLanguage": "Sesli giriş bu cihaz dili için kullanılamıyor." + "unavailableLanguage": "Sesli giriş bu cihaz dili için kullanılamıyor.", + "listeningStopped": "Dinleme durdu." }, "share": { "title": "Kilo'ya paylaş", diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 0e4339e4fa..9885d8858d 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -2380,7 +2380,8 @@ "busy": "Голосове введення зайняте. Спробуйте ще раз.", "stopped": "Голосове введення зупинено. Торкніться мікрофона, щоб спробувати ще раз.", "unavailableDevice": "Голосове введення недоступне на цьому пристрої.", - "unavailableLanguage": "Голосове введення недоступне для мови цього пристрою." + "unavailableLanguage": "Голосове введення недоступне для мови цього пристрою.", + "listeningStopped": "Прослуховування зупинено." }, "share": { "title": "Поділитися в Kilo", diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index 4dca812920..748d6d5d75 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -2808,7 +2808,8 @@ "busy": "وائس انپٹ مصروف ہے۔ دوبارہ کوشش کریں۔", "stopped": "وائس انپٹ رک گیا۔ دوبارہ کوشش کرنے کے لیے مائیکروفون پر ٹیپ کریں۔", "unavailableDevice": "وائس انپٹ اس ڈیوائس پر دستیاب نہیں ہے۔", - "unavailableLanguage": "وائس انپٹ اس ڈیوائس کی زبان کے لیے دستیاب نہیں ہے۔" + "unavailableLanguage": "وائس انپٹ اس ڈیوائس کی زبان کے لیے دستیاب نہیں ہے۔", + "listeningStopped": "سننا رک گیا۔" }, "share": { "title": "Kilo پر شیئر کریں", diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index f841d91ee0..9f0abd55f1 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -2808,7 +2808,8 @@ "busy": "Ovozli kiritish band. Qayta urinib ko'ring.", "stopped": "Ovozli kiritish to'xtatildi. Qayta urinish uchun mikrofonni bosing.", "unavailableDevice": "Ovozli kiritish bu qurilmada mavjud emas.", - "unavailableLanguage": "Ovozli kiritish bu qurilma tili uchun mavjud emas." + "unavailableLanguage": "Ovozli kiritish bu qurilma tili uchun mavjud emas.", + "listeningStopped": "Tinglash to'xtatildi." }, "share": { "title": "Kiloga ulashish", diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index ad613657b5..fe8357b683 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -2354,7 +2354,8 @@ "busy": "Nhập giọng nói đang bận. Thử lại.", "stopped": "Đã dừng nhập giọng nói. Chạm vào micro để thử lại.", "unavailableDevice": "Không thể nhập giọng nói trên thiết bị này.", - "unavailableLanguage": "Không thể nhập giọng nói bằng ngôn ngữ của thiết bị này." + "unavailableLanguage": "Không thể nhập giọng nói bằng ngôn ngữ của thiết bị này.", + "listeningStopped": "Đã dừng nghe." }, "share": { "title": "Chia sẻ đến Kilo", diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index e5620dae08..ad89c0202a 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -2808,7 +2808,8 @@ "busy": "Ìfẹ̀sí ohùn dí. Gbìyànjú lẹ́ẹ̀kan sí i.", "stopped": "Ìfẹ̀sí ohùn dúró. Tẹ míkírọ́fóònù láti gbìyànjú lẹ́ẹ̀kan sí i.", "unavailableDevice": "Ìfẹ̀sí ohùn kò sí lórí ẹ̀rọ yìí.", - "unavailableLanguage": "Ìfẹ̀sí ohùn kò sí fún èdè ẹ̀rọ yìí." + "unavailableLanguage": "Ìfẹ̀sí ohùn kò sí fún èdè ẹ̀rọ yìí.", + "listeningStopped": "Gbigbọ ti duro." }, "share": { "title": "Pin si Kilo", diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index f1eb2679cc..881c6bea8f 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -2354,7 +2354,8 @@ "busy": "语音输入正忙。请重试。", "stopped": "语音输入已停止。点击麦克风重试。", "unavailableDevice": "此设备不支持语音输入。", - "unavailableLanguage": "语音输入不支持此设备的语言。" + "unavailableLanguage": "语音输入不支持此设备的语言。", + "listeningStopped": "已停止聆听。" }, "share": { "title": "分享到 Kilo", diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index 677eca0e66..5b745371a1 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -2354,7 +2354,8 @@ "busy": "語音輸入忙碌中。請再試一次。", "stopped": "語音輸入已停止。點按麥克風圖示再試一次。", "unavailableDevice": "此裝置上無法使用語音輸入。", - "unavailableLanguage": "此裝置語言無法使用語音輸入。" + "unavailableLanguage": "此裝置語言無法使用語音輸入。", + "listeningStopped": "已停止聆聽。" }, "share": { "title": "分享到 Kilo", diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 5503373319..3e59c14aa9 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -2808,7 +2808,8 @@ "busy": "Ukufaka ngezwi kumatasatasa. Zama futhi.", "stopped": "Ukufaka ngezwi kumisiwe. Thepha imakrofoni ukuze uzame futhi.", "unavailableDevice": "Ukufaka ngezwi akutholakali kule divayisi.", - "unavailableLanguage": "Ukufaka ngezwi akutholakali kulolu limi lwedivayisi." + "unavailableLanguage": "Ukufaka ngezwi akutholakali kulolu limi lwedivayisi.", + "listeningStopped": "Ukulalela kumisiwe." }, "share": { "title": "Yabelana nge-Kilo",