i18n: RTL smoke tests, Portuguese locale and a translation guide

- Add rtl_smoke_test covering inventory, detail (lot sheet) and settings
  under Directionality.rtl, with an Arabic label and vernacular name.
- Ship pt.i18n.json (Portuguese) as proof the locale pipeline scales beyond
  es/en; wire its language tile in Settings.
- docs/i18n.md documents the Weblate-ready flow for adding locales.
This commit is contained in:
vjrj 2026-07-09 23:06:49 +02:00
parent d6781870d9
commit 5d2b41a110
11 changed files with 1736 additions and 6 deletions

View file

@ -60,6 +60,7 @@ Widget wrapScreen({
required VarietyRepository repository,
required Widget child,
AppLocale locale = AppLocale.en,
TextDirection? textDirection,
}) {
LocaleSettings.setLocaleSync(locale);
return TranslationProvider(
@ -75,7 +76,9 @@ Widget wrapScreen({
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: child,
home: textDirection == null
? child
: Directionality(textDirection: textDirection, child: child),
),
),
),
@ -88,6 +91,7 @@ Widget wrapDetail({
required String varietyId,
SpeciesRepository? species,
AppLocale locale = AppLocale.en,
TextDirection? textDirection,
}) {
LocaleSettings.setLocaleSync(locale);
return TranslationProvider(
@ -106,7 +110,12 @@ Widget wrapDetail({
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: const VarietyDetailScreen(),
home: textDirection == null
? const VarietyDetailScreen()
: Directionality(
textDirection: textDirection,
child: const VarietyDetailScreen(),
),
),
),
),

View file

@ -0,0 +1,99 @@
import 'package:commons_core/commons_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tane/db/database.dart';
import 'package:tane/db/enums.dart';
import 'package:tane/ui/inventory_list_screen.dart';
import 'package:tane/ui/settings_screen.dart';
import '../support/test_support.dart';
/// RTL is a first-class requirement (CLAUDE.md): the main screens must build
/// and lay out under right-to-left without overflowing or misplacing
/// directional affordances. Overflow errors fail these tests automatically.
void main() {
late AppDatabase db;
setUp(() => db = newTestDatabase());
tearDown(() => db.close());
testWidgets('the inventory list renders under RTL', (tester) async {
final repo = newTestRepository(db);
final id = await repo.addQuickVariety(
label: 'طماطم بلدية', // an Arabic label, as a keeper would write it
category: 'Solanaceae',
);
await repo.addLot(
varietyId: id,
harvestYear: 2024,
quantity: const Quantity(kind: QuantityKind.handful),
offerStatus: OfferStatus.shared,
);
await repo.addQuickVariety(label: 'Maize', category: 'Poaceae');
await tester.pumpWidget(
wrapScreen(
repository: repo,
textDirection: TextDirection.rtl,
child: const InventoryListScreen(),
),
);
await tester.pumpAndSettle();
expect(find.text('طماطم بلدية'), findsOneWidget);
expect(find.byKey(const Key('inventory.filter.sharing')), findsOneWidget);
await disposeTree(tester);
});
testWidgets('the variety detail (lots, calendar, names) renders under RTL', (
tester,
) async {
final repo = newTestRepository(db);
final id = await repo.addQuickVariety(
label: 'Grandma tomato',
category: 'Solanaceae',
);
await repo.addVernacularName(id, 'طماطم');
await repo.addLot(
varietyId: id,
harvestYear: 2023,
quantity: const Quantity(kind: QuantityKind.packet, count: 3),
abundance: Abundance.plentyToShare,
offerStatus: OfferStatus.exchange,
);
await tester.pumpWidget(
wrapDetail(
repository: repo,
varietyId: id,
species: newTestSpeciesRepository(db),
textDirection: TextDirection.rtl,
),
);
await tester.pumpAndSettle();
expect(find.text('Grandma tomato'), findsOneWidget);
expect(find.text('طماطم'), findsOneWidget);
// The lot sheet (the busiest form) must also lay out under RTL.
await tester.tap(find.byKey(const Key('detail.addLot')));
await tester.pumpAndSettle();
expect(find.byKey(const Key('addLot.save')), findsOneWidget);
await disposeTree(tester);
});
testWidgets('settings renders under RTL', (tester) async {
final repo = newTestRepository(db);
await tester.pumpWidget(
wrapScreen(
repository: repo,
textDirection: TextDirection.rtl,
child: const SettingsScreen(),
),
);
await tester.pumpAndSettle();
expect(find.text('Português'), findsOneWidget);
await disposeTree(tester);
});
}