From 53879ff776244cc58f797094fef6b0fb6d35e847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Sun, 6 Sep 2026 18:15:48 +0200 Subject: [PATCH] feat(mobile): version-gate feature flags by minimum app version In the mobile app, make the feature flags version-aware. A flag that a build does not understand must not change behaviour. Today the app reads a flag and acts on it whatever version wrote it, so an older build can act on a newer flag and a newer build can miss one that was renamed. Reproduce first: find where the app reads its feature flags, show what an older build does with a flag it does not know, and record what you saw. Then give every flag a minimum app version. The app applies a flag only when its own version is at or above that minimum, and falls back to the flag's default otherwise. Show the choice in the debug/settings surface that already lists the flags, so a tester can see which flags a build applies and why. Prove both paths live in the PR body: a flag the build applies, and a flag it skips because the build is too old. --- .../app-unlock-screen.test-helpers.tsx | 7 + .../feature-flags-section.mounted.test.tsx | 124 ++++++++ .../src/components/feature-flags-section.tsx | 61 ++++ .../preferences-screen.mounted.test.tsx | 49 ++++ .../src/components/preferences-screen.tsx | 4 + apps/mobile/src/i18n/locales/af.json | 7 +- apps/mobile/src/i18n/locales/am.json | 7 +- apps/mobile/src/i18n/locales/ar.json | 7 +- apps/mobile/src/i18n/locales/az.json | 7 +- apps/mobile/src/i18n/locales/be.json | 7 +- apps/mobile/src/i18n/locales/bg.json | 7 +- apps/mobile/src/i18n/locales/bn.json | 7 +- apps/mobile/src/i18n/locales/bs.json | 7 +- apps/mobile/src/i18n/locales/ca.json | 7 +- apps/mobile/src/i18n/locales/ckb.json | 7 +- apps/mobile/src/i18n/locales/cs.json | 7 +- apps/mobile/src/i18n/locales/cy.json | 7 +- apps/mobile/src/i18n/locales/da.json | 7 +- apps/mobile/src/i18n/locales/de.json | 7 +- apps/mobile/src/i18n/locales/el.json | 7 +- apps/mobile/src/i18n/locales/en.json | 7 +- apps/mobile/src/i18n/locales/es.json | 7 +- apps/mobile/src/i18n/locales/et.json | 7 +- apps/mobile/src/i18n/locales/eu.json | 7 +- apps/mobile/src/i18n/locales/fa.json | 7 +- apps/mobile/src/i18n/locales/fi.json | 7 +- apps/mobile/src/i18n/locales/fil.json | 7 +- apps/mobile/src/i18n/locales/fr.json | 7 +- apps/mobile/src/i18n/locales/ga.json | 7 +- apps/mobile/src/i18n/locales/gl.json | 7 +- apps/mobile/src/i18n/locales/gu.json | 7 +- apps/mobile/src/i18n/locales/ha.json | 7 +- apps/mobile/src/i18n/locales/he.json | 7 +- apps/mobile/src/i18n/locales/hi.json | 7 +- apps/mobile/src/i18n/locales/hr.json | 7 +- apps/mobile/src/i18n/locales/ht.json | 7 +- apps/mobile/src/i18n/locales/hu.json | 7 +- apps/mobile/src/i18n/locales/hy.json | 7 +- apps/mobile/src/i18n/locales/id.json | 7 +- apps/mobile/src/i18n/locales/ig.json | 7 +- apps/mobile/src/i18n/locales/is.json | 7 +- apps/mobile/src/i18n/locales/it.json | 7 +- apps/mobile/src/i18n/locales/ja.json | 7 +- apps/mobile/src/i18n/locales/ka.json | 7 +- apps/mobile/src/i18n/locales/kk.json | 7 +- apps/mobile/src/i18n/locales/km.json | 7 +- apps/mobile/src/i18n/locales/kn.json | 7 +- apps/mobile/src/i18n/locales/ko.json | 7 +- apps/mobile/src/i18n/locales/lo.json | 7 +- apps/mobile/src/i18n/locales/lt.json | 7 +- apps/mobile/src/i18n/locales/lv.json | 7 +- apps/mobile/src/i18n/locales/mg.json | 7 +- apps/mobile/src/i18n/locales/mi.json | 7 +- apps/mobile/src/i18n/locales/mk.json | 7 +- apps/mobile/src/i18n/locales/ml.json | 7 +- apps/mobile/src/i18n/locales/mn.json | 7 +- apps/mobile/src/i18n/locales/mr.json | 7 +- apps/mobile/src/i18n/locales/ms.json | 7 +- apps/mobile/src/i18n/locales/mt.json | 7 +- apps/mobile/src/i18n/locales/my.json | 7 +- apps/mobile/src/i18n/locales/nb.json | 7 +- apps/mobile/src/i18n/locales/ne.json | 7 +- apps/mobile/src/i18n/locales/nl.json | 7 +- apps/mobile/src/i18n/locales/om.json | 7 +- apps/mobile/src/i18n/locales/or.json | 7 +- apps/mobile/src/i18n/locales/pa.json | 7 +- apps/mobile/src/i18n/locales/pl.json | 7 +- apps/mobile/src/i18n/locales/ps.json | 7 +- apps/mobile/src/i18n/locales/pt-BR.json | 7 +- apps/mobile/src/i18n/locales/pt.json | 7 +- apps/mobile/src/i18n/locales/ro.json | 7 +- apps/mobile/src/i18n/locales/ru.json | 7 +- apps/mobile/src/i18n/locales/si.json | 7 +- apps/mobile/src/i18n/locales/sk.json | 7 +- apps/mobile/src/i18n/locales/sl.json | 7 +- apps/mobile/src/i18n/locales/so.json | 7 +- apps/mobile/src/i18n/locales/sq.json | 7 +- apps/mobile/src/i18n/locales/sr.json | 7 +- apps/mobile/src/i18n/locales/sv.json | 7 +- apps/mobile/src/i18n/locales/sw.json | 7 +- apps/mobile/src/i18n/locales/ta.json | 7 +- apps/mobile/src/i18n/locales/te.json | 7 +- apps/mobile/src/i18n/locales/th.json | 7 +- apps/mobile/src/i18n/locales/tr.json | 7 +- apps/mobile/src/i18n/locales/uk.json | 7 +- apps/mobile/src/i18n/locales/ur.json | 7 +- apps/mobile/src/i18n/locales/uz.json | 7 +- apps/mobile/src/i18n/locales/vi.json | 7 +- apps/mobile/src/i18n/locales/yo.json | 7 +- apps/mobile/src/i18n/locales/zh-Hans.json | 7 +- apps/mobile/src/i18n/locales/zh-Hant.json | 7 +- apps/mobile/src/i18n/locales/zu.json | 7 +- .../analytics/posthog.feature-flags.test.ts | 264 ++++++++++++++++++ apps/mobile/src/lib/analytics/posthog.ts | 118 +++++++- apps/mobile/src/lib/feature-flags.test.ts | 80 ++++++ apps/mobile/src/lib/feature-flags.ts | 79 ++++++ tools/i18n/check-catalogs.mjs | 7 + 97 files changed, 1312 insertions(+), 90 deletions(-) create mode 100644 apps/mobile/src/components/feature-flags-section.mounted.test.tsx create mode 100644 apps/mobile/src/components/feature-flags-section.tsx create mode 100644 apps/mobile/src/lib/analytics/posthog.feature-flags.test.ts create mode 100644 apps/mobile/src/lib/feature-flags.test.ts create mode 100644 apps/mobile/src/lib/feature-flags.ts diff --git a/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx b/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx index 5a55fd5afc..e9ef231608 100644 --- a/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx +++ b/apps/mobile/src/components/app-unlock-screen.test-helpers.tsx @@ -242,6 +242,13 @@ vi.mock('@/lib/hooks/use-trusted-hosts', () => ({ useTrustedHosts: () => ({ trustedHosts: [], hasLoaded: true }), })); vi.mock('@/lib/picker-bridge', () => ({ setLanguagePickerBridge: vi.fn() })); +// The preferences screen mounts the feature-flag debug surface, which reads +// PostHog flag statuses; the real module pulls in expo-application's native +// chain, which no mounted test loads. An empty registry keeps the section +// out of these scenes. +vi.mock('@/lib/analytics/posthog', () => ({ + useFeatureFlagStatuses: () => [], +})); function Draft() { const [value, onChange] = useState('saved draft'); diff --git a/apps/mobile/src/components/feature-flags-section.mounted.test.tsx b/apps/mobile/src/components/feature-flags-section.mounted.test.tsx new file mode 100644 index 0000000000..d66700ab65 --- /dev/null +++ b/apps/mobile/src/components/feature-flags-section.mounted.test.tsx @@ -0,0 +1,124 @@ +/* eslint-disable typescript-eslint/no-deprecated -- react-test-renderer is the DOM-free renderer used to mount React/RN trees under vitest (same pattern as preferences-screen.mounted.test.tsx) */ +import { createElement } from 'react'; +import { act, type ReactTestRenderer } from 'react-test-renderer'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import '@/i18n'; +import { FeatureFlagsSection } from '@/components/feature-flags-section'; +import { renderWithProviders } from '@/test/render-with-providers'; + +/** Statuses the mocked PostHog module reports; each test seeds this. */ +const posthog = vi.hoisted(() => ({ + statuses: [] as Record[], +})); +vi.mock('@/lib/analytics/posthog', () => ({ + useFeatureFlagStatuses: () => posthog.statuses, +})); + +vi.mock('react-native', () => ({ + View: 'View', +})); +vi.mock('@/components/ui/text', () => ({ Text: 'Text' })); + +let view: Awaited> | undefined = undefined; +async function flush(): Promise { + await act(async () => { + await vi.dynamicImportSettled(); + }); +} +async function mount(): Promise { + view = await renderWithProviders(createElement(FeatureFlagsSection)); + await flush(); + return view.renderer; +} + +/** All rendered Text strings, flattened (composed lines arrive as arrays). */ +function textLines(tree: ReactTestRenderer): string[] { + return tree.root + .findAll(node => typeof node.type === 'string' && (node.type as string) === 'Text') + .map(node => [node.props.children].flat().join('')); +} + +beforeEach(() => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + posthog.statuses = []; +}); +afterEach(() => { + view?.unmount(); + view = undefined; + vi.unstubAllGlobals(); +}); + +const applied = { + key: 'mobile-pr-review', + minAppVersion: '1.0.4', + defaultValue: true, + appVersion: '1.0.5', + applied: true, + value: true, + reason: 'applied', + loaded: true, +}; +const skipped = { + key: 'mobile-quick-chat', + minAppVersion: '1.0.6', + defaultValue: false, + appVersion: '1.0.5', + applied: false, + value: false, + reason: 'build-too-old', + loaded: true, +}; +const unloaded = { + key: 'mobile-pr-review', + minAppVersion: '1.0.4', + defaultValue: true, + appVersion: '1.0.8', + applied: false, + value: true, + reason: 'applied', + loaded: false, +}; + +describe('FeatureFlagsSection', () => { + it('lists every flag with the value the build acts on and why', async () => { + posthog.statuses = [applied, skipped]; + const tree = await mount(); + + const lines = textLines(tree); + expect(lines).toContain('Feature flags'); + expect(lines).toContain('mobile-pr-review'); + expect(lines).toContain('Enabled · remote · ≥ 1.0.4'); + expect(lines).toContain('mobile-quick-chat'); + expect(lines).toContain('Off · default · < 1.0.6'); + expect(lines).toContain('v1.0.5'); + }); + + it('marks a flag the build skips as default in use with the minimum version', async () => { + posthog.statuses = [skipped]; + const tree = await mount(); + + expect(textLines(tree)).toContain('Off · default · < 1.0.6'); + }); + + it('marks an applied flag as remote value in use', async () => { + posthog.statuses = [applied]; + const tree = await mount(); + + expect(textLines(tree)).toContain('Enabled · remote · ≥ 1.0.4'); + }); + + it('marks defaults in use while remote flags are not loaded yet', async () => { + posthog.statuses = [unloaded]; + const tree = await mount(); + + expect(textLines(tree)).toContain('Enabled · default · not loaded'); + }); + + it('renders nothing when the registry is empty', async () => { + posthog.statuses = []; + const tree = await mount(); + + expect(textLines(tree)).toEqual([]); + }); +}); diff --git a/apps/mobile/src/components/feature-flags-section.tsx b/apps/mobile/src/components/feature-flags-section.tsx new file mode 100644 index 0000000000..0664e62d0b --- /dev/null +++ b/apps/mobile/src/components/feature-flags-section.tsx @@ -0,0 +1,61 @@ +import { View } from 'react-native'; +import { useTranslation } from 'react-i18next'; + +import { Text } from '@/components/ui/text'; +import { type FeatureFlagStatus, useFeatureFlagStatuses } from '@/lib/analytics/posthog'; + +/** + * Debug surface for feature flags (Preferences). Lists every registered flag + * with what this build resolved it to and why, so a tester can see which + * flags the build applies and which it skips because the build predates the + * flag's minimum app version. Read-only: flags are controlled in PostHog. + * + * Each row reads ` · · `, e.g. + * `Enabled · remote · ≥ 1.0.4`: the value the UI acts on, whether it came + * from PostHog or the flag's default, and the gate that decided. The source + * and relation copy is technical notation (see the i18n allowlist); the + * translated part is the value word and the section header. + */ +function FlagRow({ status }: { status: FeatureFlagStatus }) { + const { t } = useTranslation(); + const value = status.value ? t('common.enabled') : t('common.off'); + let reason = t('preferences.featureFlagNotLoaded'); + if (status.loaded) { + reason = status.applied + ? t('preferences.featureFlagApplied', { min: status.minAppVersion }) + : t('preferences.featureFlagSkipped', { min: status.minAppVersion }); + } + return ( + + {status.key} + + {value} · {reason} + + + ); +} + +export function FeatureFlagsSection() { + const { t } = useTranslation(); + const statuses = useFeatureFlagStatuses(); + if (statuses.length === 0) { + return null; + } + return ( + + + {t('preferences.featureFlags')} + + + {statuses.map(status => ( + + ))} + + + {t('preferences.featureFlagsBuild', { + version: statuses[0]?.appVersion ?? '?', + })} + + + ); +} diff --git a/apps/mobile/src/components/preferences-screen.mounted.test.tsx b/apps/mobile/src/components/preferences-screen.mounted.test.tsx index 99fbefded1..a711ebd68f 100644 --- a/apps/mobile/src/components/preferences-screen.mounted.test.tsx +++ b/apps/mobile/src/components/preferences-screen.mounted.test.tsx @@ -1,4 +1,5 @@ /* eslint-disable typescript-eslint/no-deprecated -- react-test-renderer is the DOM-free renderer used to mount React/RN trees under vitest (same pattern as image-viewer-modal.mounted.test.tsx) */ +/* oxlint-disable max-lines -- this screen's mounted suite grew past 300 lines with the feature-flag debug surface cases */ import { type ElementType } from 'react'; import { act, type ReactTestRenderer } from 'react-test-renderer'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -10,6 +11,15 @@ import { renderWithProviders } from '@/test/render-with-providers'; const push = vi.hoisted(() => vi.fn()); const setLanguagePickerBridge = vi.hoisted(() => vi.fn()); +// The screen mounts the feature-flag debug surface, which reads PostHog flag +// statuses; seed an empty registry so the section stays out of these tests' +// snapshots unless a case opts in. +const posthog = vi.hoisted(() => ({ + statuses: [] as Record[], +})); +vi.mock('@/lib/analytics/posthog', () => ({ + useFeatureFlagStatuses: () => posthog.statuses, +})); const native = vi.hoisted(() => ({ hasHardwareAsync: vi.fn(), isEnrolledAsync: vi.fn(), @@ -135,6 +145,7 @@ async function mountPreferences(raw: string | null = null): Promise { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.resetAllMocks(); + posthog.statuses = []; storage.setItemAsync.mockImplementation(async (_key: string, value: string) => { await Promise.resolve(); storage.value = value; @@ -216,6 +227,44 @@ describe('PreferencesScreen Return-sends switch', () => { }); }); +describe('PreferencesScreen feature-flag debug surface', () => { + it('lists which flags the build applies and which it skips, with reasons', async () => { + posthog.statuses = [ + { + key: 'mobile-pr-review', + minAppVersion: '1.0.4', + defaultValue: true, + appVersion: '1.0.5', + applied: true, + value: true, + reason: 'applied', + loaded: true, + }, + { + key: 'mobile-quick-chat', + minAppVersion: '1.0.6', + defaultValue: false, + appVersion: '1.0.5', + applied: false, + value: false, + reason: 'build-too-old', + loaded: true, + }, + ]; + const renderer = await mountPreferences(); + + const lines = renderer.root + .findAll(node => typeof node.type === 'string' && (node.type as string) === 'Text') + .map(node => [node.props.children].flat().join('')); + expect(lines).toContain('Feature flags'); + expect(lines).toContain('mobile-pr-review'); + expect(lines).toContain('Enabled · remote · ≥ 1.0.4'); + expect(lines).toContain('mobile-quick-chat'); + expect(lines).toContain('Off · default · < 1.0.6'); + expect(lines).toContain('v1.0.5'); + }); +}); + function biometric(renderer: ReactTestRenderer) { return renderer.root.findByProps({ accessibilityLabel: 'Unlock with biometrics' }); } diff --git a/apps/mobile/src/components/preferences-screen.tsx b/apps/mobile/src/components/preferences-screen.tsx index d760d459d7..f9c6087d19 100644 --- a/apps/mobile/src/components/preferences-screen.tsx +++ b/apps/mobile/src/components/preferences-screen.tsx @@ -13,6 +13,7 @@ import { ActivityIndicator, Switch, View } from 'react-native'; import { useTranslation } from 'react-i18next'; import { AppUnlockFeedback } from '@/components/app-unlock-screen'; +import { FeatureFlagsSection } from '@/components/feature-flags-section'; import { ScreenHeader } from '@/components/screen-header'; import { TabScreenScrollView } from '@/components/tab-screen'; import { ConfigureRow } from '@/components/ui/configure-row'; @@ -188,6 +189,9 @@ export function PreferencesScreen() { /> + {/* Feature flags — debug surface: which flags this build applies and why. */} + + {/* Account */} diff --git a/apps/mobile/src/i18n/locales/af.json b/apps/mobile/src/i18n/locales/af.json index 286b0850a4..f78234a2ff 100644 --- a/apps/mobile/src/i18n/locales/af.json +++ b/apps/mobile/src/i18n/locales/af.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Return-toets stuur boodskap", "returnSendsMessageSubtitle": "Wanneer dit af is, voeg die Return-toets 'n nuwe reël in agente se boodskapinvoer in.", "biometricUnlock": "Ontsluit met biometrie", - "biometricUnlockSubtitle": "Ontsluit wanneer die toep begin en nadat dit vyf minute in die agtergrond was." + "biometricUnlockSubtitle": "Ontsluit wanneer die toep begin en nadat dit vyf minute in die agtergrond was.", + "featureFlags": "Funksie-vlae", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Trekversoek onbeskikbaar", diff --git a/apps/mobile/src/i18n/locales/am.json b/apps/mobile/src/i18n/locales/am.json index 36ac76d577..b10de77631 100644 --- a/apps/mobile/src/i18n/locales/am.json +++ b/apps/mobile/src/i18n/locales/am.json @@ -240,7 +240,12 @@ "returnSendsMessage": "የመመለሻ ቁልፍ መልእክት ይልካል", "returnSendsMessageSubtitle": "ሲጠፋ፣ የመመለሻ ቁልፍ በወኪል መልእክት ማስገቢያዎች ውስጥ አዲስ መስመር ያስገባል።", "biometricUnlock": "በባዮሜትሪክስ መክፈት", - "biometricUnlockSubtitle": "መተግበሪያው ሲጀመር እና ከበስተጀርባ ለአምስት ደቂቃዎች ከቆየ በኋላ መክፈት ያስፈልጋል።" + "biometricUnlockSubtitle": "መተግበሪያው ሲጀመር እና ከበስተጀርባ ለአምስት ደቂቃዎች ከቆየ በኋላ መክፈት ያስፈልጋል።", + "featureFlags": "የተግባር ባንዲራዎች", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "የማዋሃድ ጥያቄው አይገኝም", diff --git a/apps/mobile/src/i18n/locales/ar.json b/apps/mobile/src/i18n/locales/ar.json index ca7998ce9e..e202a116e5 100644 --- a/apps/mobile/src/i18n/locales/ar.json +++ b/apps/mobile/src/i18n/locales/ar.json @@ -240,7 +240,12 @@ "returnSendsMessage": "مفتاح الإدخال يرسل الرسالة", "returnSendsMessageSubtitle": "عند تعطيل هذا الخيار، يُدرج مفتاح الإدخال سطرًا جديدًا في حقول رسائل الوكلاء.", "biometricUnlock": "فتح القفل بالقياسات الحيوية", - "biometricUnlockSubtitle": "يلزم فتح القفل عند تشغيل التطبيق وبعد بقائه في الخلفية لمدة خمس دقائق." + "biometricUnlockSubtitle": "يلزم فتح القفل عند تشغيل التطبيق وبعد بقائه في الخلفية لمدة خمس دقائق.", + "featureFlags": "أعلام الميزات", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "الأنشطة المباشرة", diff --git a/apps/mobile/src/i18n/locales/az.json b/apps/mobile/src/i18n/locales/az.json index 6477004095..88738a0046 100644 --- a/apps/mobile/src/i18n/locales/az.json +++ b/apps/mobile/src/i18n/locales/az.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Sətirsonu düyməsi mesaj göndərir", "returnSendsMessageSubtitle": "Bu seçim deaktiv olduqda sətirsonu düyməsi agentin mesaj sahəsinə yeni sətir əlavə edir.", "biometricUnlock": "Biometrik məlumatlarla kilidi aç", - "biometricUnlockSubtitle": "Tətbiq başladıqda və arxa planda beş dəqiqə qaldıqdan sonra kilidi açmaq tələb olunur." + "biometricUnlockSubtitle": "Tətbiq başladıqda və arxa planda beş dəqiqə qaldıqdan sonra kilidi açmaq tələb olunur.", + "featureFlags": "Funksiya bayroqları", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Birləşdirmə sorğusu əlçatan deyil", diff --git a/apps/mobile/src/i18n/locales/be.json b/apps/mobile/src/i18n/locales/be.json index 1b138c1007..fe5ddc10db 100644 --- a/apps/mobile/src/i18n/locales/be.json +++ b/apps/mobile/src/i18n/locales/be.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Клавіша ўводу адпраўляе паведамленне", "returnSendsMessageSubtitle": "Калі налада выключаная, клавіша ўводу ўстаўляе новы радок у палі ўводу паведамленняў агентам.", "biometricUnlock": "Біяметрычная разблакіроўка", - "biometricUnlockSubtitle": "Разблакіроўка патрабуецца пры запуску праграмы і пасля пяці хвілін у фонавым рэжыме." + "biometricUnlockSubtitle": "Разблакіроўка патрабуецца пры запуску праграмы і пасля пяці хвілін у фонавым рэжыме.", + "featureFlags": "Сцягі функцый", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Запыт на зліццё недаступны", diff --git a/apps/mobile/src/i18n/locales/bg.json b/apps/mobile/src/i18n/locales/bg.json index a12a172a07..b0f6285775 100644 --- a/apps/mobile/src/i18n/locales/bg.json +++ b/apps/mobile/src/i18n/locales/bg.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Клавишът за нов ред изпраща съобщение", "returnSendsMessageSubtitle": "Когато настройката е изключена, клавишът за нов ред вмъква нов ред в полетата за въвеждане на съобщения до агенти.", "biometricUnlock": "Отключване с биометрични данни", - "biometricUnlockSubtitle": "Изисква се отключване при стартиране и след пет минути във фонов режим." + "biometricUnlockSubtitle": "Изисква се отключване при стартиране и след пет минути във фонов режим.", + "featureFlags": "Флагове на функции", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Недостъпна заявка за обединяване", diff --git a/apps/mobile/src/i18n/locales/bn.json b/apps/mobile/src/i18n/locales/bn.json index a746383e78..b6f55f8c0a 100644 --- a/apps/mobile/src/i18n/locales/bn.json +++ b/apps/mobile/src/i18n/locales/bn.json @@ -240,7 +240,12 @@ "returnSendsMessage": "রিটার্ন কী বার্তা পাঠায়", "returnSendsMessageSubtitle": "বন্ধ থাকলে, রিটার্ন কী এজেন্টের বার্তা লেখার ঘরে নতুন লাইন যোগ করে।", "biometricUnlock": "বায়োমেট্রিক আনলক", - "biometricUnlockSubtitle": "অ্যাপ চালু করার সময় এবং ব্যাকগ্রাউন্ডে পাঁচ মিনিট থাকার পর আনলক করতে হবে।" + "biometricUnlockSubtitle": "অ্যাপ চালু করার সময় এবং ব্যাকগ্রাউন্ডে পাঁচ মিনিট থাকার পর আনলক করতে হবে।", + "featureFlags": "ফিচার ফ্ল্যাগ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "পুল রিকোয়েস্ট পাওয়া যাচ্ছে না", diff --git a/apps/mobile/src/i18n/locales/bs.json b/apps/mobile/src/i18n/locales/bs.json index ec78daca02..df0878aa69 100644 --- a/apps/mobile/src/i18n/locales/bs.json +++ b/apps/mobile/src/i18n/locales/bs.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Tipka za novi red šalje poruku", "returnSendsMessageSubtitle": "Kada je isključeno, tipka za novi red umeće novi red u polja za unos poruke agentima.", "biometricUnlock": "Otključavanje biometrijom", - "biometricUnlockSubtitle": "Otključavanje je obavezno pri pokretanju aplikacije i nakon pet minuta provedenih u pozadini." + "biometricUnlockSubtitle": "Otključavanje je obavezno pri pokretanju aplikacije i nakon pet minuta provedenih u pozadini.", + "featureFlags": "Zastavice funkcija", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Zahtjev za spajanje nije dostupan", diff --git a/apps/mobile/src/i18n/locales/ca.json b/apps/mobile/src/i18n/locales/ca.json index 8646dddfdf..b5ee23d3e4 100644 --- a/apps/mobile/src/i18n/locales/ca.json +++ b/apps/mobile/src/i18n/locales/ca.json @@ -240,7 +240,12 @@ "returnSendsMessage": "La tecla de retorn envia el missatge", "returnSendsMessageSubtitle": "Quan està desactivat, la tecla de retorn insereix un salt de línia als camps de missatge dels agents.", "biometricUnlock": "Desbloqueja amb biometria", - "biometricUnlockSubtitle": "Cal desbloquejar l'aplicació en iniciar-la i després de cinc minuts en segon pla." + "biometricUnlockSubtitle": "Cal desbloquejar l'aplicació en iniciar-la i després de cinc minuts en segon pla.", + "featureFlags": "Indicadors de funcions", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Sol·licitud d'integració no disponible", diff --git a/apps/mobile/src/i18n/locales/ckb.json b/apps/mobile/src/i18n/locales/ckb.json index ecd8a6c111..25115f0b4c 100644 --- a/apps/mobile/src/i18n/locales/ckb.json +++ b/apps/mobile/src/i18n/locales/ckb.json @@ -240,7 +240,12 @@ "returnSendsMessage": "دوگمەی گەڕانەوە پەیام دەنێرێت", "returnSendsMessageSubtitle": "کاتێک ئەم بژاردەیە ناچالاکە، دوگمەی گەڕانەوە دێڕێکی نوێ لە خانەکانی نووسینی پەیام بۆ ئەجێنت تێدەخات.", "biometricUnlock": "کردنەوەی قوفڵ بە بایۆمەتریک", - "biometricUnlockSubtitle": "لە کاتی دەستپێکردنی ئەپەکە و دوای پێنج خولەک مانەوەی لە پاشبنەمادا، کردنەوەی قوفڵ پێویستە." + "biometricUnlockSubtitle": "لە کاتی دەستپێکردنی ئەپەکە و دوای پێنج خولەک مانەوەی لە پاشبنەمادا، کردنەوەی قوفڵ پێویستە.", + "featureFlags": "ئاڵای تایبەتمەندییەکان", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "داواکاری ڕاکێشان بەردەست نییە", diff --git a/apps/mobile/src/i18n/locales/cs.json b/apps/mobile/src/i18n/locales/cs.json index 5edfa12109..e327166782 100644 --- a/apps/mobile/src/i18n/locales/cs.json +++ b/apps/mobile/src/i18n/locales/cs.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Klávesa Enter odesílá zprávu", "returnSendsMessageSubtitle": "Při vypnutí této volby klávesa Enter vloží nový řádek do pole pro zprávu agentovi.", "biometricUnlock": "Biometrické odemykání", - "biometricUnlockSubtitle": "Vyžaduje odemknutí při spuštění aplikace a po pěti minutách na pozadí." + "biometricUnlockSubtitle": "Vyžaduje odemknutí při spuštění aplikace a po pěti minutách na pozadí.", + "featureFlags": "Příznaky funkcí", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "PR není dostupný", diff --git a/apps/mobile/src/i18n/locales/cy.json b/apps/mobile/src/i18n/locales/cy.json index 571519fd8a..bc18651426 100644 --- a/apps/mobile/src/i18n/locales/cy.json +++ b/apps/mobile/src/i18n/locales/cy.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Mae'r fysell Dychwelyd yn anfon neges", "returnSendsMessageSubtitle": "Pan fydd yr opsiwn hwn wedi'i ddiffodd, mae'r fysell Dychwelyd yn mewnosod llinell newydd ym mlychau negeseuon yr asiantau.", "biometricUnlock": "Datgloi â biometreg", - "biometricUnlockSubtitle": "Rhaid datgloi wrth gychwyn yr ap ac ar ôl pum munud yn y cefndir." + "biometricUnlockSubtitle": "Rhaid datgloi wrth gychwyn yr ap ac ar ôl pum munud yn y cefndir.", + "featureFlags": "Baneri nodweddion", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Cais tynnu ddim ar gael", diff --git a/apps/mobile/src/i18n/locales/da.json b/apps/mobile/src/i18n/locales/da.json index 729d83840c..b235bfb2f8 100644 --- a/apps/mobile/src/i18n/locales/da.json +++ b/apps/mobile/src/i18n/locales/da.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Returtasten sender beskeden", "returnSendsMessageSubtitle": "Når dette er slået fra, indsætter returtasten et linjeskift i agenternes beskedfelter.", "biometricUnlock": "Lås op med biometri", - "biometricUnlockSubtitle": "Lås op ved opstart, og når appen har været i baggrunden i fem minutter." + "biometricUnlockSubtitle": "Lås op ved opstart, og når appen har været i baggrunden i fem minutter.", + "featureFlags": "Funktionsflag", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Pull request ikke tilgængelig", diff --git a/apps/mobile/src/i18n/locales/de.json b/apps/mobile/src/i18n/locales/de.json index a59ea3d1ee..a4efcb4c71 100644 --- a/apps/mobile/src/i18n/locales/de.json +++ b/apps/mobile/src/i18n/locales/de.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Eingabetaste sendet Nachricht", "returnSendsMessageSubtitle": "Ist diese Option deaktiviert, fügt die Eingabetaste im Eingabefeld für Agenten einen Zeilenumbruch ein.", "biometricUnlock": "Mit Biometrie entsperren", - "biometricUnlockSubtitle": "Beim Start und nach fünf Minuten im Hintergrund ist das Entsperren erforderlich." + "biometricUnlockSubtitle": "Beim Start und nach fünf Minuten im Hintergrund ist das Entsperren erforderlich.", + "featureFlags": "Feature-Flags", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "Live-Aktivitäten", diff --git a/apps/mobile/src/i18n/locales/el.json b/apps/mobile/src/i18n/locales/el.json index f07cd65fc9..509d08f5ae 100644 --- a/apps/mobile/src/i18n/locales/el.json +++ b/apps/mobile/src/i18n/locales/el.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Αποστολή μηνύματος με το πλήκτρο Enter", "returnSendsMessageSubtitle": "Όταν η επιλογή είναι ανενεργή, το πλήκτρο Enter εισάγει νέα γραμμή στα πεδία μηνυμάτων των πρακτόρων.", "biometricUnlock": "Ξεκλείδωμα με βιομετρικά στοιχεία", - "biometricUnlockSubtitle": "Απαιτείται ξεκλείδωμα κατά την εκκίνηση της εφαρμογής και αφού παραμείνει στο παρασκήνιο για πέντε λεπτά." + "biometricUnlockSubtitle": "Απαιτείται ξεκλείδωμα κατά την εκκίνηση της εφαρμογής και αφού παραμείνει στο παρασκήνιο για πέντε λεπτά.", + "featureFlags": "Σημαίες λειτουργιών", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Μη διαθέσιμο αίτημα ενσωμάτωσης", diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 45c2163436..44c798cff9 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -240,7 +240,12 @@ "prReviewAttribution": "Add app attribution to PR reviews", "prReviewAttributionSubtitle": "Append a Reviewed via Kilo footer when you submit a review.", "returnSendsMessage": "Return key sends message", - "returnSendsMessageSubtitle": "When off, Return inserts a newline in agent composers." + "returnSendsMessageSubtitle": "When off, Return inserts a newline in agent composers.", + "featureFlags": "Feature flags", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Pull request unavailable", diff --git a/apps/mobile/src/i18n/locales/es.json b/apps/mobile/src/i18n/locales/es.json index 8e10557c5e..4773fa14dd 100644 --- a/apps/mobile/src/i18n/locales/es.json +++ b/apps/mobile/src/i18n/locales/es.json @@ -240,7 +240,12 @@ "returnSendsMessage": "La tecla Intro envía el mensaje", "returnSendsMessageSubtitle": "Si está desactivado, Intro inserta un salto de línea en los campos de mensaje del agente.", "biometricUnlock": "Desbloqueo biométrico", - "biometricUnlockSubtitle": "Se requiere desbloquear la aplicación al iniciarla y después de cinco minutos en segundo plano." + "biometricUnlockSubtitle": "Se requiere desbloquear la aplicación al iniciarla y después de cinco minutos en segundo plano.", + "featureFlags": "Indicadores de funciones", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "channel": { diff --git a/apps/mobile/src/i18n/locales/et.json b/apps/mobile/src/i18n/locales/et.json index e4819e35fe..df1c9145dd 100644 --- a/apps/mobile/src/i18n/locales/et.json +++ b/apps/mobile/src/i18n/locales/et.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Sisestusklahv saadab sõnumi", "returnSendsMessageSubtitle": "Kui see on välja lülitatud, lisab sisestusklahv agentide sõnumi sisestusväljadel reavahetuse.", "biometricUnlock": "Biomeetriline lukust avamine", - "biometricUnlockSubtitle": "Rakendus tuleb lukust avada käivitamisel ja pärast viit minutit taustal olemist." + "biometricUnlockSubtitle": "Rakendus tuleb lukust avada käivitamisel ja pärast viit minutit taustal olemist.", + "featureFlags": "Funktsioonilipud", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Tõmbetaotlus pole saadaval", diff --git a/apps/mobile/src/i18n/locales/eu.json b/apps/mobile/src/i18n/locales/eu.json index 135d373cd1..7c3d2880e6 100644 --- a/apps/mobile/src/i18n/locales/eu.json +++ b/apps/mobile/src/i18n/locales/eu.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Sartu teklak mezua bidaltzen du", "returnSendsMessageSubtitle": "Desgaituta dagoenean, Sartu teklak lerro-jauzia txertatzen du agenteen mezu-eremuetan.", "biometricUnlock": "Desblokeatu biometriarekin", - "biometricUnlockSubtitle": "Desblokeatu aplikazioa abiaraztean eta atzeko planoan bost minutu igaro ondoren." + "biometricUnlockSubtitle": "Desblokeatu aplikazioa abiaraztean eta atzeko planoan bost minutu igaro ondoren.", + "featureFlags": "Funtzio-bandera", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Bateratze-eskaera ez dago eskuragarri", diff --git a/apps/mobile/src/i18n/locales/fa.json b/apps/mobile/src/i18n/locales/fa.json index 950576699a..d2f72de9ff 100644 --- a/apps/mobile/src/i18n/locales/fa.json +++ b/apps/mobile/src/i18n/locales/fa.json @@ -240,7 +240,12 @@ "returnSendsMessage": "ارسال پیام با کلید اینتر", "returnSendsMessageSubtitle": "اگر این گزینه غیرفعال باشد، کلید اینتر در کادر پیام عامل یک خط جدید ایجاد می‌کند.", "biometricUnlock": "باز کردن قفل با احراز هویت زیست‌سنجی", - "biometricUnlockSubtitle": "هنگام راه‌اندازی برنامه و پس از پنج دقیقه ماندن آن در پس‌زمینه، باز کردن قفل الزامی است." + "biometricUnlockSubtitle": "هنگام راه‌اندازی برنامه و پس از پنج دقیقه ماندن آن در پس‌زمینه، باز کردن قفل الزامی است.", + "featureFlags": "پرچم‌های ویژگی", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "درخواست ادغام در دسترس نیست", diff --git a/apps/mobile/src/i18n/locales/fi.json b/apps/mobile/src/i18n/locales/fi.json index dacd5e04ff..6f8507f579 100644 --- a/apps/mobile/src/i18n/locales/fi.json +++ b/apps/mobile/src/i18n/locales/fi.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Enter-näppäin lähettää viestin", "returnSendsMessageSubtitle": "Kun asetus on pois päältä, Enter lisää rivinvaihdon agenttien viestikenttiin.", "biometricUnlock": "Avaa lukitus biometrisellä tunnistuksella", - "biometricUnlockSubtitle": "Lukitus on avattava sovelluksen käynnistyessä ja sen oltua taustalla viisi minuuttia." + "biometricUnlockSubtitle": "Lukitus on avattava sovelluksen käynnistyessä ja sen oltua taustalla viisi minuuttia.", + "featureFlags": "Ominaisuusliput", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Muutospyyntö ei ole käytettävissä", diff --git a/apps/mobile/src/i18n/locales/fil.json b/apps/mobile/src/i18n/locales/fil.json index c8bd2187c1..6c5d534ae6 100644 --- a/apps/mobile/src/i18n/locales/fil.json +++ b/apps/mobile/src/i18n/locales/fil.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Nagpapadala ng mensahe ang teklang Return", "returnSendsMessageSubtitle": "Kapag naka-off, naglalagay ang Return ng bagong linya sa mga sulatan ng mensahe para sa ahente.", "biometricUnlock": "I-unlock gamit ang biometriko", - "biometricUnlockSubtitle": "I-unlock ang app sa pagbukas at matapos ang limang minuto sa background." + "biometricUnlockSubtitle": "I-unlock ang app sa pagbukas at matapos ang limang minuto sa background.", + "featureFlags": "Mga feature flag", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Hindi mabuksan ang pull request", diff --git a/apps/mobile/src/i18n/locales/fr.json b/apps/mobile/src/i18n/locales/fr.json index bc7bd2a4e9..f46bc6d374 100644 --- a/apps/mobile/src/i18n/locales/fr.json +++ b/apps/mobile/src/i18n/locales/fr.json @@ -1982,7 +1982,12 @@ "returnSendsMessage": "La touche Entrée envoie le message", "returnSendsMessageSubtitle": "Si cette option est désactivée, la touche Entrée insère un saut de ligne dans les champs de saisie des agents.", "biometricUnlock": "Déverrouillage biométrique", - "biometricUnlockSubtitle": "Déverrouillage requis au lancement et après cinq minutes en arrière-plan." + "biometricUnlockSubtitle": "Déverrouillage requis au lancement et après cinq minutes en arrière-plan.", + "featureFlags": "Indicateurs de fonctionnalités", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "addCredits": { "cta": "Ajouter des crédits", diff --git a/apps/mobile/src/i18n/locales/ga.json b/apps/mobile/src/i18n/locales/ga.json index d0500c53fe..4d93e87383 100644 --- a/apps/mobile/src/i18n/locales/ga.json +++ b/apps/mobile/src/i18n/locales/ga.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Seolann an eochair aisfhillte teachtaireacht", "returnSendsMessageSubtitle": "Nuair atá sé seo as, cuireann an eochair aisfhillte líne nua isteach i mboscaí teachtaireachta na ngníomhairí.", "biometricUnlock": "Díghlasáil le bithmhéadracht", - "biometricUnlockSubtitle": "Ní mór an aip a dhíghlasáil nuair a sheoltar í agus tar éis di cúig nóiméad a chaitheamh sa chúlra." + "biometricUnlockSubtitle": "Ní mór an aip a dhíghlasáil nuair a sheoltar í agus tar éis di cúig nóiméad a chaitheamh sa chúlra.", + "featureFlags": "Bratacha gnéithe", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Iarratas tarraingthe gan fáil", diff --git a/apps/mobile/src/i18n/locales/gl.json b/apps/mobile/src/i18n/locales/gl.json index 13e58a59ba..475167b43e 100644 --- a/apps/mobile/src/i18n/locales/gl.json +++ b/apps/mobile/src/i18n/locales/gl.json @@ -240,7 +240,12 @@ "returnSendsMessage": "A tecla Intro envía a mensaxe", "returnSendsMessageSubtitle": "Cando está desactivado, Intro insire un salto de liña nos campos de mensaxe dos axentes.", "biometricUnlock": "Desbloquear con biometría", - "biometricUnlockSubtitle": "Solicitar o desbloqueo ao abrir a aplicación e despois de cinco minutos en segundo plano." + "biometricUnlockSubtitle": "Solicitar o desbloqueo ao abrir a aplicación e despois de cinco minutos en segundo plano.", + "featureFlags": "Bandeiras de funcións", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Solicitude de integración non dispoñible", diff --git a/apps/mobile/src/i18n/locales/gu.json b/apps/mobile/src/i18n/locales/gu.json index cc0ea227c8..38b356306a 100644 --- a/apps/mobile/src/i18n/locales/gu.json +++ b/apps/mobile/src/i18n/locales/gu.json @@ -240,7 +240,12 @@ "returnSendsMessage": "રિટર્ન કી સંદેશ મોકલે છે", "returnSendsMessageSubtitle": "આ બંધ હોય ત્યારે રિટર્ન કી એજન્ટના સંદેશના ખાનામાં નવી પંક્તિ ઉમેરે છે.", "biometricUnlock": "બાયોમેટ્રિક્સથી અનલૉક કરો", - "biometricUnlockSubtitle": "એપ શરૂ કરતી વખતે અને પૃષ્ઠભૂમિમાં પાંચ મિનિટ રહ્યા પછી અનલૉક કરવું જરૂરી છે." + "biometricUnlockSubtitle": "એપ શરૂ કરતી વખતે અને પૃષ્ઠભૂમિમાં પાંચ મિનિટ રહ્યા પછી અનલૉક કરવું જરૂરી છે.", + "featureFlags": "ફીચર ફ્લેગ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "પુલ રિક્વેસ્ટ ઉપલબ્ધ નથી", diff --git a/apps/mobile/src/i18n/locales/ha.json b/apps/mobile/src/i18n/locales/ha.json index 262e3ac410..e725096e31 100644 --- a/apps/mobile/src/i18n/locales/ha.json +++ b/apps/mobile/src/i18n/locales/ha.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Maɓallin sabon layi yana aika saƙo", "returnSendsMessageSubtitle": "Idan an kashe wannan, maɓallin sabon layi yana saka sabon layi a wuraren rubuta saƙo ga wakilai.", "biometricUnlock": "Buɗe kulle da bayanan halittar jiki", - "biometricUnlockSubtitle": "Ana buƙatar buɗe kulle lokacin fara manhajar da bayan ta yi minti biyar a baya." + "biometricUnlockSubtitle": "Ana buƙatar buɗe kulle lokacin fara manhajar da bayan ta yi minti biyar a baya.", + "featureFlags": "Tutocin sifofi", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Buƙatar haɗawa ba ta samuwa", diff --git a/apps/mobile/src/i18n/locales/he.json b/apps/mobile/src/i18n/locales/he.json index 6d18afc406..b2adf75c06 100644 --- a/apps/mobile/src/i18n/locales/he.json +++ b/apps/mobile/src/i18n/locales/he.json @@ -240,7 +240,12 @@ "returnSendsMessage": "מקש אנטר שולח הודעה", "returnSendsMessageSubtitle": "כשהאפשרות כבויה, מקש אנטר מוסיף שורה חדשה בשדות ההודעה של הסוכנים.", "biometricUnlock": "ביטול נעילה באמצעות זיהוי ביומטרי", - "biometricUnlockSubtitle": "נדרש לבטל את הנעילה בעת הפעלת האפליקציה ולאחר חמש דקות ברקע." + "biometricUnlockSubtitle": "נדרש לבטל את הנעילה בעת הפעלת האפליקציה ולאחר חמש דקות ברקע.", + "featureFlags": "דגלי תכונות", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "פעילויות בזמן אמת", diff --git a/apps/mobile/src/i18n/locales/hi.json b/apps/mobile/src/i18n/locales/hi.json index ff34f0d476..e4baf64fc3 100644 --- a/apps/mobile/src/i18n/locales/hi.json +++ b/apps/mobile/src/i18n/locales/hi.json @@ -240,7 +240,12 @@ "returnSendsMessage": "रिटर्न की दबाने पर संदेश भेजें", "returnSendsMessageSubtitle": "यह विकल्प बंद होने पर रिटर्न की दबाने से एजेंट के संदेश लिखने के बॉक्स में नई पंक्ति जुड़ती है।", "biometricUnlock": "बायोमेट्रिक्स से अनलॉक करें", - "biometricUnlockSubtitle": "ऐप शुरू करने पर और पृष्ठभूमि में पाँच मिनट रहने के बाद अनलॉक करना आवश्यक है।" + "biometricUnlockSubtitle": "ऐप शुरू करने पर और पृष्ठभूमि में पाँच मिनट रहने के बाद अनलॉक करना आवश्यक है।", + "featureFlags": "फ़ीचर फ़्लैग", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "लाइव गतिविधियाँ", diff --git a/apps/mobile/src/i18n/locales/hr.json b/apps/mobile/src/i18n/locales/hr.json index 2eead89508..0ec70fbe94 100644 --- a/apps/mobile/src/i18n/locales/hr.json +++ b/apps/mobile/src/i18n/locales/hr.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Tipka za novi redak šalje poruku", "returnSendsMessageSubtitle": "Kad je isključeno, tipka za novi redak umeće novi redak u polja za unos poruke agentima.", "biometricUnlock": "Otključavanje biometrijom", - "biometricUnlockSubtitle": "Otključavanje je potrebno pri pokretanju aplikacije i nakon pet minuta u pozadini." + "biometricUnlockSubtitle": "Otključavanje je potrebno pri pokretanju aplikacije i nakon pet minuta u pozadini.", + "featureFlags": "Zastavice značajki", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Zahtjev za spajanje nije dostupan", diff --git a/apps/mobile/src/i18n/locales/ht.json b/apps/mobile/src/i18n/locales/ht.json index 3c23c3b25a..ebb9690b0c 100644 --- a/apps/mobile/src/i18n/locales/ht.json +++ b/apps/mobile/src/i18n/locales/ht.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Bouton retou a voye mesaj", "returnSendsMessageSubtitle": "Lè opsyon sa a dezaktive, bouton retou a ajoute yon nouvo liy nan chan mesaj ajan yo.", "biometricUnlock": "Debloke ak byometri", - "biometricUnlockSubtitle": "Debloke aplikasyon an lè li kòmanse ak apre li pase senk minit an aryèplan." + "biometricUnlockSubtitle": "Debloke aplikasyon an lè li kòmanse ak apre li pase senk minit an aryèplan.", + "featureFlags": "Drapo fonksyon", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Demann fizyon an pa disponib", diff --git a/apps/mobile/src/i18n/locales/hu.json b/apps/mobile/src/i18n/locales/hu.json index efef5fe362..71497ebd52 100644 --- a/apps/mobile/src/i18n/locales/hu.json +++ b/apps/mobile/src/i18n/locales/hu.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Az Enter billentyű elküldi az üzenetet", "returnSendsMessageSubtitle": "Kikapcsolva az Enter új sort szúr be az ügynökök üzenetbeviteli mezőibe.", "biometricUnlock": "Feloldás biometrikus azonosítással", - "biometricUnlockSubtitle": "Az alkalmazás indításakor és öt perc háttérben töltött idő után feloldás szükséges." + "biometricUnlockSubtitle": "Az alkalmazás indításakor és öt perc háttérben töltött idő után feloldás szükséges.", + "featureFlags": "Funkciójelzők", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "A módosítási kérelem nem érhető el", diff --git a/apps/mobile/src/i18n/locales/hy.json b/apps/mobile/src/i18n/locales/hy.json index db557ba6d2..0e0be5e83a 100644 --- a/apps/mobile/src/i18n/locales/hy.json +++ b/apps/mobile/src/i18n/locales/hy.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Մուտքի ստեղնն ուղարկում է հաղորդագրությունը", "returnSendsMessageSubtitle": "Երբ անջատված է, մուտքի ստեղնը նոր տող է ավելացնում գործակալների հաղորդագրության մուտքագրման դաշտերում։", "biometricUnlock": "Ապակողպել կենսաչափական տվյալներով", - "biometricUnlockSubtitle": "Հավելվածը գործարկելիս և ֆոնային ռեժիմում հինգ րոպե մնալուց հետո պահանջվում է ապակողպել այն։" + "biometricUnlockSubtitle": "Հավելվածը գործարկելիս և ֆոնային ռեժիմում հինգ րոպե մնալուց հետո պահանջվում է ապակողպել այն։", + "featureFlags": "Ֆունկցիաների դրոշներ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Միավորման հարցումը հասանելի չէ", diff --git a/apps/mobile/src/i18n/locales/id.json b/apps/mobile/src/i18n/locales/id.json index 3de086c2c5..f4ca731e68 100644 --- a/apps/mobile/src/i18n/locales/id.json +++ b/apps/mobile/src/i18n/locales/id.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Tombol Enter mengirim pesan", "returnSendsMessageSubtitle": "Saat nonaktif, Enter menyisipkan baris baru di kolom pesan agen.", "biometricUnlock": "Buka kunci dengan biometrik", - "biometricUnlockSubtitle": "Minta autentikasi biometrik saat aplikasi dibuka dan setelah lima menit di latar belakang." + "biometricUnlockSubtitle": "Minta autentikasi biometrik saat aplikasi dibuka dan setelah lima menit di latar belakang.", + "featureFlags": "Bendera fitur", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "Aktivitas langsung", diff --git a/apps/mobile/src/i18n/locales/ig.json b/apps/mobile/src/i18n/locales/ig.json index d31b71aa27..f2e0236260 100644 --- a/apps/mobile/src/i18n/locales/ig.json +++ b/apps/mobile/src/i18n/locales/ig.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Igodo nloghachi na-eziga ozi", "returnSendsMessageSubtitle": "Mgbe agbanyụrụ ya, igodo nloghachi na-etinye ahịrị ọhụrụ n'ebe ide ozi ndị nnọchi anya.", "biometricUnlock": "Mepee mkpọchi site na njirimara ahụ", - "biometricUnlockSubtitle": "Wepụ mkpọchi mgbe ngwa malitere na mgbe ọ nọrọ nkeji ise n'azụ." + "biometricUnlockSubtitle": "Wepụ mkpọchi mgbe ngwa malitere na mgbe ọ nọrọ nkeji ise n'azụ.", + "featureFlags": "Ọkọ atụmatụ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Arịrịọ njikọta adịghị", diff --git a/apps/mobile/src/i18n/locales/is.json b/apps/mobile/src/i18n/locales/is.json index d0cb73eb26..1f8185ae6d 100644 --- a/apps/mobile/src/i18n/locales/is.json +++ b/apps/mobile/src/i18n/locales/is.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Færsluhnappur sendir skilaboð", "returnSendsMessageSubtitle": "Þegar slökkt er á þessu setur færsluhnappurinn inn línuskil í ritlum fulltrúa.", "biometricUnlock": "Aflæsing með lífkennum", - "biometricUnlockSubtitle": "Krefjast aflæsingar við ræsingu og eftir fimm mínútur í bakgrunni." + "biometricUnlockSubtitle": "Krefjast aflæsingar við ræsingu og eftir fimm mínútur í bakgrunni.", + "featureFlags": "Eiginleikafánar", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Sameiningarbeiðni ekki tiltæk", diff --git a/apps/mobile/src/i18n/locales/it.json b/apps/mobile/src/i18n/locales/it.json index 5694d74ab6..5b94f5fe62 100644 --- a/apps/mobile/src/i18n/locales/it.json +++ b/apps/mobile/src/i18n/locales/it.json @@ -2046,7 +2046,12 @@ "returnSendsMessage": "Il tasto Invio invia il messaggio", "returnSendsMessageSubtitle": "Se disattivato, il tasto Invio inserisce un'interruzione di riga nei campi dei messaggi agli agenti.", "biometricUnlock": "Sblocco biometrico", - "biometricUnlockSubtitle": "Lo sblocco è richiesto all'avvio dell'app e dopo cinque minuti in secondo piano." + "biometricUnlockSubtitle": "Lo sblocco è richiesto all'avvio dell'app e dopo cinque minuti in secondo piano.", + "featureFlags": "Flag di funzionalità", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "addCredits": { "cta": "Aggiungi crediti", diff --git a/apps/mobile/src/i18n/locales/ja.json b/apps/mobile/src/i18n/locales/ja.json index 725ef3ca06..7a5be134dd 100644 --- a/apps/mobile/src/i18n/locales/ja.json +++ b/apps/mobile/src/i18n/locales/ja.json @@ -240,7 +240,12 @@ "returnSendsMessage": "リターンキーでメッセージを送信", "returnSendsMessageSubtitle": "オフの場合、リターンキーでエージェントのメッセージ入力欄に改行を挿入します。", "biometricUnlock": "生体認証でロック解除", - "biometricUnlockSubtitle": "アプリの起動時と、バックグラウンドで5分経過した後は、ロック解除が必要です。" + "biometricUnlockSubtitle": "アプリの起動時と、バックグラウンドで5分経過した後は、ロック解除が必要です。", + "featureFlags": "機能フラグ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "ライブアクティビティ", diff --git a/apps/mobile/src/i18n/locales/ka.json b/apps/mobile/src/i18n/locales/ka.json index 2b7e4e29b2..ef0493608d 100644 --- a/apps/mobile/src/i18n/locales/ka.json +++ b/apps/mobile/src/i18n/locales/ka.json @@ -240,7 +240,12 @@ "returnSendsMessage": "შეყვანის კლავიშით შეტყობინების გაგზავნა", "returnSendsMessageSubtitle": "თუ გამორთულია, შეყვანის კლავიში აგენტის შეტყობინების ველში ახალ სტრიქონს ამატებს.", "biometricUnlock": "ბიომეტრიით განბლოკვა", - "biometricUnlockSubtitle": "განბლოკვა საჭიროა აპის გაშვებისას და ფონურ რეჟიმში ხუთი წუთის გასვლის შემდეგ." + "biometricUnlockSubtitle": "განბლოკვა საჭიროა აპის გაშვებისას და ფონურ რეჟიმში ხუთი წუთის გასვლის შემდეგ.", + "featureFlags": "ფუნქციების დროშები", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "შერწყმის მოთხოვნა მიუწვდომელია", diff --git a/apps/mobile/src/i18n/locales/kk.json b/apps/mobile/src/i18n/locales/kk.json index c878522091..92c9512e69 100644 --- a/apps/mobile/src/i18n/locales/kk.json +++ b/apps/mobile/src/i18n/locales/kk.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Enter пернесімен хабарлама жіберу", "returnSendsMessageSubtitle": "Бұл баптау өшірулі болса, Enter пернесі агентке хабарлама жазу өрісінде жаңа жолға ауыстырады.", "biometricUnlock": "Биометрия арқылы құлыпты ашу", - "biometricUnlockSubtitle": "Қолданбаны ашқанда және ол фондық режимде бес минут болғаннан кейін құлыпты ашу қажет." + "biometricUnlockSubtitle": "Қолданбаны ашқанда және ол фондық режимде бес минут болғаннан кейін құлыпты ашу қажет.", + "featureFlags": "Функция байрақтары", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Біріктіру сұрауы қолжетімсіз", diff --git a/apps/mobile/src/i18n/locales/km.json b/apps/mobile/src/i18n/locales/km.json index 166a5ee206..0e45c3e072 100644 --- a/apps/mobile/src/i18n/locales/km.json +++ b/apps/mobile/src/i18n/locales/km.json @@ -240,7 +240,12 @@ "returnSendsMessage": "គ្រាប់ចុចបញ្ចូលប្រើសម្រាប់ផ្ញើសារ", "returnSendsMessageSubtitle": "នៅពេលបិទ គ្រាប់ចុចបញ្ចូលនឹងបញ្ចូលបន្ទាត់ថ្មីក្នុងប្រអប់សរសេរសារទៅភ្នាក់ងារ។", "biometricUnlock": "ដោះសោដោយប្រើជីវមាត្រ", - "biometricUnlockSubtitle": "តម្រូវឱ្យដោះសោនៅពេលចាប់ផ្តើមកម្មវិធី និងបន្ទាប់ពីកម្មវិធីស្ថិតនៅផ្ទៃខាងក្រោយរយៈពេលប្រាំនាទី។" + "biometricUnlockSubtitle": "តម្រូវឱ្យដោះសោនៅពេលចាប់ផ្តើមកម្មវិធី និងបន្ទាប់ពីកម្មវិធីស្ថិតនៅផ្ទៃខាងក្រោយរយៈពេលប្រាំនាទី។", + "featureFlags": "ទង់លក្ខណៈពិសេស", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "មិនអាចបើកសំណើបញ្ចូលកូដបាន", diff --git a/apps/mobile/src/i18n/locales/kn.json b/apps/mobile/src/i18n/locales/kn.json index 3478cb4686..7545e64969 100644 --- a/apps/mobile/src/i18n/locales/kn.json +++ b/apps/mobile/src/i18n/locales/kn.json @@ -240,7 +240,12 @@ "returnSendsMessage": "ರಿಟರ್ನ್ ಕೀಲಿಯು ಸಂದೇಶ ಕಳುಹಿಸುತ್ತದೆ", "returnSendsMessageSubtitle": "ಈ ಆಯ್ಕೆ ನಿಷ್ಕ್ರಿಯವಾಗಿದ್ದಾಗ ರಿಟರ್ನ್ ಕೀಲಿಯು ಏಜೆಂಟ್‌ನ ಸಂದೇಶ ಪೆಟ್ಟಿಗೆಯಲ್ಲಿ ಹೊಸ ಸಾಲನ್ನು ಸೇರಿಸುತ್ತದೆ.", "biometricUnlock": "ಬಯೋಮೆಟ್ರಿಕ್ಸ್ ಮೂಲಕ ಅನ್‌ಲಾಕ್ ಮಾಡಿ", - "biometricUnlockSubtitle": "ಅಪ್ಲಿಕೇಶನ್ ಪ್ರಾರಂಭಿಸಿದಾಗ ಮತ್ತು ಐದು ನಿಮಿಷ ಹಿನ್ನೆಲೆಯಲ್ಲಿ ಇದ್ದ ನಂತರ ಅನ್‌ಲಾಕ್ ಮಾಡಬೇಕು." + "biometricUnlockSubtitle": "ಅಪ್ಲಿಕೇಶನ್ ಪ್ರಾರಂಭಿಸಿದಾಗ ಮತ್ತು ಐದು ನಿಮಿಷ ಹಿನ್ನೆಲೆಯಲ್ಲಿ ಇದ್ದ ನಂತರ ಅನ್‌ಲಾಕ್ ಮಾಡಬೇಕು.", + "featureFlags": "ಫೀಚರ್ ಫ್ಲ್ಯಾಗ್ಗಳು", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "ಪುಲ್ ರಿಕ್ವೆಸ್ಟ್ ಲಭ್ಯವಿಲ್ಲ", diff --git a/apps/mobile/src/i18n/locales/ko.json b/apps/mobile/src/i18n/locales/ko.json index 4658659c1b..8dfbd68753 100644 --- a/apps/mobile/src/i18n/locales/ko.json +++ b/apps/mobile/src/i18n/locales/ko.json @@ -240,7 +240,12 @@ "returnSendsMessage": "리턴 키로 메시지 보내기", "returnSendsMessageSubtitle": "끄면 에이전트의 메시지 입력란에서 리턴 키로 줄바꿈을 삽입합니다.", "biometricUnlock": "생체 인증으로 잠금 해제", - "biometricUnlockSubtitle": "앱을 실행할 때와 백그라운드에서 5분이 지난 후에는 잠금을 해제해야 합니다." + "biometricUnlockSubtitle": "앱을 실행할 때와 백그라운드에서 5분이 지난 후에는 잠금을 해제해야 합니다.", + "featureFlags": "기능 플래그", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "실시간 활동", diff --git a/apps/mobile/src/i18n/locales/lo.json b/apps/mobile/src/i18n/locales/lo.json index f9123cd9c8..3e2cdf8a86 100644 --- a/apps/mobile/src/i18n/locales/lo.json +++ b/apps/mobile/src/i18n/locales/lo.json @@ -240,7 +240,12 @@ "returnSendsMessage": "ກົດປຸ່ມຂຶ້ນແຖວໃໝ່ເພື່ອສົ່ງຂໍ້ຄວາມ", "returnSendsMessageSubtitle": "ເມື່ອປິດຕົວເລືອກນີ້ ປຸ່ມຂຶ້ນແຖວໃໝ່ຈະແຊກແຖວໃໝ່ໃນຊ່ອງຂຽນຂໍ້ຄວາມຫາຕົວແທນ.", "biometricUnlock": "ປົດລັອກດ້ວຍຂໍ້ມູນຊີວະມິຕິ", - "biometricUnlockSubtitle": "ຕ້ອງປົດລັອກເມື່ອເປີດແອັບ ແລະ ຫຼັງຈາກແອັບຢູ່ໃນພື້ນຫຼັງເປັນເວລາຫ້ານາທີ." + "biometricUnlockSubtitle": "ຕ້ອງປົດລັອກເມື່ອເປີດແອັບ ແລະ ຫຼັງຈາກແອັບຢູ່ໃນພື້ນຫຼັງເປັນເວລາຫ້ານາທີ.", + "featureFlags": "ທຸງຄຸນສົມບັດ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "ບໍ່ສາມາດເປີດຄຳຂໍລວມໂຄດ", diff --git a/apps/mobile/src/i18n/locales/lt.json b/apps/mobile/src/i18n/locales/lt.json index 4dd0d7f401..22dc55435a 100644 --- a/apps/mobile/src/i18n/locales/lt.json +++ b/apps/mobile/src/i18n/locales/lt.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Įvesties klavišas siunčia žinutę", "returnSendsMessageSubtitle": "Išjungus šią parinktį, įvesties klavišas agentų žinučių įvesties laukuose įterpia naują eilutę.", "biometricUnlock": "Atrakinti naudojant biometrinius duomenis", - "biometricUnlockSubtitle": "Paleidus programėlę ir jai penkias minutes išbuvus fone, reikia ją atrakinti." + "biometricUnlockSubtitle": "Paleidus programėlę ir jai penkias minutes išbuvus fone, reikia ją atrakinti.", + "featureFlags": "Funkcijų vėliavėlės", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Pakeitimų užklausa nepasiekiama", diff --git a/apps/mobile/src/i18n/locales/lv.json b/apps/mobile/src/i18n/locales/lv.json index a0c7b8f5ab..90b367ef65 100644 --- a/apps/mobile/src/i18n/locales/lv.json +++ b/apps/mobile/src/i18n/locales/lv.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Ievades taustiņš nosūta ziņojumu", "returnSendsMessageSubtitle": "Ja šī opcija ir izslēgta, ievades taustiņš aģenta ziņojuma ievades laukā ievieto jaunu rindu.", "biometricUnlock": "Biometriskā atbloķēšana", - "biometricUnlockSubtitle": "Atbloķēšana ir nepieciešama, palaižot lietotni un pēc piecām fonā pavadītām minūtēm." + "biometricUnlockSubtitle": "Atbloķēšana ir nepieciešama, palaižot lietotni un pēc piecām fonā pavadītām minūtēm.", + "featureFlags": "Funkciju karodziņi", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Izmaiņu pieprasījums nav pieejams", diff --git a/apps/mobile/src/i18n/locales/mg.json b/apps/mobile/src/i18n/locales/mg.json index 695d7dbe6f..eef73d4389 100644 --- a/apps/mobile/src/i18n/locales/mg.json +++ b/apps/mobile/src/i18n/locales/mg.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Mandefa hafatra ny bokotra fampidirana", "returnSendsMessageSubtitle": "Raha tsy mandeha ity safidy ity, dia mampiditra andalana vaovao ao amin'ny faritra fanoratana hafatra ho an'ny mpanampy ny bokotra fampidirana.", "biometricUnlock": "Vahao amin'ny biometrika ny hidy", - "biometricUnlockSubtitle": "Ilaina ny mamaha ny hidy rehefa manomboka ny rindranasa sy rehefa tsy naseho nandritra ny dimy minitra." + "biometricUnlockSubtitle": "Ilaina ny mamaha ny hidy rehefa manomboka ny rindranasa sy rehefa tsy naseho nandritra ny dimy minitra.", + "featureFlags": "Saina endri-javatra", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Tsy azo sokafana ny fangatahana fampiraisana", diff --git a/apps/mobile/src/i18n/locales/mi.json b/apps/mobile/src/i18n/locales/mi.json index b679487eca..1a5daa57b4 100644 --- a/apps/mobile/src/i18n/locales/mi.json +++ b/apps/mobile/src/i18n/locales/mi.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Ka tukuna te karere mā te pātuhi tāuru", "returnSendsMessageSubtitle": "Ina weto, ka whakaurua he rārangi hōu mā te pātuhi tāuru ki ngā pouaka karere māngai.", "biometricUnlock": "Wetekina mā te tautuhi koiora", - "biometricUnlockSubtitle": "Me wewete te taupānga ina tīmata, ā, ina hoki mai i muri i te rima meneti i te papamuri." + "biometricUnlockSubtitle": "Me wewete te taupānga ina tīmata, ā, ina hoki mai i muri i te rima meneti i te papamuri.", + "featureFlags": "Kāreti āhuatanga", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Kāore i te wātea te tono kume", diff --git a/apps/mobile/src/i18n/locales/mk.json b/apps/mobile/src/i18n/locales/mk.json index a8f09d348b..3d43ec0113 100644 --- a/apps/mobile/src/i18n/locales/mk.json +++ b/apps/mobile/src/i18n/locales/mk.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Копчето за нов ред испраќа порака", "returnSendsMessageSubtitle": "Кога е исклучено, копчето за нов ред вметнува нов ред во полињата за внес на пораки на агентите.", "biometricUnlock": "Отклучување со биометрија", - "biometricUnlockSubtitle": "Потребно е отклучување при стартување на апликацијата и откако ќе помине пет минути во заднина." + "biometricUnlockSubtitle": "Потребно е отклучување при стартување на апликацијата и откако ќе помине пет минути во заднина.", + "featureFlags": "Знаменца на функции", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Барањето за спојување е недостапно", diff --git a/apps/mobile/src/i18n/locales/ml.json b/apps/mobile/src/i18n/locales/ml.json index 82782bd51f..10794003f0 100644 --- a/apps/mobile/src/i18n/locales/ml.json +++ b/apps/mobile/src/i18n/locales/ml.json @@ -240,7 +240,12 @@ "returnSendsMessage": "റിട്ടേൺ കീ സന്ദേശം അയയ്ക്കുന്നു", "returnSendsMessageSubtitle": "ഇത് ഓഫായിരിക്കുമ്പോൾ റിട്ടേൺ കീ ഏജന്റിന്റെ സന്ദേശ ബോക്സിൽ പുതിയ വരി ചേർക്കും.", "biometricUnlock": "ബയോമെട്രിക് അൺലോക്ക്", - "biometricUnlockSubtitle": "ആപ്പ് ആരംഭിക്കുമ്പോഴും പശ്ചാത്തലത്തിൽ അഞ്ച് മിനിറ്റ് കഴിഞ്ഞ് തിരികെ വരുമ്പോഴും അൺലോക്ക് ചെയ്യണം." + "biometricUnlockSubtitle": "ആപ്പ് ആരംഭിക്കുമ്പോഴും പശ്ചാത്തലത്തിൽ അഞ്ച് മിനിറ്റ് കഴിഞ്ഞ് തിരികെ വരുമ്പോഴും അൺലോക്ക് ചെയ്യണം.", + "featureFlags": "ഫീച്ചർ ഫ്ലാഗുകൾ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "പുൾ റിക്വസ്റ്റ് ലഭ്യമല്ല", diff --git a/apps/mobile/src/i18n/locales/mn.json b/apps/mobile/src/i18n/locales/mn.json index f76177507f..bb9fe65a9d 100644 --- a/apps/mobile/src/i18n/locales/mn.json +++ b/apps/mobile/src/i18n/locales/mn.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Мөр шилжүүлэх товч мессеж илгээнэ", "returnSendsMessageSubtitle": "Унтраалттай үед мөр шилжүүлэх товч агентын мессеж оруулах талбарт шинэ мөр оруулна.", "biometricUnlock": "Биометрээр түгжээ тайлах", - "biometricUnlockSubtitle": "Аппыг эхлүүлэхэд болон арын горимд таван минут байсны дараа түгжээг тайлах шаардлагатай." + "biometricUnlockSubtitle": "Аппыг эхлүүлэхэд болон арын горимд таван минут байсны дараа түгжээг тайлах шаардлагатай.", + "featureFlags": "Функцын флагууд", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Нэгтгэх хүсэлтийг нээх боломжгүй", diff --git a/apps/mobile/src/i18n/locales/mr.json b/apps/mobile/src/i18n/locales/mr.json index b83d3da894..2401e8404b 100644 --- a/apps/mobile/src/i18n/locales/mr.json +++ b/apps/mobile/src/i18n/locales/mr.json @@ -240,7 +240,12 @@ "returnSendsMessage": "रिटर्न कळ संदेश पाठवते", "returnSendsMessageSubtitle": "हा पर्याय बंद असताना रिटर्न कळ दाबल्यास एजंटच्या संदेशपेटीत नवीन ओळ घातली जाते.", "biometricUnlock": "बायोमेट्रिक्स वापरून अनलॉक करा", - "biometricUnlockSubtitle": "अ‍ॅप सुरू करताना आणि पाच मिनिटे पार्श्वभूमीत राहिल्यानंतर अनलॉक करणे आवश्यक आहे." + "biometricUnlockSubtitle": "अ‍ॅप सुरू करताना आणि पाच मिनिटे पार्श्वभूमीत राहिल्यानंतर अनलॉक करणे आवश्यक आहे.", + "featureFlags": "वैशिष्ट्य ध्वज", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "पुल रिक्वेस्ट अनुपलब्ध", diff --git a/apps/mobile/src/i18n/locales/ms.json b/apps/mobile/src/i18n/locales/ms.json index 5917e7bd90..581267c976 100644 --- a/apps/mobile/src/i18n/locales/ms.json +++ b/apps/mobile/src/i18n/locales/ms.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Kekunci baris baharu menghantar mesej", "returnSendsMessageSubtitle": "Apabila tetapan ini dinyahdayakan, kekunci baris baharu menyisipkan baris baharu dalam ruang mesej ejen.", "biometricUnlock": "Buka kunci dengan biometrik", - "biometricUnlockSubtitle": "Buka kunci apabila apl dilancarkan dan selepas lima minit di latar belakang." + "biometricUnlockSubtitle": "Buka kunci apabila apl dilancarkan dan selepas lima minit di latar belakang.", + "featureFlags": "Bendera ciri", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Permintaan tarik tidak tersedia", diff --git a/apps/mobile/src/i18n/locales/mt.json b/apps/mobile/src/i18n/locales/mt.json index 0999de335b..5ec81377e8 100644 --- a/apps/mobile/src/i18n/locales/mt.json +++ b/apps/mobile/src/i18n/locales/mt.json @@ -240,7 +240,12 @@ "returnSendsMessage": "It-tast tar-ritorn jibgħat il-messaġġ", "returnSendsMessageSubtitle": "Meta din l-għażla tkun mitfija, it-tast tar-ritorn idaħħal linja ġdida fil-qasam tal-messaġġ lill-aġent.", "biometricUnlock": "Żblokka bil-bijometrija", - "biometricUnlockSubtitle": "Żblokka l-applikazzjoni meta titnieda u wara ħames minuti fl-isfond." + "biometricUnlockSubtitle": "Żblokka l-applikazzjoni meta titnieda u wara ħames minuti fl-isfond.", + "featureFlags": "Flags tal-karatteristiċi", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "It-talba għall-għaqda mhix disponibbli", diff --git a/apps/mobile/src/i18n/locales/my.json b/apps/mobile/src/i18n/locales/my.json index a31852eada..390f4047ae 100644 --- a/apps/mobile/src/i18n/locales/my.json +++ b/apps/mobile/src/i18n/locales/my.json @@ -240,7 +240,12 @@ "returnSendsMessage": "စာကြောင်းပြန်ခလုတ်ဖြင့် မက်ဆေ့ခ်ျပို့ပါ", "returnSendsMessageSubtitle": "ပိတ်ထားပါက စာကြောင်းပြန်ခလုတ်သည် အေးဂျင့်ထံ စာရေးသည့်အကွက်တွင် စာကြောင်းအသစ် ထည့်ပေးပါသည်။", "biometricUnlock": "ဇီဝအချက်အလက်ဖြင့် သော့ဖွင့်ခြင်း", - "biometricUnlockSubtitle": "အက်ပ်ကို စတင်ဖွင့်သည့်အခါနှင့် နောက်ခံတွင် ငါးမိနစ်ကြာ ရှိနေပြီးနောက် သော့ဖွင့်ရန် လိုအပ်ပါသည်။" + "biometricUnlockSubtitle": "အက်ပ်ကို စတင်ဖွင့်သည့်အခါနှင့် နောက်ခံတွင် ငါးမိနစ်ကြာ ရှိနေပြီးနောက် သော့ဖွင့်ရန် လိုအပ်ပါသည်။", + "featureFlags": "လုပ်ဆောင်ချက်အလံများ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "PR မရနိုင်ပါ", diff --git a/apps/mobile/src/i18n/locales/nb.json b/apps/mobile/src/i18n/locales/nb.json index 88b6525fae..f50030572f 100644 --- a/apps/mobile/src/i18n/locales/nb.json +++ b/apps/mobile/src/i18n/locales/nb.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Returtasten sender meldingen", "returnSendsMessageSubtitle": "Når dette er av, setter returtasten inn et linjeskift i agentenes meldingsfelt.", "biometricUnlock": "Lås opp med biometri", - "biometricUnlockSubtitle": "Lås opp appen ved oppstart og etter fem minutter i bakgrunnen." + "biometricUnlockSubtitle": "Lås opp appen ved oppstart og etter fem minutter i bakgrunnen.", + "featureFlags": "Funksjonsflagg", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "PR-en er utilgjengelig", diff --git a/apps/mobile/src/i18n/locales/ne.json b/apps/mobile/src/i18n/locales/ne.json index 9528ad1997..d2b832653d 100644 --- a/apps/mobile/src/i18n/locales/ne.json +++ b/apps/mobile/src/i18n/locales/ne.json @@ -240,7 +240,12 @@ "returnSendsMessage": "रिटर्न कुञ्जीले सन्देश पठाउँछ", "returnSendsMessageSubtitle": "यो विकल्प बन्द हुँदा रिटर्न कुञ्जीले एजेन्टको सन्देश लेख्ने ठाउँमा नयाँ पङ्क्ति थप्छ।", "biometricUnlock": "बायोमेट्रिक अनलक", - "biometricUnlockSubtitle": "एप सुरु गर्दा र पृष्ठभूमिमा पाँच मिनेट रहेपछि अनलक गर्नुपर्छ।" + "biometricUnlockSubtitle": "एप सुरु गर्दा र पृष्ठभूमिमा पाँच मिनेट रहेपछि अनलक गर्नुपर्छ।", + "featureFlags": "विशेषता झन्डा", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "पुल रिक्वेस्ट उपलब्ध छैन", diff --git a/apps/mobile/src/i18n/locales/nl.json b/apps/mobile/src/i18n/locales/nl.json index 1c765b33d1..d28f68b04a 100644 --- a/apps/mobile/src/i18n/locales/nl.json +++ b/apps/mobile/src/i18n/locales/nl.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Return-toets verzendt bericht", "returnSendsMessageSubtitle": "Als dit uitstaat, voegt Return een nieuwe regel toe in het berichtveld van de agent.", "biometricUnlock": "Ontgrendelen met biometrie", - "biometricUnlockSubtitle": "Ontgrendelen is vereist bij het starten van de app en nadat de app vijf minuten op de achtergrond heeft gestaan." + "biometricUnlockSubtitle": "Ontgrendelen is vereist bij het starten van de app en nadat de app vijf minuten op de achtergrond heeft gestaan.", + "featureFlags": "Functievlaggen", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "channel": { diff --git a/apps/mobile/src/i18n/locales/om.json b/apps/mobile/src/i18n/locales/om.json index 1281c9c57d..5b88ba91f8 100644 --- a/apps/mobile/src/i18n/locales/om.json +++ b/apps/mobile/src/i18n/locales/om.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Furtuun galchii ergaa ergiti", "returnSendsMessageSubtitle": "Yoo cufame, furtuun galchii iddoo ergaa ergamaa barreessuu keessatti sarara haaraa galchiti.", "biometricUnlock": "Baayoometirikisiin qulfii bani", - "biometricUnlockSubtitle": "Appilikeeshinichi yeroo jalqabamu fi duubbee keessatti daqiiqaa shan erga turee booda qulfii bani." + "biometricUnlockSubtitle": "Appilikeeshinichi yeroo jalqabamu fi duubbee keessatti daqiiqaa shan erga turee booda qulfii bani.", + "featureFlags": "Albaasa amalaa", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Gaaffiin walitti makuu hin argamu", diff --git a/apps/mobile/src/i18n/locales/or.json b/apps/mobile/src/i18n/locales/or.json index f8640ffe2a..899b5431ef 100644 --- a/apps/mobile/src/i18n/locales/or.json +++ b/apps/mobile/src/i18n/locales/or.json @@ -240,7 +240,12 @@ "returnSendsMessage": "ରିଟର୍ନ କି ବାର୍ତ୍ତା ପଠାଏ", "returnSendsMessageSubtitle": "ଏହା ବନ୍ଦ ଥିଲେ ରିଟର୍ନ କି ଏଜେଣ୍ଟର ବାର୍ତ୍ତା ଲେଖିବା ସ୍ଥାନରେ ନୂଆ ଧାଡ଼ି ଯୋଡ଼େ।", "biometricUnlock": "ବାୟୋମେଟ୍ରିକ୍ସ ସହିତ ଅନଲକ୍ କରନ୍ତୁ", - "biometricUnlockSubtitle": "ଆପ୍ ଆରମ୍ଭ ହେବାବେଳେ ଏବଂ ପୃଷ୍ଠଭୂମିରେ ପାଞ୍ଚ ମିନିଟ୍ ରହିବା ପରେ ଅନଲକ୍ କରିବା ଆବଶ୍ୟକ।" + "biometricUnlockSubtitle": "ଆପ୍ ଆରମ୍ଭ ହେବାବେଳେ ଏବଂ ପୃଷ୍ଠଭୂମିରେ ପାଞ୍ଚ ମିନିଟ୍ ରହିବା ପରେ ଅନଲକ୍ କରିବା ଆବଶ୍ୟକ।", + "featureFlags": "ବିଶେଷତ୍ୱ ପତାକା", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "ପୁଲ୍ ରିକ୍ୱେଷ୍ଟ ଉପଲବ୍ଧ ନାହିଁ", diff --git a/apps/mobile/src/i18n/locales/pa.json b/apps/mobile/src/i18n/locales/pa.json index 8eb46225e4..2333c63e23 100644 --- a/apps/mobile/src/i18n/locales/pa.json +++ b/apps/mobile/src/i18n/locales/pa.json @@ -240,7 +240,12 @@ "returnSendsMessage": "ਰਿਟਰਨ ਕੁੰਜੀ ਸੁਨੇਹਾ ਭੇਜਦੀ ਹੈ", "returnSendsMessageSubtitle": "ਇਹ ਸੈਟਿੰਗ ਬੰਦ ਹੋਣ 'ਤੇ, ਰਿਟਰਨ ਕੁੰਜੀ ਏਜੰਟ ਨੂੰ ਸੁਨੇਹਾ ਲਿਖਣ ਵਾਲੇ ਖਾਨੇ ਵਿੱਚ ਨਵੀਂ ਲਾਈਨ ਪਾਉਂਦੀ ਹੈ।", "biometricUnlock": "ਬਾਇਓਮੈਟ੍ਰਿਕਸ ਨਾਲ ਅਨਲੌਕ ਕਰੋ", - "biometricUnlockSubtitle": "ਐਪ ਚਾਲੂ ਕਰਨ ਵੇਲੇ ਅਤੇ ਪਿਛੋਕੜ ਵਿੱਚ ਪੰਜ ਮਿੰਟ ਰਹਿਣ ਤੋਂ ਬਾਅਦ ਅਨਲੌਕ ਕਰੋ।" + "biometricUnlockSubtitle": "ਐਪ ਚਾਲੂ ਕਰਨ ਵੇਲੇ ਅਤੇ ਪਿਛੋਕੜ ਵਿੱਚ ਪੰਜ ਮਿੰਟ ਰਹਿਣ ਤੋਂ ਬਾਅਦ ਅਨਲੌਕ ਕਰੋ।", + "featureFlags": "ਫ਼ੀਚਰ ਫ਼ਲੈਗ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "ਪੁੱਲ ਰਿਕਵੈਸਟ ਉਪਲਬਧ ਨਹੀਂ", diff --git a/apps/mobile/src/i18n/locales/pl.json b/apps/mobile/src/i18n/locales/pl.json index 417a95dc19..7930856c24 100644 --- a/apps/mobile/src/i18n/locales/pl.json +++ b/apps/mobile/src/i18n/locales/pl.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Klawisz Enter wysyła wiadomość", "returnSendsMessageSubtitle": "Gdy ta opcja jest wyłączona, klawisz Enter wstawia nowy wiersz w polach wiadomości agentów.", "biometricUnlock": "Odblokowywanie biometryczne", - "biometricUnlockSubtitle": "Odblokowanie jest wymagane przy uruchomieniu aplikacji oraz po pięciu minutach w tle." + "biometricUnlockSubtitle": "Odblokowanie jest wymagane przy uruchomieniu aplikacji oraz po pięciu minutach w tle.", + "featureFlags": "Flagi funkcji", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "Aktywności na żywo", diff --git a/apps/mobile/src/i18n/locales/ps.json b/apps/mobile/src/i18n/locales/ps.json index 4334d2027d..143a60836b 100644 --- a/apps/mobile/src/i18n/locales/ps.json +++ b/apps/mobile/src/i18n/locales/ps.json @@ -240,7 +240,12 @@ "returnSendsMessage": "د انټر کیلي پیغام لېږي", "returnSendsMessageSubtitle": "که دا تنظیم بند وي، د انټر کیلي د ایجنټ د پیغام لیکلو په ځای کې نوې کرښه زیاتوي.", "biometricUnlock": "د بایومیټریک له لارې د قلف خلاصول", - "biometricUnlockSubtitle": "د اپلیکیشن د پیل پر مهال او په شالید کې له پنځو دقیقو تېرولو وروسته قلف خلاص کړئ." + "biometricUnlockSubtitle": "د اپلیکیشن د پیل پر مهال او په شالید کې له پنځو دقیقو تېرولو وروسته قلف خلاص کړئ.", + "featureFlags": "د ځانګړتیاوو بیرغونه", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "پل ریکویسټ نشته", diff --git a/apps/mobile/src/i18n/locales/pt-BR.json b/apps/mobile/src/i18n/locales/pt-BR.json index 74cb75fe1a..0b66f4a472 100644 --- a/apps/mobile/src/i18n/locales/pt-BR.json +++ b/apps/mobile/src/i18n/locales/pt-BR.json @@ -2046,7 +2046,12 @@ "returnSendsMessage": "A tecla Enter envia a mensagem", "returnSendsMessageSubtitle": "Quando essa opção está desativada, a tecla Enter insere uma quebra de linha nos campos de mensagem dos agentes.", "biometricUnlock": "Desbloqueio por biometria", - "biometricUnlockSubtitle": "Exigir o desbloqueio ao abrir o app e após cinco minutos em segundo plano." + "biometricUnlockSubtitle": "Exigir o desbloqueio ao abrir o app e após cinco minutos em segundo plano.", + "featureFlags": "Sinalizadores de recursos", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "addCredits": { "cta": "Adicionar créditos", diff --git a/apps/mobile/src/i18n/locales/pt.json b/apps/mobile/src/i18n/locales/pt.json index b3f23af8e0..024401e9c5 100644 --- a/apps/mobile/src/i18n/locales/pt.json +++ b/apps/mobile/src/i18n/locales/pt.json @@ -240,7 +240,12 @@ "returnSendsMessage": "A tecla de retorno envia a mensagem", "returnSendsMessageSubtitle": "Quando desativado, a tecla de retorno insere uma nova linha nos campos de mensagem dos agentes.", "biometricUnlock": "Desbloquear com biometria", - "biometricUnlockSubtitle": "É necessário desbloquear ao iniciar e após cinco minutos em segundo plano." + "biometricUnlockSubtitle": "É necessário desbloquear ao iniciar e após cinco minutos em segundo plano.", + "featureFlags": "Sinalizadores de recursos", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "PR indisponível", diff --git a/apps/mobile/src/i18n/locales/ro.json b/apps/mobile/src/i18n/locales/ro.json index b0a75f0871..df3c308f43 100644 --- a/apps/mobile/src/i18n/locales/ro.json +++ b/apps/mobile/src/i18n/locales/ro.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Tasta Enter trimite mesajul", "returnSendsMessageSubtitle": "Când opțiunea este dezactivată, tasta Enter inserează o linie nouă în câmpurile de mesaj ale agenților.", "biometricUnlock": "Deblocare prin autentificare biometrică", - "biometricUnlockSubtitle": "Deblocarea este necesară la pornirea aplicației și după cinci minute în fundal." + "biometricUnlockSubtitle": "Deblocarea este necesară la pornirea aplicației și după cinci minute în fundal.", + "featureFlags": "Fanioane de funcții", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "PR indisponibil", diff --git a/apps/mobile/src/i18n/locales/ru.json b/apps/mobile/src/i18n/locales/ru.json index 16092f95b3..e48e753a71 100644 --- a/apps/mobile/src/i18n/locales/ru.json +++ b/apps/mobile/src/i18n/locales/ru.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Отправлять сообщение клавишей ввода", "returnSendsMessageSubtitle": "Если отключено, клавиша ввода добавляет перенос строки в поле ввода сообщения агенту.", "biometricUnlock": "Разблокировка по биометрии", - "biometricUnlockSubtitle": "Разблокировка требуется при запуске приложения и после пяти минут в фоновом режиме." + "biometricUnlockSubtitle": "Разблокировка требуется при запуске приложения и после пяти минут в фоновом режиме.", + "featureFlags": "Флаги функций", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "Эфир активности", diff --git a/apps/mobile/src/i18n/locales/si.json b/apps/mobile/src/i18n/locales/si.json index 622939ae4f..f3ab4c8850 100644 --- a/apps/mobile/src/i18n/locales/si.json +++ b/apps/mobile/src/i18n/locales/si.json @@ -240,7 +240,12 @@ "returnSendsMessage": "එන්ටර් යතුරෙන් පණිවිඩය යවයි", "returnSendsMessageSubtitle": "මෙය අක්‍රීය කළ විට එන්ටර් යතුරෙන් නියෝජිත පණිවිඩ කොටුවල නව පේළියක් ඇතුළත් කරයි.", "biometricUnlock": "ජෛවමිතික දත්ත භාවිතයෙන් අගුළු හරින්න", - "biometricUnlockSubtitle": "යෙදුම ආරම්භ කරන විට සහ පසුබිමේ මිනිත්තු පහක් පැවතීමෙන් පසු අගුළු හැරීම අවශ්‍ය වේ." + "biometricUnlockSubtitle": "යෙදුම ආරම්භ කරන විට සහ පසුබිමේ මිනිත්තු පහක් පැවතීමෙන් පසු අගුළු හැරීම අවශ්‍ය වේ.", + "featureFlags": "විශේෂාංග ධ්වජ", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "පුල් ඉල්ලීම ලබා ගත නොහැක", diff --git a/apps/mobile/src/i18n/locales/sk.json b/apps/mobile/src/i18n/locales/sk.json index 6c9763823d..47c8d0ac6b 100644 --- a/apps/mobile/src/i18n/locales/sk.json +++ b/apps/mobile/src/i18n/locales/sk.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Kláves Enter odošle správu", "returnSendsMessageSubtitle": "Keď je táto možnosť vypnutá, kláves Enter vloží nový riadok do poľa na správu agentovi.", "biometricUnlock": "Odomknúť pomocou biometrie", - "biometricUnlockSubtitle": "Odomknutie sa vyžaduje pri spustení aplikácie a po piatich minútach na pozadí." + "biometricUnlockSubtitle": "Odomknutie sa vyžaduje pri spustení aplikácie a po piatich minútach na pozadí.", + "featureFlags": "Príznaky funkcií", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Žiadosť o zlúčenie nie je dostupná", diff --git a/apps/mobile/src/i18n/locales/sl.json b/apps/mobile/src/i18n/locales/sl.json index b4c2daf386..63322f22a9 100644 --- a/apps/mobile/src/i18n/locales/sl.json +++ b/apps/mobile/src/i18n/locales/sl.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Tipka Enter pošlje sporočilo", "returnSendsMessageSubtitle": "Ko je možnost izklopljena, tipka Enter vstavi novo vrstico v polja za vnos sporočil agentom.", "biometricUnlock": "Odklepanje z biometrijo", - "biometricUnlockSubtitle": "Odklepanje je obvezno ob zagonu aplikacije in po petih minutah v ozadju." + "biometricUnlockSubtitle": "Odklepanje je obvezno ob zagonu aplikacije in po petih minutah v ozadju.", + "featureFlags": "Zastavice funkcij", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Zahtevek za združitev ni na voljo", diff --git a/apps/mobile/src/i18n/locales/so.json b/apps/mobile/src/i18n/locales/so.json index 19a2a388d8..08c6614eb6 100644 --- a/apps/mobile/src/i18n/locales/so.json +++ b/apps/mobile/src/i18n/locales/so.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Furaha gelintu wuxuu diraa farriinta", "returnSendsMessageSubtitle": "Marka la damiyo, furaha gelintu wuxuu sadar cusub ku daraa goobaha farriimaha wakiilka lagu qoro.", "biometricUnlock": "Ku fur aqoonsiga astaamaha jirka", - "biometricUnlockSubtitle": "Fur marka barnaamijku bilaabmo iyo marka uu shan daqiiqo gadaal ka shaqaynayay." + "biometricUnlockSubtitle": "Fur marka barnaamijku bilaabmo iyo marka uu shan daqiiqo gadaal ka shaqaynayay.", + "featureFlags": "Calaamadaha sifooyinka", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Codsiga isku-darka lama heli karo", diff --git a/apps/mobile/src/i18n/locales/sq.json b/apps/mobile/src/i18n/locales/sq.json index 36b039d5c7..74f0f8e454 100644 --- a/apps/mobile/src/i18n/locales/sq.json +++ b/apps/mobile/src/i18n/locales/sq.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Tasti Enter dërgon mesazhin", "returnSendsMessageSubtitle": "Kur ky opsion është i çaktivizuar, tasti Enter shton një rresht të ri në fushën e mesazhit për agjentin.", "biometricUnlock": "Zhbllokim me biometri", - "biometricUnlockSubtitle": "Zhbllokimi kërkohet kur hap aplikacionin dhe pasi ai qëndron pesë minuta në sfond." + "biometricUnlockSubtitle": "Zhbllokimi kërkohet kur hap aplikacionin dhe pasi ai qëndron pesë minuta në sfond.", + "featureFlags": "Flamujt e veçorive", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Kërkesa për bashkim nuk është e disponueshme", diff --git a/apps/mobile/src/i18n/locales/sr.json b/apps/mobile/src/i18n/locales/sr.json index f96238042d..08b9952ed9 100644 --- a/apps/mobile/src/i18n/locales/sr.json +++ b/apps/mobile/src/i18n/locales/sr.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Taster za novi red šalje poruku", "returnSendsMessageSubtitle": "Kada je ova opcija isključena, taster za novi red umeće novi red u polje za poruku agentu.", "biometricUnlock": "Otključavanje biometrijom", - "biometricUnlockSubtitle": "Otključavanje je obavezno pri pokretanju aplikacije i nakon pet minuta u pozadini." + "biometricUnlockSubtitle": "Otključavanje je obavezno pri pokretanju aplikacije i nakon pet minuta u pozadini.", + "featureFlags": "Zastavice funkcija", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Zahtev za spajanje nije dostupan", diff --git a/apps/mobile/src/i18n/locales/sv.json b/apps/mobile/src/i18n/locales/sv.json index 8faace4fcb..24329a6eda 100644 --- a/apps/mobile/src/i18n/locales/sv.json +++ b/apps/mobile/src/i18n/locales/sv.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Returtangenten skickar meddelanden", "returnSendsMessageSubtitle": "När detta är avstängt infogar returtangenten en radbrytning i agenternas meddelandefält.", "biometricUnlock": "Lås upp med biometri", - "biometricUnlockSubtitle": "Upplåsning krävs när appen startas och efter fem minuter i bakgrunden." + "biometricUnlockSubtitle": "Upplåsning krävs när appen startas och efter fem minuter i bakgrunden.", + "featureFlags": "Funktionsflaggor", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "PR:en är inte tillgänglig", diff --git a/apps/mobile/src/i18n/locales/sw.json b/apps/mobile/src/i18n/locales/sw.json index 838e76c486..bf71068f64 100644 --- a/apps/mobile/src/i18n/locales/sw.json +++ b/apps/mobile/src/i18n/locales/sw.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Kitufe cha kuingiza hutuma ujumbe", "returnSendsMessageSubtitle": "Kipengele hiki kikiwa kimezimwa, kitufe cha kuingiza huongeza mstari mpya kwenye sehemu za kuandika ujumbe kwa mawakala.", "biometricUnlock": "Fungua kwa bayometriki", - "biometricUnlockSubtitle": "Kufungua kunahitajika unapoanzisha programu na baada ya programu kukaa chinichini kwa dakika tano." + "biometricUnlockSubtitle": "Kufungua kunahitajika unapoanzisha programu na baada ya programu kukaa chinichini kwa dakika tano.", + "featureFlags": "Alamu za vipengele", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Ombi la kuunganisha halipatikani", diff --git a/apps/mobile/src/i18n/locales/ta.json b/apps/mobile/src/i18n/locales/ta.json index 3c07c96fc1..ab1fe3dd80 100644 --- a/apps/mobile/src/i18n/locales/ta.json +++ b/apps/mobile/src/i18n/locales/ta.json @@ -240,7 +240,12 @@ "returnSendsMessage": "என்டர் விசை செய்தியை அனுப்பும்", "returnSendsMessageSubtitle": "இதை முடக்கினால், ஏஜென்ட்டின் செய்திப் பெட்டியில் என்டர் விசை புதிய வரியைச் செருகும்.", "biometricUnlock": "உயிரளவியல் மூலம் பூட்டைத் திறக்கவும்", - "biometricUnlockSubtitle": "செயலியைத் தொடங்கும்போதும், பின்னணியில் ஐந்து நிமிடங்கள் இருந்த பிறகும் பூட்டைத் திறக்க வேண்டும்." + "biometricUnlockSubtitle": "செயலியைத் தொடங்கும்போதும், பின்னணியில் ஐந்து நிமிடங்கள் இருந்த பிறகும் பூட்டைத் திறக்க வேண்டும்.", + "featureFlags": "அம்சக் கொடிகள்", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "இணைப்புக் கோரிக்கை கிடைக்கவில்லை", diff --git a/apps/mobile/src/i18n/locales/te.json b/apps/mobile/src/i18n/locales/te.json index 946dddd994..2024e455aa 100644 --- a/apps/mobile/src/i18n/locales/te.json +++ b/apps/mobile/src/i18n/locales/te.json @@ -240,7 +240,12 @@ "returnSendsMessage": "రిటర్న్ కీ సందేశాన్ని పంపుతుంది", "returnSendsMessageSubtitle": "దీన్ని ఆఫ్ చేస్తే, రిటర్న్ కీ ఏజెంట్ సందేశ పెట్టెల్లో కొత్త పంక్తిని చేర్చుతుంది.", "biometricUnlock": "బయోమెట్రిక్స్‌తో అన్‌లాక్ చేయండి", - "biometricUnlockSubtitle": "యాప్‌ను తెరిచినప్పుడు, అలాగే అది నేపథ్యంలో ఐదు నిమిషాలు ఉన్న తర్వాత అన్‌లాక్ చేయాలి." + "biometricUnlockSubtitle": "యాప్‌ను తెరిచినప్పుడు, అలాగే అది నేపథ్యంలో ఐదు నిమిషాలు ఉన్న తర్వాత అన్‌లాక్ చేయాలి.", + "featureFlags": "ఫీచర్ ఫ్లాగులు", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "పుల్ రిక్వెస్ట్ అందుబాటులో లేదు", diff --git a/apps/mobile/src/i18n/locales/th.json b/apps/mobile/src/i18n/locales/th.json index f494aeb3a4..57c8f0c515 100644 --- a/apps/mobile/src/i18n/locales/th.json +++ b/apps/mobile/src/i18n/locales/th.json @@ -240,7 +240,12 @@ "returnSendsMessage": "ปุ่มขึ้นบรรทัดใหม่ใช้ส่งข้อความ", "returnSendsMessageSubtitle": "เมื่อปิด ปุ่มขึ้นบรรทัดใหม่จะแทรกบรรทัดใหม่ในช่องป้อนข้อความของเอเจนต์", "biometricUnlock": "ปลดล็อกด้วยข้อมูลชีวมิติ", - "biometricUnlockSubtitle": "ต้องปลดล็อกเมื่อเปิดแอปและเมื่อกลับมาใช้งานหลังจากแอปอยู่เบื้องหลังครบห้านาที" + "biometricUnlockSubtitle": "ต้องปลดล็อกเมื่อเปิดแอปและเมื่อกลับมาใช้งานหลังจากแอปอยู่เบื้องหลังครบห้านาที", + "featureFlags": "แฟล็กฟีเจอร์", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "เปิด PR ไม่ได้", diff --git a/apps/mobile/src/i18n/locales/tr.json b/apps/mobile/src/i18n/locales/tr.json index f3d89c8ec5..212bd6296c 100644 --- a/apps/mobile/src/i18n/locales/tr.json +++ b/apps/mobile/src/i18n/locales/tr.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Enter tuşu mesaj gönderir", "returnSendsMessageSubtitle": "Kapalıyken Enter tuşu, ajanla konuşurken mesaj alanına yeni satır ekler.", "biometricUnlock": "Biyometrik doğrulama ile kilit açma", - "biometricUnlockSubtitle": "Uygulama başlatıldığında ve arka planda beş dakika kaldıktan sonra kilidin açılması gerekir." + "biometricUnlockSubtitle": "Uygulama başlatıldığında ve arka planda beş dakika kaldıktan sonra kilidin açılması gerekir.", + "featureFlags": "Özellik bayrakları", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "Canlı etkinlikler", diff --git a/apps/mobile/src/i18n/locales/uk.json b/apps/mobile/src/i18n/locales/uk.json index 10b7ddd170..9e6d81ef5e 100644 --- a/apps/mobile/src/i18n/locales/uk.json +++ b/apps/mobile/src/i18n/locales/uk.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Клавіша вводу надсилає повідомлення", "returnSendsMessageSubtitle": "Якщо вимкнено, клавіша вводу вставляє новий рядок у полях введення повідомлень агентам.", "biometricUnlock": "Біометричне розблокування", - "biometricUnlockSubtitle": "Запитувати біометричне розблокування під час запуску та після п'яти хвилин у фоновому режимі." + "biometricUnlockSubtitle": "Запитувати біометричне розблокування під час запуску та після п'яти хвилин у фоновому режимі.", + "featureFlags": "Прапори функцій", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "Дії наживо", diff --git a/apps/mobile/src/i18n/locales/ur.json b/apps/mobile/src/i18n/locales/ur.json index ac56511395..85be921146 100644 --- a/apps/mobile/src/i18n/locales/ur.json +++ b/apps/mobile/src/i18n/locales/ur.json @@ -240,7 +240,12 @@ "returnSendsMessage": "ریٹرن کی دبانے سے پیغام بھیجیں", "returnSendsMessageSubtitle": "اگر یہ ترتیب بند ہو تو ریٹرن کی دبانے سے ایجنٹ کو پیغام لکھنے کے خانے میں نئی سطر شامل ہوتی ہے۔", "biometricUnlock": "بایومیٹرکس سے انلاک کریں", - "biometricUnlockSubtitle": "ایپ شروع کرنے پر اور پس منظر میں پانچ منٹ رہنے کے بعد انلاک کرنا ضروری ہے۔" + "biometricUnlockSubtitle": "ایپ شروع کرنے پر اور پس منظر میں پانچ منٹ رہنے کے بعد انلاک کرنا ضروری ہے۔", + "featureFlags": "فیچر فلیگز", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "پل ریکویسٹ دستیاب نہیں", diff --git a/apps/mobile/src/i18n/locales/uz.json b/apps/mobile/src/i18n/locales/uz.json index 9a155bb25b..759b3c21fc 100644 --- a/apps/mobile/src/i18n/locales/uz.json +++ b/apps/mobile/src/i18n/locales/uz.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Enter tugmasi xabar yuboradi", "returnSendsMessageSubtitle": "O'chirilganda Enter tugmasi agentga xabar kiritish maydonida yangi qator ochadi.", "biometricUnlock": "Biometriya yordamida qulfni ochish", - "biometricUnlockSubtitle": "Ilova ishga tushirilganda va fonda besh daqiqa turgandan keyin qulfni ochish talab qilinadi." + "biometricUnlockSubtitle": "Ilova ishga tushirilganda va fonda besh daqiqa turgandan keyin qulfni ochish talab qilinadi.", + "featureFlags": "Funksiya bayroqlari", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "PR mavjud emas", diff --git a/apps/mobile/src/i18n/locales/vi.json b/apps/mobile/src/i18n/locales/vi.json index 8acdf1bcef..da65bcb371 100644 --- a/apps/mobile/src/i18n/locales/vi.json +++ b/apps/mobile/src/i18n/locales/vi.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Phím xuống dòng gửi tin nhắn", "returnSendsMessageSubtitle": "Khi tắt, phím xuống dòng sẽ chèn dòng mới vào ô nhập tin nhắn gửi cho tác nhân.", "biometricUnlock": "Mở khóa bằng sinh trắc học", - "biometricUnlockSubtitle": "Yêu cầu mở khóa khi khởi chạy ứng dụng và sau khi ứng dụng ở chế độ nền trong năm phút." + "biometricUnlockSubtitle": "Yêu cầu mở khóa khi khởi chạy ứng dụng và sau khi ứng dụng ở chế độ nền trong năm phút.", + "featureFlags": "Cờ tính năng", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "Hoạt động trực tiếp", diff --git a/apps/mobile/src/i18n/locales/yo.json b/apps/mobile/src/i18n/locales/yo.json index 02ef87d2b6..9364bea1b5 100644 --- a/apps/mobile/src/i18n/locales/yo.json +++ b/apps/mobile/src/i18n/locales/yo.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Bọ́tìnì ìpadàbọ̀ ń fi ìfiránṣẹ́ ránṣẹ́", "returnSendsMessageSubtitle": "Tí èyí bá wà ní pípa, bọ́tìnì ìpadàbọ̀ máa ń fi ìlà tuntun sí ààyè ìkọ̀wé sí aṣojú.", "biometricUnlock": "Ṣí títìpa pẹ̀lú ìdánimọ̀ àbùdá ara", - "biometricUnlockSubtitle": "Ṣí títìpa nígbà tí o bá ṣí ohun èlò àti lẹ́yìn ìṣẹ́jú márùn-ún tí kò sí níwájú iboju." + "biometricUnlockSubtitle": "Ṣí títìpa nígbà tí o bá ṣí ohun èlò àti lẹ́yìn ìṣẹ́jú márùn-ún tí kò sí níwájú iboju.", + "featureFlags": "Àṣì ẹ̀yà", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Ìbéèrè ìṣọ̀kan kò sí", diff --git a/apps/mobile/src/i18n/locales/zh-Hans.json b/apps/mobile/src/i18n/locales/zh-Hans.json index ac625d27f1..3786a7d1d2 100644 --- a/apps/mobile/src/i18n/locales/zh-Hans.json +++ b/apps/mobile/src/i18n/locales/zh-Hans.json @@ -240,7 +240,12 @@ "returnSendsMessage": "回车键发送消息", "returnSendsMessageSubtitle": "关闭后,按回车键会在智能体的消息输入框中换行。", "biometricUnlock": "使用生物识别解锁", - "biometricUnlockSubtitle": "启动应用时以及应用在后台停留五分钟后,均需解锁。" + "biometricUnlockSubtitle": "启动应用时以及应用在后台停留五分钟后,均需解锁。", + "featureFlags": "功能标志", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "实时活动", diff --git a/apps/mobile/src/i18n/locales/zh-Hant.json b/apps/mobile/src/i18n/locales/zh-Hant.json index 7909394aa2..83d3564c21 100644 --- a/apps/mobile/src/i18n/locales/zh-Hant.json +++ b/apps/mobile/src/i18n/locales/zh-Hant.json @@ -240,7 +240,12 @@ "returnSendsMessage": "按換行鍵傳送訊息", "returnSendsMessageSubtitle": "關閉後,在智慧代理的訊息輸入框中按換行鍵只會換行。", "biometricUnlock": "使用生物辨識解鎖", - "biometricUnlockSubtitle": "啟動應用程式時,以及在背景停留五分鐘後返回時,都需要解鎖。" + "biometricUnlockSubtitle": "啟動應用程式時,以及在背景停留五分鐘後返回時,都需要解鎖。", + "featureFlags": "功能旗標", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "notifications": { "liveActivities": "即時動態", diff --git a/apps/mobile/src/i18n/locales/zu.json b/apps/mobile/src/i18n/locales/zu.json index 85f294b0f3..8199d1ffe0 100644 --- a/apps/mobile/src/i18n/locales/zu.json +++ b/apps/mobile/src/i18n/locales/zu.json @@ -240,7 +240,12 @@ "returnSendsMessage": "Inkinobho yokubuyela ithumela umlayezo", "returnSendsMessageSubtitle": "Uma lokhu kuvaliwe, inkinobho yokubuyela ifaka umugqa omusha ezindaweni zokubhalela abenzeli.", "biometricUnlock": "Vula ngebhayomethrikhi", - "biometricUnlockSubtitle": "Vula uhlelo lokusebenza lapho luqala nangemva kokuba selunemizuzu emihlanu lusebenza ngemuva." + "biometricUnlockSubtitle": "Vula uhlelo lokusebenza lapho luqala nangemva kokuba selunemizuzu emihlanu lusebenza ngemuva.", + "featureFlags": "Amaflagi ezici", + "featureFlagsBuild": "v{{version}}", + "featureFlagApplied": "remote · ≥ {{min}}", + "featureFlagSkipped": "default · < {{min}}", + "featureFlagNotLoaded": "default · not loaded" }, "prReview": { "pullRequestUnavailable": "Isicelo sokuhlanganisa asitholakali", diff --git a/apps/mobile/src/lib/analytics/posthog.feature-flags.test.ts b/apps/mobile/src/lib/analytics/posthog.feature-flags.test.ts new file mode 100644 index 0000000000..71442ad88e --- /dev/null +++ b/apps/mobile/src/lib/analytics/posthog.feature-flags.test.ts @@ -0,0 +1,264 @@ +/* eslint-disable typescript-eslint/no-deprecated -- react-test-renderer is the DOM-free renderer used to mount React/RN trees under vitest (same pattern as preferences-screen.mounted.test.tsx) */ +import { createElement } from 'react'; +import TestRenderer, { act } from 'react-test-renderer'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { type FeatureFlagStatus } from './posthog'; + +const hoisted = vi.hoisted(() => { + const client = { + register: vi.fn(), + setPersonPropertiesForFlags: vi.fn(), + onFeatureFlags: vi.fn(), + capture: vi.fn(), + screen: vi.fn(), + identify: vi.fn(), + reloadFeatureFlags: vi.fn(), + getFeatureFlag: vi.fn(), + reset: vi.fn(), + setPersistedProperty: vi.fn(), + optOut: vi.fn().mockResolvedValue(undefined), + shutdown: vi.fn(), + flush: vi.fn(), + }; + // Mutable so each test can set the build version under test before the + // module loads. `useFeatureFlag` reads it at read time, not import time. + const application = { + nativeApplicationVersion: '1.0.8' as string | undefined, + nativeBuildVersion: '45', + }; + const controller = { + allowsOptional: vi.fn().mockReturnValue(true), + currentGeneration: vi.fn().mockReturnValue(0), + }; + return { client, application, controller }; +}); + +vi.mock('posthog-react-native', () => ({ + default: vi.fn(function PostHogMock() { + return hoisted.client; + }), + PostHogPersistedProperty: { + Queue: 'queue', + LogsQueue: 'logs_queue', + AiQueue: 'ai_queue', + }, +})); + +vi.mock('expo-device', () => ({ + DeviceType: { UNKNOWN: 0, PHONE: 1, TABLET: 2, DESKTOP: 3, TV: 4 }, + deviceType: null, +})); + +vi.mock('expo-application', () => ({ + get nativeApplicationVersion() { + return hoisted.application.nativeApplicationVersion; + }, + get nativeBuildVersion() { + return hoisted.application.nativeBuildVersion; + }, +})); + +vi.mock('@/lib/config', () => ({ POSTHOG_API_KEY: 'test-key' })); + +vi.mock('@/lib/telemetry/controller', () => ({ + allowsOptional: hoisted.controller.allowsOptional, + currentGeneration: hoisted.controller.currentGeneration, +})); + +vi.mock('@/lib/telemetry/posthog-storage', () => ({ + sealPostHogStorage: vi.fn(), + unsealPostHogStorage: vi.fn(), + purgePostHogPersistence: vi.fn(), + isPostHogStorageSealed: vi.fn().mockReturnValue(false), + posthogCustomStorage: {}, +})); + +vi.stubGlobal('__DEV__', false); +vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + +/** Mount a probe that reads `useFeatureFlag` the way real screens do. */ +async function readFlag(key: string, defaultValue: boolean): Promise { + const posthog = await import('./posthog'); + let observed: boolean | undefined = undefined; + function Probe() { + observed = posthog.useFeatureFlag(key, defaultValue); + return null; + } + let renderer: TestRenderer.ReactTestRenderer | undefined = undefined; + act(() => { + renderer = TestRenderer.create(createElement(Probe)); + }); + act(() => { + renderer?.unmount(); + }); + // undefined here means the probe never rendered — every expectation below + // then fails, which is the honest signal. + return observed; +} + +beforeEach(() => { + vi.clearAllMocks(); + vi.resetModules(); + hoisted.application.nativeApplicationVersion = '1.0.8'; + hoisted.controller.allowsOptional.mockReturnValue(true); + hoisted.controller.currentGeneration.mockReturnValue(0); + hoisted.client.getFeatureFlag.mockReset(); +}); + +describe('version-aware feature flags', () => { + it('an older build falls back to the default for a flag introduced in a newer version', async () => { + // Build 1.0.5 predates mobile-quick-chat (minimum 1.0.6): it must not act + // on the remote value, whatever PostHog returns. + hoisted.application.nativeApplicationVersion = '1.0.5'; + hoisted.client.getFeatureFlag.mockImplementation( + (key: string) => (key === 'mobile-quick-chat' ? true : undefined) as never + ); + const { initPostHog } = await import('./posthog'); + initPostHog(); + + const probe = await readFlag('mobile-quick-chat', false); + + expect(probe).toBe(false); + }); + + it('a build at or above the minimum applies the remote flag value', async () => { + // Build 1.0.8 understands mobile-quick-chat (minimum 1.0.6): the remote + // value drives the UI in both directions. + hoisted.client.getFeatureFlag.mockImplementation( + (key: string) => (key === 'mobile-quick-chat' ? true : undefined) as never + ); + const { initPostHog } = await import('./posthog'); + initPostHog(); + + expect(await readFlag('mobile-quick-chat', false)).toBe(true); + + hoisted.client.getFeatureFlag.mockImplementation(() => false as never); + expect(await readFlag('mobile-quick-chat', false)).toBe(false); + }); + + it('a build at exactly the minimum version applies the flag', async () => { + hoisted.application.nativeApplicationVersion = '1.0.6'; + hoisted.client.getFeatureFlag.mockImplementation( + (key: string) => (key === 'mobile-quick-chat' ? true : undefined) as never + ); + const { initPostHog } = await import('./posthog'); + initPostHog(); + + expect(await readFlag('mobile-quick-chat', false)).toBe(true); + }); + + it('keeps the caller default while flags are not loaded', async () => { + // No client (consent pending / dev build): every flag falls back to its + // default, exactly as before this change. + const posthog = await import('./posthog'); + posthog.initPostHog(); + hoisted.client.getFeatureFlag.mockImplementation(() => undefined as never); + + expect(await readFlag('mobile-quick-chat', false)).toBe(false); + expect(await readFlag('mobile-pr-review', true)).toBe(true); + }); + + it('an unknown flag key keeps the legacy behavior', async () => { + // A key with no registry entry has no minimum, so the remote value still + // applies (this is how a flag gains a version gate: by being registered). + hoisted.client.getFeatureFlag.mockImplementation(() => true as never); + const { initPostHog } = await import('./posthog'); + initPostHog(); + + expect(await readFlag('mobile-not-in-registry', false)).toBe(true); + }); +}); + +describe('feature flag statuses (debug surface)', () => { + it('reports which flags the build applies and which it skips, with reasons', async () => { + hoisted.application.nativeApplicationVersion = '1.0.5'; + hoisted.client.getFeatureFlag.mockImplementation( + (key: string) => (key === 'mobile-pr-review' ? false : undefined) as never + ); + const { initPostHog, getFeatureFlagStatuses } = await import('./posthog'); + initPostHog(); + + expect(getFeatureFlagStatuses()).toEqual([ + { + key: 'mobile-pr-review', + minAppVersion: '1.0.4', + defaultValue: true, + appVersion: '1.0.5', + applied: true, + value: false, + reason: 'applied', + loaded: true, + }, + { + key: 'mobile-quick-chat', + minAppVersion: '1.0.6', + defaultValue: false, + appVersion: '1.0.5', + applied: false, + value: false, + reason: 'build-too-old', + loaded: false, + }, + ]); + }); + + it('reports defaults in use while the client has no flag values', async () => { + const { initPostHog, getFeatureFlagStatuses } = await import('./posthog'); + initPostHog(); + + const statuses = getFeatureFlagStatuses(); + expect(statuses.every(status => !status.loaded)).toBe(true); + expect(statuses.every(status => !status.applied)).toBe(true); + expect(statuses.map(status => status.value)).toEqual( + statuses.map(status => status.defaultValue) + ); + }); + + it('reports a skipped flag even when a remote value exists', async () => { + hoisted.application.nativeApplicationVersion = '1.0.5'; + hoisted.client.getFeatureFlag.mockImplementation(() => true as never); + const { initPostHog, getFeatureFlagStatuses } = await import('./posthog'); + initPostHog(); + + const quickChat = getFeatureFlagStatuses().find(status => status.key === 'mobile-quick-chat'); + expect(quickChat).toMatchObject({ + applied: false, + reason: 'build-too-old', + value: false, + loaded: true, + }); + }); + + it('updates reactively when the client re-emits flags', async () => { + hoisted.client.getFeatureFlag.mockImplementation(() => undefined as never); + const posthog = await import('./posthog'); + posthog.initPostHog(); + + const statuses: FeatureFlagStatus[][] = []; + function Probe() { + statuses.push(posthog.useFeatureFlagStatuses()); + return null; + } + let renderer: TestRenderer.ReactTestRenderer | undefined = undefined; + act(() => { + renderer = TestRenderer.create(createElement(Probe)); + }); + + // Remote value arrives for the current build: statuses flip to applied. + hoisted.client.getFeatureFlag.mockImplementation( + (key: string) => (key === 'mobile-quick-chat' ? true : undefined) as never + ); + const onChange = hoisted.client.onFeatureFlags.mock.calls[0]?.[0] as () => void; + act(() => { + onChange(); + }); + + const latest = statuses.at(-1) ?? []; + const quickChat = latest.find(status => status.key === 'mobile-quick-chat'); + expect(quickChat).toMatchObject({ applied: true, value: true, reason: 'applied' }); + act(() => { + renderer?.unmount(); + }); + }); +}); diff --git a/apps/mobile/src/lib/analytics/posthog.ts b/apps/mobile/src/lib/analytics/posthog.ts index 7567bb3597..db10c0fafe 100644 --- a/apps/mobile/src/lib/analytics/posthog.ts +++ b/apps/mobile/src/lib/analytics/posthog.ts @@ -9,6 +9,12 @@ import PostHog, { PostHogPersistedProperty } from 'posthog-react-native'; import { useCallback, useSyncExternalStore } from 'react'; import { POSTHOG_API_KEY } from '@/lib/config'; +import { + currentAppVersion, + FEATURE_FLAG_DEFINITIONS, + getFeatureFlagDefinition, + isAppVersionAtLeast, +} from '@/lib/feature-flags'; import { allowsOptional, currentGeneration } from '@/lib/telemetry/controller'; import { posthogCustomStorage, @@ -70,9 +76,14 @@ export { export type AnalyticsSurface = (typeof ANALYTICS_SURFACES)[number]; // PostHog feature flags. The project is shared with web, so mobile-only flags -// are prefixed to avoid colliding with web flag keys. -export const FEATURE_FLAG_PR_REVIEW = 'mobile-pr-review'; -export const FEATURE_FLAG_QUICK_CHAT = 'mobile-quick-chat'; +// are prefixed to avoid colliding with web flag keys. The keys and their +// version gates live in `@/lib/feature-flags`; they are re-exported here so +// existing `@/lib/analytics/posthog` imports keep working unchanged. +export { + FEATURE_FLAG_PR_REVIEW, + FEATURE_FLAG_QUICK_CHAT, + type FeatureFlagDefinition, +} from '@/lib/feature-flags'; let client: PostHog | null = null; /** Generation that created the client. Stale events from a prior account @@ -199,6 +210,7 @@ export function initPostHog(): void { customStorage: posthogCustomStorage, }); clientGeneration = currentGeneration(); + statusRevision += 1; notifyPostHogReady(); // Super property on every event so dashboards can filter mobile vs web // without relying on $lib. @@ -207,6 +219,8 @@ export function initPostHog(): void { // even before (or without) an identified user. Triggers a flag reload. client.setPersonPropertiesForFlags(appVersionProperties()); client.onFeatureFlags(() => { + // Remote values changed: the statuses snapshot is stale. + statusRevision += 1; for (const listener of flagListeners) { listener(); } @@ -344,6 +358,7 @@ export async function discardPostHog(): Promise { // oxlint-disable-next-line anti-slop/no-runtime-typeof -- see comment above: guards against a client that violates its own type's contract if (typeof c?.setPersistedProperty !== 'function') { client = null; + statusRevision += 1; notifyPostHogReady(); return; } @@ -360,6 +375,7 @@ export async function discardPostHog(): Promise { // Ready listeners persist — they must observe both the false transition // now and a true transition from a later initPostHog. client = null; + statusRevision += 1; notifyPostHogReady(); try { @@ -401,15 +417,85 @@ export async function resumePostHog(): Promise { unsealPostHogStorage(); } +/** + * Resolve one flag for this build. A registered flag whose minimum app version + * is above this build's version is never applied — the build predates the + * flag, so it falls back to the definition's default whatever PostHog returns. + * Keys without a definition keep the legacy behavior (apply the remote value). + */ function isFeatureEnabled(key: string, defaultValue: boolean): boolean { + const definition = getFeatureFlagDefinition(key); + if (definition && !isAppVersionAtLeast(currentAppVersion(), definition.minAppVersion)) { + return definition.defaultValue; + } const value = client?.getFeatureFlag(key); return value === undefined ? defaultValue : value === true; } +/** How the version gate resolved for one flag (see `FeatureFlagStatus`). */ +export type FeatureFlagStatusReason = 'applied' | 'build-too-old'; + +/** One registry flag's resolved state for this build, for the debug surface. */ +export type FeatureFlagStatus = Readonly<{ + key: string; + minAppVersion: string; + defaultValue: boolean; + /** This build's version; null when the platform does not report one. */ + appVersion: string | null; + /** True when the remote value is trusted and used. */ + applied: boolean; + /** The value the UI acts on. */ + value: boolean; + reason: FeatureFlagStatusReason; + /** True when the client had a remote value for the key. */ + loaded: boolean; +}>; + +// `useFeatureFlagStatuses` returns a stable snapshot between flag updates, so +// `useSyncExternalStore` does not loop on a fresh array each read. +let statusRevision = 0; +let statusCache: { revision: number; statuses: FeatureFlagStatus[] } | null = null; + +function buildStatuses(): FeatureFlagStatus[] { + // Registry order is the row order on the debug surface. + return FEATURE_FLAG_DEFINITIONS.map(definition => { + const appVersion = currentAppVersion(); + const remote = client?.getFeatureFlag(definition.key); + const loaded = remote !== undefined; + const tooOld = !isAppVersionAtLeast(appVersion, definition.minAppVersion); + if (tooOld) { + return { + key: definition.key, + minAppVersion: definition.minAppVersion, + defaultValue: definition.defaultValue, + appVersion, + applied: false, + value: definition.defaultValue, + reason: 'build-too-old', + loaded, + }; + } + return { + key: definition.key, + minAppVersion: definition.minAppVersion, + defaultValue: definition.defaultValue, + appVersion, + applied: loaded, + value: loaded ? remote === true : definition.defaultValue, + reason: 'applied', + loaded, + }; + }); +} + /** * Reactively read a boolean feature flag. Fails open: while the client is * disabled (dev builds), uninitialized, or flags have not loaded yet, returns * `defaultValue`. Flags only ever flip UI off on an explicit `false`. + * + * Version-gated: a registered flag (see `@/lib/feature-flags`) applies its + * remote value only when this build is at or above the flag's minimum app + * version; older builds get the flag's default instead. */ export function useFeatureFlag(key: string, defaultValue = false): boolean { const subscribe = useCallback((onChange: () => void) => { @@ -421,3 +507,29 @@ export function useFeatureFlag(key: string, defaultValue = false): boolean { const getSnapshot = useCallback(() => isFeatureEnabled(key, defaultValue), [key, defaultValue]); return useSyncExternalStore(subscribe, getSnapshot); } + +/** + * All registry flags' resolved state for this build (see `FeatureFlagStatus`), + * for the debug surface in Preferences. Stable reference between flag + * updates, so it is safe as a `useSyncExternalStore` snapshot. + */ +export function getFeatureFlagStatuses(): FeatureFlagStatus[] { + if (statusCache?.revision !== statusRevision) { + statusCache = { revision: statusRevision, statuses: buildStatuses() }; + } + return statusCache.statuses; +} + +/** + * Reactively read every registry flag's status. Updates with the same flag + * lifecycle as `useFeatureFlag`: on client creation, discard, and flag loads. + */ +export function useFeatureFlagStatuses(): FeatureFlagStatus[] { + const subscribe = useCallback((onChange: () => void) => { + flagListeners.add(onChange); + return () => { + flagListeners.delete(onChange); + }; + }, []); + return useSyncExternalStore(subscribe, getFeatureFlagStatuses); +} diff --git a/apps/mobile/src/lib/feature-flags.test.ts b/apps/mobile/src/lib/feature-flags.test.ts new file mode 100644 index 0000000000..b4e0c900be --- /dev/null +++ b/apps/mobile/src/lib/feature-flags.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + compareAppVersions, + currentAppVersion, + FEATURE_FLAG_DEFINITIONS, + FEATURE_FLAG_PR_REVIEW, + FEATURE_FLAG_QUICK_CHAT, + getFeatureFlagDefinition, + isAppVersionAtLeast, +} from './feature-flags'; + +// expo-application is a native module whose source pulls in react-native +// (Flow-typed), which the pure vitest pipeline cannot parse. Mock it with a +// mutable version so the gate is tested against the build under test. +// (vi.mock is hoisted above the imports by vitest.) +const hoisted = vi.hoisted(() => ({ + nativeApplicationVersion: '1.0.8' as string | undefined, +})); +vi.mock('expo-application', () => ({ + get nativeApplicationVersion() { + return hoisted.nativeApplicationVersion; + }, +})); + +describe('compareAppVersions', () => { + it.each([ + ['1.0.8', '1.0.8', 0], + ['1.0.8', '1.0.4', 1], + ['1.0.4', '1.0.8', -1], + ['1.10.0', '1.9.9', 1], + ['2.0.0', '1.99.99', 1], + ['1.0', '1.0.0', 0], + ['1', '1.0.1', -1], + ['0.9', '1.0.0', -1], + ] as const)('compares %s vs %s', (a, b, expected) => { + expect(compareAppVersions(a, b)).toBe(expected); + }); + + it('treats non-numeric segments as zero instead of throwing', () => { + expect(compareAppVersions('1.0.x', '1.0.0')).toBe(0); + }); +}); + +describe('isAppVersionAtLeast', () => { + it('clears the gate at and above the minimum', () => { + expect(isAppVersionAtLeast('1.0.6', '1.0.6')).toBe(true); + expect(isAppVersionAtLeast('1.0.8', '1.0.6')).toBe(true); + expect(isAppVersionAtLeast('2.0.0', '1.0.6')).toBe(true); + }); + + it('holds the gate below the minimum and for an unknown version', () => { + expect(isAppVersionAtLeast('1.0.5', '1.0.6')).toBe(false); + expect(isAppVersionAtLeast('0.9.9', '1.0.6')).toBe(false); + expect(isAppVersionAtLeast(null, '1.0.6')).toBe(false); + expect(isAppVersionAtLeast(undefined, '1.0.6')).toBe(false); + expect(isAppVersionAtLeast('', '1.0.6')).toBe(false); + }); +}); + +describe('registry', () => { + it('registers both shipped flags with their first release', () => { + expect(FEATURE_FLAG_DEFINITIONS).toEqual([ + { key: FEATURE_FLAG_PR_REVIEW, minAppVersion: '1.0.4', defaultValue: true }, + { key: FEATURE_FLAG_QUICK_CHAT, minAppVersion: '1.0.6', defaultValue: false }, + ]); + }); + + it('looks a definition up by key', () => { + expect(getFeatureFlagDefinition(FEATURE_FLAG_QUICK_CHAT)?.minAppVersion).toBe('1.0.6'); + expect(getFeatureFlagDefinition('mobile-unknown')).toBeUndefined(); + }); + + it('reads the running build version, null when absent', () => { + hoisted.nativeApplicationVersion = '1.0.8'; + expect(currentAppVersion()).toBe('1.0.8'); + hoisted.nativeApplicationVersion = undefined; + expect(currentAppVersion()).toBeNull(); + }); +}); diff --git a/apps/mobile/src/lib/feature-flags.ts b/apps/mobile/src/lib/feature-flags.ts new file mode 100644 index 0000000000..8c558aae41 --- /dev/null +++ b/apps/mobile/src/lib/feature-flags.ts @@ -0,0 +1,79 @@ +import * as Application from 'expo-application'; + +/** + * Version-aware feature flags. + * + * Every flag a mobile build reads must be registered here with the oldest app + * version that understands it. The app applies a flag's remote value only when + * its own version is at or above that minimum, and falls back to the flag's + * default otherwise — a flag that a build does not understand must never + * change that build's behaviour, whatever PostHog returns. + * + * Workflow: when a flag (or a renamed one) is introduced in version X, register + * it with `minAppVersion: X`. Older builds that already carry this registry + * then skip the key they do not understand instead of acting on it. + */ + +export const FEATURE_FLAG_PR_REVIEW = 'mobile-pr-review'; +export const FEATURE_FLAG_QUICK_CHAT = 'mobile-quick-chat'; + +export type FeatureFlagDefinition = Readonly<{ + key: string; + /** Oldest app version that understands this flag. Lower builds fall back to `defaultValue`. */ + minAppVersion: string; + /** Value used when the build is too old or the remote value is not loaded. Must match every `useFeatureFlag` call site for the key. */ + defaultValue: boolean; +}>; + +/** + * The registry the debug surface lists. Minimums record the first release that + * shipped each flag's reading code: + * - `mobile-pr-review` (#4669) first released in 1.0.4. + * - `mobile-quick-chat` (#5541) first released in 1.0.6. + */ +export const FEATURE_FLAG_DEFINITIONS: readonly FeatureFlagDefinition[] = [ + { key: FEATURE_FLAG_PR_REVIEW, minAppVersion: '1.0.4', defaultValue: true }, + { key: FEATURE_FLAG_QUICK_CHAT, minAppVersion: '1.0.6', defaultValue: false }, +]; + +const DEFINITIONS_BY_KEY: ReadonlyMap = new Map( + FEATURE_FLAG_DEFINITIONS.map(definition => [definition.key, definition]) +); + +export function getFeatureFlagDefinition(key: string): FeatureFlagDefinition | undefined { + return DEFINITIONS_BY_KEY.get(key); +} + +/** This build's version, or null when the platform does not report one. */ +export function currentAppVersion(): string | null { + return Application.nativeApplicationVersion ?? null; +} + +/** + * Compare dotted numeric version strings (`1.10.0` > `1.9.9`, missing + * segments count as zero). Returns <0, 0, or >0 like `strcmp`. + */ +export function compareAppVersions(a: string, b: string): number { + const left = a.split('.').map(segment => Number.parseInt(segment, 10) || 0); + const right = b.split('.').map(segment => Number.parseInt(segment, 10) || 0); + const length = Math.max(left.length, right.length); + for (let index = 0; index < length; index += 1) { + const delta = (left[index] ?? 0) - (right[index] ?? 0); + if (delta !== 0) { + return delta < 0 ? -1 : 1; + } + } + return 0; +} + +/** + * True when `current` is at or above `min`. An unknown build version never + * clears the gate: a build that cannot prove its age does not get to apply a + * flag it may not understand, so the flag's default holds. + */ +export function isAppVersionAtLeast(current: string | null | undefined, min: string): boolean { + if (!current) { + return false; + } + return compareAppVersions(current, min) >= 0; +} diff --git a/tools/i18n/check-catalogs.mjs b/tools/i18n/check-catalogs.mjs index e28676cfa5..00e7c6f6ba 100644 --- a/tools/i18n/check-catalogs.mjs +++ b/tools/i18n/check-catalogs.mjs @@ -79,6 +79,13 @@ const ENGLISH_IDENTICAL_ALLOWLIST = new Set([ 'profile.providerAnaconda', 'profile.providerApple', 'profile.providerDiscord', + // Feature-flag debug surface (Preferences): version comparisons and the + // source of a row's value are notation — flag keys, versions and the + // remote/default markers are technical tokens, not translatable prose. + 'preferences.featureFlagsBuild', + 'preferences.featureFlagApplied', + 'preferences.featureFlagSkipped', + 'preferences.featureFlagNotLoaded', 'common.github', 'common.gitlab', 'profile.providerGoogle',