Skip to content
Merged
Show file tree
Hide file tree
Changes from 65 commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
bde991a
fix(episodes): every LLM transcript labels voices densely, never raw …
Git-on-my-level Oct 23, 2026
dd347bf
test(backend): guard every LLM formatter against raw speaker-id labels
Git-on-my-level Oct 23, 2026
7dd7948
Merge live speaker formatter dependency (#23404)
Git-on-my-level Oct 23, 2026
a3c16a0
fix(speakers): add typed identity, count and presentation guardrails
Git-on-my-level Oct 23, 2026
d78dfa0
chore(api): refresh speaker display contract snapshot
Git-on-my-level Oct 24, 2026
f79f136
chore(clients): regenerate safe speaker metadata types
Git-on-my-level Oct 24, 2026
0f34fb0
docs: align historical reload pointer with main
Git-on-my-level Oct 24, 2026
63a27d4
fix(notes): People names bind to a speaker only with this conversatio…
Git-on-my-level Oct 24, 2026
53249b5
fix(notes): type identity name lists for the backend typecheck
Git-on-my-level Oct 24, 2026
bd421e2
Merge current main including #23472 into PR #23462
Git-on-my-level Oct 24, 2026
12eafa7
test(speakers): repair CI loaders and canonical display assertions
Git-on-my-level Oct 24, 2026
4397f19
test(app): drop duplicate import in speaker names test
Git-on-my-level Oct 24, 2026
2674101
Merge current main projection fixes into PR #23462
Git-on-my-level Oct 24, 2026
0961af8
chore: integrate speaker identity prerequisites for live epochs
Git-on-my-level Oct 24, 2026
46d645c
fix(speakers): defer short live epoch ordinals and gate acoustic grou…
Git-on-my-level Oct 24, 2026
dc9f8ba
test(speakers): align inherited assertions with typed prompt identities
Git-on-my-level Oct 24, 2026
ce89b72
test(backend): wake-word renderer expects opaque speaker prompt keys
Git-on-my-level Oct 24, 2026
0c85589
fix(speakers): keep live excerpt names bound to their evidence
Git-on-my-level Oct 24, 2026
73bb9ef
Merge published speaker CI fixes before the remaining loader repair
Git-on-my-level Oct 24, 2026
c518a37
fix(notes): defer known_spellings import so isolated models loads sta…
Git-on-my-level Oct 24, 2026
79083a0
test(backend): isolate spelling database reads in date harness
Git-on-my-level Oct 24, 2026
38397a1
chore: refresh speaker identity prerequisite
Git-on-my-level Oct 24, 2026
c34e9ce
chore: refresh main before live speaker epoch publication
Git-on-my-level Oct 24, 2026
e505182
fix(app): count distinct participant identities with shared names
Git-on-my-level Oct 24, 2026
6a74ad9
Merge published lazy spelling import repair
Git-on-my-level Oct 24, 2026
f2194d4
test(backend): remove adapters superseded by lazy spelling import
Git-on-my-level Oct 24, 2026
56d8709
Merge current main while retaining canonical speaker presentation
Git-on-my-level Oct 24, 2026
876034f
perf(test): avoid counter subprocesses in isolated worker polling
Git-on-my-level Oct 24, 2026
2c2f04e
test(ci): retain main full-scan speaker exemption boundary
Git-on-my-level Oct 24, 2026
ff78741
test(backend): preserve distinct labels in whole-transcript hash asse…
Git-on-my-level Oct 24, 2026
8999b65
perf(test): wait for tracked worker completions without busy polling
Git-on-my-level Oct 24, 2026
0e77171
test(backend): synchronize heartbeat drain readiness explicitly
Git-on-my-level Oct 24, 2026
d9be3ed
perf(test): avoid unused plugins in isolated pytest workers
Git-on-my-level Oct 24, 2026
f539d67
Merge remote-tracking branch 'origin/main' into ci-fix/speaker-identi…
Git-on-my-level Oct 24, 2026
02a2b6a
fix(backend): type defensive retrieval and duration boundaries accura…
Git-on-my-level Oct 24, 2026
9e9606e
fix(backend): retain nullable query narrowing before text validation
Git-on-my-level Oct 24, 2026
ed60c62
chore: integrate reconciled speaker identity prerequisite
Git-on-my-level Oct 24, 2026
aabf467
chore: integrate current main after speaker guardrails reconciliation
Git-on-my-level Oct 24, 2026
5ce61f3
test(speakers): qualify swapped provider labels after listen reconnect
Git-on-my-level Oct 24, 2026
d33179a
test(speakers): qualify legacy live labels with placed audio clocks
Git-on-my-level Oct 24, 2026
18d27ad
test(backend): initialize Wrapped router dependencies in the class fi…
Git-on-my-level Oct 24, 2026
49041fd
fix(backend): normalize result text explicitly and load pure test dep…
Git-on-my-level Oct 24, 2026
0d935c3
test(backend): load real result-bound helpers in the product-info fix…
Git-on-my-level Oct 24, 2026
2b8313b
test(web): await the scheduled task-row hover control
Git-on-my-level Oct 24, 2026
7ae989f
chore: refresh main before live epoch publication
Git-on-my-level Oct 24, 2026
e826062
Merge current main and preserve validated CI boundary repairs
Git-on-my-level Oct 24, 2026
b6bd0a8
test(backend): initialize isolated payment router in fixture setup
Git-on-my-level Oct 24, 2026
6a08efe
fix(speakers): retain qualified live labels across moment deltas
Git-on-my-level Oct 24, 2026
4b760e4
chore: refresh speaker prerequisite before live epoch publication
Git-on-my-level Oct 24, 2026
ff289bf
chore: refresh main before live epoch publication
Git-on-my-level Oct 24, 2026
c499d99
fix(notes): preserve contact mentions and developer speaker identities
Git-on-my-level Oct 24, 2026
0d323ce
test(speakers): fix timestamps in raw-identity assertions
Git-on-my-level Oct 24, 2026
9292145
chore: integrate refreshed speaker identity prerequisite
Git-on-my-level Oct 24, 2026
9e23b44
chore: reconcile current main with live speaker epochs
Git-on-my-level Oct 24, 2026
d544d26
test(ci): align fixture setup with current main
Git-on-my-level Oct 24, 2026
972cefe
chore: refresh main before live epoch publication
Git-on-my-level Oct 24, 2026
2d2b789
chore: refresh prerequisite before main fixture reconciliation
Git-on-my-level Oct 24, 2026
07c1112
chore: retain current main payment fixture isolation
Git-on-my-level Oct 24, 2026
fe4adf5
chore: refresh main before live epoch publication
Git-on-my-level Oct 24, 2026
5871054
test(ci): keep merged fixtures coherent with current main
Git-on-my-level Oct 24, 2026
ebf26f6
chore: refresh main before route fixture repair
Git-on-my-level Oct 24, 2026
8385c9c
test(speakers): align external route fixture with rendering contract
Git-on-my-level Oct 24, 2026
2f14c47
chore: reconcile episode and registry additions from main
Git-on-my-level Oct 24, 2026
72ad5c1
fix(ci): avoid adjacent additions while preserving guardrail coverage
Git-on-my-level Oct 24, 2026
a201506
chore: integrate prerequisite CI fixture coherence repair
Git-on-my-level Oct 24, 2026
cb9c61f
chore: integrate capture evidence and speaker fixture dependencies
Git-on-my-level Oct 24, 2026
1f1c239
test(ci): compose capture evidence and speaker fixture imports
Git-on-my-level Oct 24, 2026
eaeba35
chore: integrate prerequisite fixture placement repair
Git-on-my-level Oct 24, 2026
5011a0f
chore: reconcile bounded episode receipts with speaker fixtures
Git-on-my-level Oct 24, 2026
16a4fb2
chore: refresh main before live epoch publication
Git-on-my-level Oct 24, 2026
072a983
test(ci): isolate speaker stubs from evolving episode reset fixture
Git-on-my-level Oct 24, 2026
1f55d8f
chore: align shared speaker-name fixture with prerequisite
Git-on-my-level Oct 24, 2026
b8b97bd
chore: refresh main before live epoch publication
Git-on-my-level Oct 24, 2026
7d400b8
chore: integrate merged speaker identity prerequisite from main
Git-on-my-level Oct 25, 2026
4342fb7
Merge origin/main into lane/live-speaker-epochs
Git-on-my-level Oct 25, 2026
65fdab9
fix(speakers): defer provisional cross-epoch owner competition
Git-on-my-level Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/checks-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
# Checks that consume PR metadata declare `requires_pr_body: true`; post-merge
# push runs exclude only that category because no authoritative PR body is available.
checks:
- id: speaker-identity-interpolation-ratchet
command: ["python3", ".github/scripts/check_speaker_label_interpolation.py", "--changed-files", "{changed_files}", "--base", "{base}"]
triggers: ["backend/**/*.py", "app/lib/**/*.dart", ".github/scripts/*speaker_label_interpolation.py", ".github/checks-manifest.yaml"]
lanes: ["local", "ci"]
reason: "#23379 and Speaker 379 live notes: raw diarization interpolation must not re-enter display or prompts"
- id: speaker-label-interpolation-tests
command: ["python3", ".github/scripts/test_check_speaker_label_interpolation.py"]
triggers: [".github/scripts/*speaker_label_interpolation.py", ".github/checks-manifest.yaml"]
lanes: ["local", "ci"]
reason: "Fingerprint ratchet rejects f-strings, format, percent and Dart raw speaker interpolation without trading away old leaks"
- id: nightly-main-contract-tests
command: ["python3", "scripts/ci/test_nightly_main.py"]
triggers: [".github/workflows/**", "scripts/ci/nightly_main_report.py", "scripts/ci/test_nightly_main*.py", ".github/checks-manifest.yaml"]
Expand Down Expand Up @@ -1868,7 +1878,7 @@ checks:
command: ["python3", ".github/scripts/check_speaker_label_interpolation.py"]
triggers: ["backend/**/*.py", ".github/scripts/check_speaker_label_interpolation.py", ".github/checks-manifest.yaml"]
lanes: ["local", "ci"]
reason: "#23381: an interpolated 'Speaker {speaker_id}' prompt label is a raw per-conversation diarization counter that summarizers copy verbatim into user-visible notes; unnamed voices must be labeled through utils.speaker_display.SpeakerLabeler dense labels"
reason: "#23381: an interpolated 'Speaker {speaker_id}' prompt label is a raw per-conversation diarization counter that summarizers copy verbatim into user-visible notes; unnamed voices must use the identity-aware utils.speakers.display authority"
- id: speaker-label-interpolation-selftest
command: ["python3", ".github/scripts/check_speaker_label_interpolation.py", "--self-test"]
triggers: [".github/scripts/check_speaker_label_interpolation.py", ".github/checks-manifest.yaml"]
Expand Down
170 changes: 153 additions & 17 deletions .github/scripts/check_speaker_label_interpolation.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,155 @@
#!/usr/bin/env python3
"""Reject interpolated ``Speaker {speaker_id}`` display labels in backend source.

A ``speaker_id`` is a per-conversation diarization counter, not a person. When a
formatter interpolates it into a ``Speaker N`` label, summarizer models copy the
label verbatim into user-visible notes (#23381). Unnamed voices must be labeled
through ``backend/utils/speaker_display.SpeakerLabeler`` (dense "Speaker A/B/..."
per rendered transcript), which is the only allowlisted builder of ``Speaker ``
labels.

The check is AST-based and zero-tolerance outside that allowlist: f-strings,
``str.format`` templates, ``%`` templates, and concatenation all count when a
constant chunk ends with the word ``Speaker`` right before a dynamic value. Log
messages that merely mention a speaker ("Speaker assignment failed: ...") do not
match, and the provider-internal ``SPEAKER_00`` namespace is a different string.
"""Ratchet raw speaker interpolation after #23379 and the Speaker 379 live-notes incident.

Existing storage/routing expressions are compared by AST fingerprint, not a count:
removing one old expression never buys permission to introduce a different leak.
Display text must use utils.speakers.display / SpeakerNames. Wire fields remain IDs.
"""

from __future__ import annotations

import argparse
import ast
from collections import Counter
from pathlib import Path
import re
import subprocess
import sys
from pathlib import Path

AUTHORITY = {
'backend/utils/speaker_display.py',
'backend/utils/speakers/display.py',
'app/lib/ui/format/speaker_names.dart',
'app/lib/ui/format/diarization_label.dart',
}
RAW = re.compile(r'(?i)(?:\b(?:raw_)?speaker_?id\b|\bdiarization_?label\b|raw_for_transport|rawForTransport)')


def findings(path: str, source: str) -> list[tuple[str, int]]:
if path in AUTHORITY or '/tests/' in path or '/test/' in path:
return []
if path.startswith('backend/') and path.endswith('.py'):
tree = ast.parse(source)
hits = []
tainted = set()

def is_raw(expression):
return bool(RAW.search(ast.unparse(expression))) or any(
isinstance(part, ast.Name)
and part.id in tainted
or isinstance(part, ast.Attribute)
and part.attr in ('speaker', 'speaker_id', 'speakerId')
for part in ast.walk(expression)
)

# Local aliases do not turn storage identity into safe display text.
for _ in range(3):
for assignment in ast.walk(tree):
if (
isinstance(assignment, (ast.Assign, ast.AnnAssign))
and assignment.value is not None
and is_raw(assignment.value)
):
targets = assignment.targets if isinstance(assignment, ast.Assign) else [assignment.target]
for target in targets:
tainted.update(part.id for part in ast.walk(target) if isinstance(part, ast.Name))
for node in ast.walk(tree):
if (isinstance(node, ast.ImportFrom) and node.module == 'utils.speaker_display') or (
isinstance(node, ast.Import) and any(alias.name == 'utils.speaker_display' for alias in node.names)
):
hits.append((ast.dump(node, include_attributes=False), node.lineno))
expressions = []
speaker_template = False
if isinstance(node, ast.JoinedStr):
expressions = [v.value for v in node.values if isinstance(v, ast.FormattedValue)]
speaker_template = any(
isinstance(left, ast.Constant)
and isinstance(left.value, str)
and re.search(r'(?<![_A-Za-z])Speaker[ \t]*$', left.value)
and isinstance(right, ast.FormattedValue)
for left, right in zip(node.values, node.values[1:])
)
elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'format':
expressions = list(node.args) + [kw.value for kw in node.keywords]
speaker_template = (
isinstance(node.func.value, ast.Constant)
and isinstance(node.func.value.value, str)
and bool(re.search(r'(?<![_A-Za-z])Speaker[ \t]*\{', node.func.value.value))
)
elif (
isinstance(node, ast.BinOp)
and isinstance(node.op, ast.Mod)
and isinstance(node.left, ast.Constant)
and isinstance(node.left.value, str)
):
expressions = [node.right]
speaker_template = bool(re.search(r'(?<![_A-Za-z])Speaker[ \t]*%', node.left.value))
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
for literal, dynamic in ((node.left, node.right), (node.right, node.left)):
if (
isinstance(literal, ast.Constant)
and isinstance(literal.value, str)
and re.search(r'(?<![_A-Za-z])Speaker[ \t]*$', literal.value)
):
expressions = [dynamic]
speaker_template = True
if not expressions:
continue
rendered = ast.unparse(node)
# Generic aliases are caught when the template itself says Speaker/SPEAKER.
raw = any(is_raw(expr) for expr in expressions)
template = (
node.func.value if isinstance(node, ast.Call) else node.left if isinstance(node, ast.BinOp) else None
)
if isinstance(template, ast.Constant) and isinstance(template.value, str):
raw = raw or bool(re.search(r'(?:\{|%\()(?:raw_)?speaker_?id\b', template.value, re.I))
alias = re.search(r'[Ss][Pp][Ee][Aa][Kk][Ee][Rr][ _]', rendered) and any(
re.search(r'\b(?:sid|speaker|speaker_key|speaker_idx)\b', ast.unparse(expr)) for expr in expressions
)
if raw or alias or speaker_template:
hits.append((ast.dump(node, include_attributes=False), node.lineno))
return hits
if path.startswith('app/lib/') and path.endswith('.dart') and '/gen/' not in path and '/l10n/' not in path:
return [
(match.group(), source.count('\n', 0, match.start()) + 1)
for match in re.finditer(
r'''(?:['"][^'"\n]*\$(?:\{[^}\n]*(?:speakerId|rawForTransport)[^}\n]*\}|speakerId\b)[^'"\n]*['"])''',
source,
)
]
return []


def regressions(path: str, before: str, after: str) -> list[int]:
old = Counter(fingerprint for fingerprint, _ in findings(path, before))
lines = []
for fingerprint, line in findings(path, after):
if old[fingerprint]:
old[fingerprint] -= 1
else:
lines.append(line)
return lines


def _main_ratchet(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--changed-files', required=True)
parser.add_argument('--base', required=True)
args = parser.parse_args(argv)
failures = []
for path in Path(args.changed_files).read_text().splitlines():
file = Path(path)
if not file.is_file() or not (path.endswith('.py') or path.endswith('.dart')):
continue
result = subprocess.run(['git', 'show', f'{args.base}:{path}'], text=True, capture_output=True)
failures.extend(
f'{path}:{line}: raw speaker interpolation; use display_participants / SpeakerNames'
for line in regressions(path, result.stdout if result.returncode == 0 else '', file.read_text())
)
print('\n'.join(failures) if failures else 'OK: no new raw speaker interpolation')
return int(bool(failures))



REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
SCAN_ROOT = Path('backend')
Expand Down Expand Up @@ -147,7 +275,7 @@ def _self_test() -> int:
return 0


def main(argv: list[str] | None = None) -> int:
def _main_fullscan(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root', type=Path, default=REPOSITORY_ROOT)
parser.add_argument('--scan-root', type=Path, default=SCAN_ROOT)
Expand All @@ -158,12 +286,20 @@ def main(argv: list[str] | None = None) -> int:
violations = collect_violations(args.root.resolve(), args.scan_root)
if not violations:
return 0
print('FAIL: "Speaker" labels must not interpolate a raw id; use utils.speaker_display.SpeakerLabeler:', file=sys.stderr)
print('FAIL: "Speaker" labels must not interpolate a raw id; use utils.speakers.display.display_participants:', file=sys.stderr)
for path, errors in sorted(violations.items()):
for error in errors:
print(f' {path}:{error}', file=sys.stderr)
return 1



def main(argv: list[str] | None = None) -> int:
arguments = sys.argv[1:] if argv is None else argv
if '--changed-files' in arguments or '--base' in arguments:
return _main_ratchet(arguments)
return _main_fullscan(arguments)


if __name__ == '__main__':
raise SystemExit(main())
46 changes: 46 additions & 0 deletions .github/scripts/test_check_speaker_label_interpolation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest

from check_speaker_label_interpolation import regressions


class SpeakerPresentationRatchetTest(unittest.TestCase):
def test_python_interpolations(self):
for source in [
'x = f"Speaker {segment.speaker_id}"',
'x = f"{speaker_id}: hello"',
'x = "Speaker {}".format(segment["speaker_id"])',
'x = "Speaker %s" % raw_speaker_id',
'x = f"Speaker {speaker}"',
'x = "Speaker " + value',
'x = f"Speaker {display_key}"',
'sid = segment.speaker_id\nx = f"Who: {sid}"',
'x = f"{segment.speaker}"',
'x = "Speaker {speaker_id}".format(**segment)',
'x = "Speaker %(speaker_id)s" % segment',
]:
with self.subTest(source=source):
self.assertTrue(regressions('backend/utils/review.py', '', source))

def test_baseline_is_fingerprinted(self):
old = 'x = f"Speaker {speaker_id}"'
self.assertFalse(regressions('backend/utils/review.py', old, old))
self.assertFalse(regressions('backend/utils/review.py', old, 'x = names.label(row)'))
self.assertTrue(regressions('backend/utils/review.py', old, 'x = f"Who: {speaker_id}"'))

def test_authority_and_tests(self):
for path in ['backend/utils/speakers/display.py', 'backend/tests/unit/test_display.py']:
self.assertFalse(regressions(path, '', 'x = f"{speaker_id}"'))

def test_dart(self):
self.assertTrue(
regressions('backend/utils/new_renderer.py', '', 'from utils.speaker_display import SpeakerLabeler')
)
self.assertFalse(
regressions('backend/utils/new_renderer.py', '', 'from utils.speakers.display import display_participants')
)
self.assertTrue(regressions('app/lib/widgets/transcript.dart', '', 'Text("Speaker ${segment.speakerId}")'))
self.assertFalse(regressions('app/lib/widgets/transcript.dart', '', 'Text(names.forSegment(segment))'))


if __name__ == '__main__':
unittest.main()
7 changes: 4 additions & 3 deletions app/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# App (Flutter) — Operational Playbook
# App (Flutter)

Inherits [`../AGENTS.md`](../AGENTS.md); adds app-specific operational guidance.
UI rules: [docs/ux-contract.md](docs/ux-contract.md).
UI: [contract](docs/ux-contract.md).

Speakers: `SpeakerNames`/`DiarizationLabel` (`lib/ui/format/`); `EpisodePresentation.participants` counts names.

## Build Bootstrap

Expand Down
1 change: 1 addition & 0 deletions app/changelog/unreleased/live-speaker-epochs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"change":"Short unidentified live turns stay unnamed across reconnects instead of creating a new numbered speaker for each socket."}
1 change: 1 addition & 0 deletions app/changelog/unreleased/speaker-identity-guardrails.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"change":"Speaker labels stay consistent across transcripts and review questions, and excess unidentified voices are grouped as Others."}
2 changes: 2 additions & 0 deletions app/docs/ux-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,13 @@ participant lists, the speaker filter and every copied, shared or exported trans
- Everyone else is **Speaker N**, N dense per conversation (1, 2, 3… in order of first
appearance, skipping the owner and Omi). Never a raw id, never a gap. Naming a person does not
renumber the others. `TranscriptSegment.getDisplaySpeakerId` returns the same N.
- Live capture passes `deferShortVoices: true`: unnamed voices remain plain **Speaker** until five seconds of non-overlapping placed speech. Unplaced text cannot qualify. Number at qualification time so a short earlier voice cannot renumber an established voice; owner and known-person names still use their existing authority. This display floor does not identify or merge voices.
- When the server could not resolve speakers across the conversation (`speaker_resolution.status`
is `unavailable`), raw ids from different recordings are not comparable, so an unnamed voice is
the plain **Speaker** (`l10n.unnamedSpeakerLabel`), never "Speaker ?" and never a number, and the
transcript heading omits its speaker count. A saved transcript names each speaker once per turn
(consecutive lines from the same voice), so a change of voice is still visible.
- When diarization splits into more than eight unnamed voices, group tiny-evidence voices as **Others** and omit exact counts in conversation headers and the transcript heading. `DiarizationLabel` keeps storage ids out of display text.
- On-device overflow transcripts (`stt_provider=on_device:<engine>`) have one **unlabeled** voice: omit speaker names and the speaker count. Local engines never imply diarization or identity.
- The speaker filter is "Filter by speaker" (`l10n.filterBySpeaker`), never the loudspeaker string
`phoneSpeaker`.
Expand Down
3 changes: 2 additions & 1 deletion app/lib/l10n/app_ar.arb
Original file line number Diff line number Diff line change
Expand Up @@ -4423,5 +4423,6 @@
"recapInTheBackground": "في الخلفية",
"retentionOfferAccept": "ابقَ بخصم {percent}%",
"retentionOfferBody": "ابقَ وادفع {price} شهريًا لمدة {months} أشهر قادمة.",
"retentionOfferApplied": "تم. خصم {percent}% لمدة {months} أشهر قادمة."
"retentionOfferApplied": "تم. خصم {percent}% لمدة {months} أشهر قادمة.",
"othersSpeakerLabel": "آخرون"
}
3 changes: 2 additions & 1 deletion app/lib/l10n/app_be.arb
Original file line number Diff line number Diff line change
Expand Up @@ -11849,5 +11849,6 @@
"recapInTheBackground": "У фоне",
"retentionOfferAccept": "Застацца са зніжкай {percent}%",
"retentionOfferBody": "Застаньцеся і плаціце {price} у месяц на працягу {months} месяцаў.",
"retentionOfferApplied": "Гатова. Зніжка {percent}% на {months} месяцы."
"retentionOfferApplied": "Гатова. Зніжка {percent}% на {months} месяцы.",
"othersSpeakerLabel": "Іншыя"
}
3 changes: 2 additions & 1 deletion app/lib/l10n/app_bg.arb
Original file line number Diff line number Diff line change
Expand Up @@ -4423,5 +4423,6 @@
"recapInTheBackground": "На заден план",
"retentionOfferAccept": "Остани с {percent}% отстъпка",
"retentionOfferBody": "Остани и плащай {price} на месец за следващите {months} месеца.",
"retentionOfferApplied": "Готово. {percent}% отстъпка за следващите {months} месеца."
"retentionOfferApplied": "Готово. {percent}% отстъпка за следващите {months} месеца.",
"othersSpeakerLabel": "Други"
}
3 changes: 2 additions & 1 deletion app/lib/l10n/app_bn.arb
Original file line number Diff line number Diff line change
Expand Up @@ -11849,5 +11849,6 @@
"recapInTheBackground": "পটভূমিতে",
"retentionOfferAccept": "{percent}% ছাড়ে থাকুন",
"retentionOfferBody": "থাকুন এবং মাসে {price} দিন, পরের {months} মাস।",
"retentionOfferApplied": "হয়ে গেছে। {percent}% ছাড়, পরের {months} মাস।"
"retentionOfferApplied": "হয়ে গেছে। {percent}% ছাড়, পরের {months} মাস।",
"othersSpeakerLabel": "অন্যরা"
}
3 changes: 2 additions & 1 deletion app/lib/l10n/app_bs.arb
Original file line number Diff line number Diff line change
Expand Up @@ -11849,5 +11849,6 @@
"recapInTheBackground": "U pozadini",
"retentionOfferAccept": "Ostani uz {percent}% popusta",
"retentionOfferBody": "Ostani i plaćaj {price} mjesečno sljedećih {months} mjeseca.",
"retentionOfferApplied": "Gotovo. {percent}% popusta sljedećih {months} mjeseca."
"retentionOfferApplied": "Gotovo. {percent}% popusta sljedećih {months} mjeseca.",
"othersSpeakerLabel": "Ostali"
}
3 changes: 2 additions & 1 deletion app/lib/l10n/app_ca.arb
Original file line number Diff line number Diff line change
Expand Up @@ -4423,5 +4423,6 @@
"recapInTheBackground": "En segon pla",
"retentionOfferAccept": "Queda't amb un {percent}% de descompte",
"retentionOfferBody": "Queda't i paga {price} al mes durant els propers {months} mesos.",
"retentionOfferApplied": "Fet. {percent}% de descompte els propers {months} mesos."
"retentionOfferApplied": "Fet. {percent}% de descompte els propers {months} mesos.",
"othersSpeakerLabel": "Altres"
}
3 changes: 2 additions & 1 deletion app/lib/l10n/app_cs.arb
Original file line number Diff line number Diff line change
Expand Up @@ -4423,5 +4423,6 @@
"recapInTheBackground": "Na pozadí",
"retentionOfferAccept": "Zůstat se slevou {percent} %",
"retentionOfferBody": "Zůstaňte a plaťte {price} měsíčně po dobu dalších {months} měsíců.",
"retentionOfferApplied": "Hotovo. Sleva {percent} % na dalších {months} měsíců."
"retentionOfferApplied": "Hotovo. Sleva {percent} % na dalších {months} měsíců.",
"othersSpeakerLabel": "Ostatní"
}
3 changes: 2 additions & 1 deletion app/lib/l10n/app_da.arb
Original file line number Diff line number Diff line change
Expand Up @@ -4465,5 +4465,6 @@
"recapInTheBackground": "I baggrunden",
"retentionOfferAccept": "Bliv for {percent}% rabat",
"retentionOfferBody": "Bliv og betal {price} om måneden de næste {months} måneder.",
"retentionOfferApplied": "Færdig. {percent}% rabat de næste {months} måneder."
"retentionOfferApplied": "Færdig. {percent}% rabat de næste {months} måneder.",
"othersSpeakerLabel": "Andre"
}
Loading
Loading