Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ vi.mock('react-native', () => ({
// resolves to its untransformed `react-native` entry (`src/index.tsx`): the
// CommonJS entry requires a Flow react-native subpath this node project cannot
// load, and every mounted suite mocks it. So stub the module's only native
// dependency instead of the module itself, and keep the real footer and lift.
// dependency instead of the module itself, and keep the real footer and lift:
// a whole-module mock of `app-aware-keyboard-padding` strips the hook the
// footer reads and the lift assertions below fail.
vi.mock('react-native-safe-area-context', () => ({
useSafeAreaInsets: () => ({ top: 0, bottom: BOTTOM_INSET, left: 0, right: 0 }),
}));
Expand Down
27 changes: 25 additions & 2 deletions apps/mobile/src/components/feature-flags-section.mounted.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { createElement } from 'react';
import { act, type ReactTestRenderer } from '@/test/renderer';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import '@/i18n';
import { i18n } from '@/i18n';
import { FeatureFlagsSection } from '@/components/feature-flags-section';
import { renderWithProviders } from '@/test/render-with-providers';

Expand Down Expand Up @@ -51,6 +51,9 @@ afterEach(() => {
view = undefined;
vi.unstubAllGlobals();
});
afterAll(async () => {
await i18n.changeLanguage('en');
});

const applied = {
key: 'mobile-pr-review',
Expand Down Expand Up @@ -141,4 +144,24 @@ describe('FeatureFlagsSection', () => {

expect(textLines(tree)).toEqual([]);
});

it('reads the whole row from the Arabic catalog', async () => {
// The explorer capture showed the fully Arabic Preferences screen mixing
// untranslated developer English into each flag row ("مفعّل · default ·
// not loaded"). Every word of the row, not only the value, must come from
// the Arabic catalog; the check below states that as "no ASCII letter in a
// row line".
await i18n.changeLanguage('ar');
posthog.statuses = [applied, skipped, unloaded];
const tree = await mount();

const lines = textLines(tree);
expect(lines).toContain('أعلام الميزات');
expect(lines).toContain('مفعّل · من الخادم · ≥ 1.0.4');
expect(lines).toContain('معطّل · افتراضي · < 1.0.6');
expect(lines).toContain('مفعّل · افتراضي · لم يتم التحميل');
for (const line of lines.filter(text => text.includes(' · '))) {
expect(line, `untranslated row copy: ${line}`).not.toMatch(/[A-Za-z]/);
}
});
});
5 changes: 3 additions & 2 deletions apps/mobile/src/components/feature-flags-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import { type FeatureFlagStatus, useFeatureFlagStatuses } from '@/lib/analytics/
* Each row reads `<value> · <source> · <relation>`, 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. Every word
* comes from the catalog — the value, the source and the state; only the
* `{{min}}`/`v{{version}}` versions and the `≥`/`<` operators are notation.
* comes from the catalog — the value, the source and the state; only the flag
* key, the `{{min}}`/`v{{version}}` versions and the `≥`/`<` operators are
* notation.
*/
function FlagRow({ status }: { status: FeatureFlagStatus }) {
const { t } = useTranslation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ describe('Text tracked labels in RTL', () => {
expect(hostText(root).props.style).toBeUndefined();
});

it('applies the same reset to the shared Eyebrow label', () => {
it('drops the Eyebrow display treatment and still resets letter spacing in RTL', () => {
// The eyebrow's tracking class is LTR-only (text.tsx EYEBROW_LATIN_DISPLAY):
// an RTL eyebrow drops it and relies on the RTL letter-spacing reset.
i18nManager.isRTL = true;
const root = mount(createElement(Eyebrow, null, 'استكشف'));

Expand Down
34 changes: 34 additions & 0 deletions apps/mobile/src/i18n/catalog-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ const PLURAL_SUFFIX = /_(?:zero|one|two|few|many|other)$/;
*/
const PENDING_TRANSLATION_KEYS = new Set<string>();

/**
* The three copy keys the feature-flag row renders: `<value> · <reason>` under
* `preferences`. The row's value word comes from `common.enabled`/`common.off`,
* which every catalog already translates, so a catalog that keeps the English
* reason string leaves half the row untranslated — the explorer capture
* `language-switch-blank` read `مفعّل · default · not loaded`. A loanword equal
* to the English string does not count here: each catalog renders its own words.
*/
const FEATURE_FLAG_ROW_KEYS = [
'preferences.featureFlagApplied',
'preferences.featureFlagSkipped',
'preferences.featureFlagNotLoaded',
];

function keyFamilies(value: unknown, prefix = '', out = new Set<string>()): Set<string> {
for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
const path = prefix ? `${prefix}.${key}` : key;
Expand All @@ -44,6 +58,15 @@ function keyFamilies(value: unknown, prefix = '', out = new Set<string>()): Set<
return out;
}

/** The value at a dotted key path, so a nested copy value can be compared. */
function readPath(value: unknown, path: string): unknown {
let node: unknown = value;
for (const part of path.split('.')) {
node = (node as Record<string, unknown> | undefined)?.[part];
}
return node;
}

const ENGLISH_FAMILIES = keyFamilies(CATALOG_LOADERS.en());

describe('catalog keys', () => {
Expand All @@ -59,6 +82,17 @@ describe('catalog keys', () => {
}
});

it.each(SUPPORTED_LANGUAGES.filter(tag => tag !== 'en'))(
'%s translates the feature-flag row copy',
tag => {
for (const key of FEATURE_FLAG_ROW_KEYS) {
expect(readPath(CATALOG_LOADERS[tag](), key), `${tag} leaves ${key} in English`).not.toBe(
readPath(CATALOG_LOADERS.en(), key)
);
}
}
);

it.each(SUPPORTED_LANGUAGES.filter(tag => tag !== 'en'))(
'%s defines exactly the English key families',
tag => {
Expand Down
6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/af.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/mobile/src/i18n/locales/am.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/mobile/src/i18n/locales/ar.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/az.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/be.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/bg.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/mobile/src/i18n/locales/bn.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/bs.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/ca.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/ckb.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/cs.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/cy.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/da.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/mobile/src/i18n/locales/de.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/el.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions apps/mobile/src/i18n/locales/es.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading