fix(i18n): 印尼语内容缺失时不再回退中文

This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-30 17:14:37 +08:00
1 parent 9ef8eb15b0
commit 19196ceb8e
13 files changed
+261 -95

No files matched your search

+2 -2
View File
@@ -241,11 +241,11 @@ class Drama {
} }
String localizedTitle(String languageCode) { String localizedTitle(String languageCode) {
return localizedText(languageCode, title, titleLocales); return localizedRequiredText(languageCode, title, titleLocales);
} }
String localizedSynopsis(String languageCode) { String localizedSynopsis(String languageCode) {
return localizedText(languageCode, synopsis, synopsisLocales); return localizedRequiredText(languageCode, synopsis, synopsisLocales);
} }
String localizedTag(String languageCode) { String localizedTag(String languageCode) {
+18 -1
View File
@@ -7,10 +7,27 @@ Map<String, String> localeMap(Object? raw) {
} }
/// 按当前语言取文案。没有对应语言时用中文,中文也没有时用 fallback。 /// 按当前语言取文案。没有对应语言时用中文,中文也没有时用 fallback。
String localizedText(String languageCode, String fallback, Map<String, String> locales) { String localizedText(
String languageCode,
String fallback,
Map<String, String> locales,
) {
final selected = locales[languageCode] ?? ''; final selected = locales[languageCode] ?? '';
if (selected.isNotEmpty) return selected; if (selected.isNotEmpty) return selected;
final chinese = locales['zh'] ?? ''; final chinese = locales['zh'] ?? '';
if (chinese.isNotEmpty) return chinese; if (chinese.isNotEmpty) return chinese;
return fallback; return fallback;
} }
/// 按当前语言取必须本地化的文案;印尼语缺失时不回退中文。
String localizedRequiredText(
String languageCode,
String fallback,
Map<String, String> locales,
) {
final primaryCode = languageCode.split(RegExp(r'[_-]')).first;
final selected = locales[languageCode] ?? locales[primaryCode] ?? '';
if (selected.trim().isNotEmpty) return selected;
if (primaryCode == 'id') return '';
return localizedText(primaryCode, fallback, locales);
}
@@ -0,0 +1,21 @@
// Author: fruit.com.cn
import '../data/models/drama.dart';
const String _indonesianLanguageCode = 'id';
/// 判断短剧是否满足当前界面语言的基础文案完整度。
bool isDramaCompleteForLocale(Drama drama, String languageCode) {
final primaryCode = languageCode.split(RegExp(r'[_-]')).first;
if (primaryCode != _indonesianLanguageCode) return true;
return drama.localizedTitle(primaryCode).trim().isNotEmpty &&
drama.localizedSynopsis(primaryCode).trim().isNotEmpty;
}
/// 过滤不满足首发语言文案完整度的短剧,保持原顺序。
List<Drama> visibleDramasForLocale(
Iterable<Drama> dramas,
String languageCode,
) => dramas
.where((drama) => isDramaCompleteForLocale(drama, languageCode))
.toList(growable: false);
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../../../core/data/models/drama.dart'; import '../../../core/data/models/drama.dart';
import '../../../core/formatting/error_text.dart'; import '../../../core/formatting/error_text.dart';
import '../../../core/formatting/content_locale_guard.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/widgets/empty_state.dart'; import '../../../core/widgets/empty_state.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
@@ -33,10 +34,18 @@ class DiscoverSearchResults extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
if (!loading && error == null && results.isNotEmpty) { final visibleResults = visibleDramasForLocale(
results,
Localizations.localeOf(context).languageCode,
);
if (!loading && error == null && visibleResults.isNotEmpty) {
return SliverPadding( return SliverPadding(
padding: _topPadding, padding: _topPadding,
sliver: SliverDramaGrid(l10n: l10n, dramas: results, onOpen: onOpen), sliver: SliverDramaGrid(
l10n: l10n,
dramas: visibleResults,
onOpen: onOpen,
),
); );
} }
return SliverPadding( return SliverPadding(
@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import '../../../core/data/models/drama.dart'; import '../../../core/data/models/drama.dart';
import '../../../core/data/services/category_controller.dart'; import '../../../core/data/services/category_controller.dart';
import '../../../core/formatting/content_locale_guard.dart';
import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/app_pull_to_refresh.dart'; import '../../../core/widgets/app_pull_to_refresh.dart';
import '../../../core/widgets/drama_grid_skeleton.dart'; import '../../../core/widgets/drama_grid_skeleton.dart';
@@ -77,7 +78,10 @@ class _HomeCategoryDramasState extends State<HomeCategoryDramas> {
final controller = context.watch<CategoryController>(); final controller = context.watch<CategoryController>();
final categoryId = widget.lockedCategoryId ?? ''; final categoryId = widget.lockedCategoryId ?? '';
final slot = categoryId.isEmpty ? null : controller.slotOf(categoryId); final slot = categoryId.isEmpty ? null : controller.slotOf(categoryId);
final dramas = slot?.dramas ?? const <Drama>[]; final dramas = visibleDramasForLocale(
slot?.dramas ?? const <Drama>[],
Localizations.localeOf(context).languageCode,
);
return Column( return Column(
children: [ children: [
Expanded( Expanded(
@@ -164,9 +168,8 @@ class _HomeCategoryDramasState extends State<HomeCategoryDramas> {
hasMore: slot?.hasMore ?? false, hasMore: slot?.hasMore ?? false,
hasItems: dramas.isNotEmpty, hasItems: dramas.isNotEmpty,
errorMessage: null, errorMessage: null,
onRetry: () => context.read<CategoryController>().loadMore( onRetry: () =>
categoryId, context.read<CategoryController>().loadMore(categoryId),
),
), ),
), ),
), ),
+13 -3
View File
@@ -12,6 +12,7 @@ import '../../../core/data/services/catalog_controller.dart';
import '../../../core/data/services/category_controller.dart'; import '../../../core/data/services/category_controller.dart';
import '../../../core/data/services/home_feed_controller.dart'; import '../../../core/data/services/home_feed_controller.dart';
import '../../../core/data/services/wallet_controller.dart'; import '../../../core/data/services/wallet_controller.dart';
import '../../../core/formatting/content_locale_guard.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/app_pull_to_refresh.dart'; import '../../../core/widgets/app_pull_to_refresh.dart';
@@ -194,7 +195,10 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
mainAxisSpacing: AppDimens.homeGridRowGap, mainAxisSpacing: AppDimens.homeGridRowGap,
); );
} }
final dramas = _dramasFor(category); final dramas = _dramasFor(
category,
Localizations.localeOf(context).languageCode,
);
if (widget.feedController.loadingFirstPage && dramas.isEmpty) { if (widget.feedController.loadingFirstPage && dramas.isEmpty) {
return const DramaGridSkeleton( return const DramaGridSkeleton(
key: Key('home-grid-skeleton'), key: Key('home-grid-skeleton'),
@@ -324,8 +328,14 @@ class _HomeFeedGridState extends State<HomeFeedGrid> {
]; ];
} }
List<Drama> _dramasFor(HomeGridCategoryShortcut category) { List<Drama> _dramasFor(
final dramas = widget.feedController.dramas; HomeGridCategoryShortcut category,
String languageCode,
) {
final dramas = visibleDramasForLocale(
widget.feedController.dramas,
languageCode,
);
final categoryName = category.categoryName.toLowerCase(); final categoryName = category.categoryName.toLowerCase();
if (categoryName.isEmpty) return dramas; if (categoryName.isEmpty) return dramas;
final matched = dramas final matched = dramas
@@ -9,6 +9,7 @@ import '../../core/data/models/drama.dart';
import '../../core/data/models/home_feed.dart'; import '../../core/data/models/home_feed.dart';
import '../../core/data/remote/catalog_api.dart'; import '../../core/data/remote/catalog_api.dart';
import '../../core/data/services/category_controller.dart'; import '../../core/data/services/category_controller.dart';
import '../../core/formatting/content_locale_guard.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/widgets/app_pull_to_refresh.dart'; import '../../core/widgets/app_pull_to_refresh.dart';
import '../../core/widgets/empty_state.dart'; import '../../core/widgets/empty_state.dart';
@@ -284,7 +285,10 @@ class _ChannelFeedState extends State<_ChannelFeed>
super.build(context); super.build(context);
final categories = context.watch<CategoryController>(); final categories = context.watch<CategoryController>();
final slot = categories.slotOf(widget.categoryId); final slot = categories.slotOf(widget.categoryId);
final dramas = slot.dramas; final dramas = visibleDramasForLocale(
slot.dramas,
Localizations.localeOf(context).languageCode,
);
final showCarousel = widget.isFeatured && widget.banners.isNotEmpty; final showCarousel = widget.isFeatured && widget.banners.isNotEmpty;
final bottomInset = MediaQuery.of(context).padding.bottom; final bottomInset = MediaQuery.of(context).padding.bottom;
+13 -1
View File
@@ -13,6 +13,7 @@ import '../../core/data/services/download_controller.dart';
import '../../core/data/services/home_feed_controller.dart'; import '../../core/data/services/home_feed_controller.dart';
import '../../core/data/services/playlist_controller.dart'; import '../../core/data/services/playlist_controller.dart';
import '../../core/data/services/wallet_controller.dart'; import '../../core/data/services/wallet_controller.dart';
import '../../core/formatting/content_locale_guard.dart';
import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors.dart';
import '../../core/widgets/empty_state.dart'; import '../../core/widgets/empty_state.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
@@ -289,7 +290,10 @@ class _RecommendPageState extends State<RecommendPage>
Widget _recommendFeed(BuildContext context) { Widget _recommendFeed(BuildContext context) {
final feed = widget.feedController; final feed = widget.feedController;
final dramas = widget.feedController.dramas; final dramas = visibleDramasForLocale(
widget.feedController.dramas,
Localizations.localeOf(context).languageCode,
);
if (dramas.isNotEmpty) { if (dramas.isNotEmpty) {
return PlayerPage( return PlayerPage(
key: const Key('recommend-feed-player'), key: const Key('recommend-feed-player'),
@@ -327,6 +331,9 @@ class _RecommendPageState extends State<RecommendPage>
key: const Key('recommend-feed-empty'), key: const Key('recommend-feed-empty'),
icon: Icons.wifi_off_rounded, icon: Icons.wifi_off_rounded,
title: l10n.networkError, title: l10n.networkError,
actionLabel: l10n.retry,
actionKey: const Key('recommend-feed-retry'),
onAction: _retryRecommendData,
), ),
); );
} }
@@ -340,6 +347,11 @@ class _RecommendPageState extends State<RecommendPage>
); );
} }
void _retryRecommendData() {
_cancelRecommendRetries();
unawaited(_refreshRecommendData());
}
PlaylistController? _watchPlaylists(BuildContext context) { PlaylistController? _watchPlaylists(BuildContext context) {
try { try {
return context.watch<PlaylistController>(); return context.watch<PlaylistController>();
@@ -10,6 +10,7 @@ import '../../../core/data/models/drama.dart';
import '../../../core/data/remote/catalog_api.dart'; import '../../../core/data/remote/catalog_api.dart';
import '../../../core/data/services/all_dramas_controller.dart'; import '../../../core/data/services/all_dramas_controller.dart';
import '../../../core/data/services/home_feed_controller.dart'; import '../../../core/data/services/home_feed_controller.dart';
import '../../../core/formatting/content_locale_guard.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/app_pull_to_refresh.dart'; import '../../../core/widgets/app_pull_to_refresh.dart';
@@ -49,7 +50,8 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_library = widget.controller ?? _library =
widget.controller ??
AllDramasController( AllDramasController(
api: widget.catalogApi == null api: widget.catalogApi == null
? null ? null
@@ -74,8 +76,9 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
await widget.feedController?.loadMore(); await widget.feedController?.loadMore();
} }
List<Drama> get _dramas => List<Drama> get _dramas => _library.usesRemote
_library.usesRemote ? _library.dramas : widget.feedController?.dramas ?? _library.dramas; ? _library.dramas
: widget.feedController?.dramas ?? _library.dramas;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -86,7 +89,9 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_filters(l10n), _filters(l10n),
Expanded(child: _dramaList(l10n, _itemExtent(constraints.maxWidth))), Expanded(
child: _dramaList(l10n, _itemExtent(constraints.maxWidth)),
),
], ],
); );
}, },
@@ -95,13 +100,18 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
double _itemExtent(double width) { double _itemExtent(double width) {
final itemWidth = final itemWidth =
(width - AppDimens.allDramasHorizontal * 2 - AppDimens.allDramasGridGap * 2) / (width -
AppDimens.allDramasHorizontal * 2 -
AppDimens.allDramasGridGap * 2) /
_gridColumns; _gridColumns;
return itemWidth / AppDimens.cardCoverAspectRatio + _cardBodyExtent; return itemWidth / AppDimens.cardCoverAspectRatio + _cardBodyExtent;
} }
Widget _dramaList(AppLocalizations l10n, double itemExtent) { Widget _dramaList(AppLocalizations l10n, double itemExtent) {
final dramas = _dramas; final dramas = visibleDramasForLocale(
_dramas,
Localizations.localeOf(context).languageCode,
);
return NotificationListener<ScrollNotification>( return NotificationListener<ScrollNotification>(
onNotification: _onScroll, onNotification: _onScroll,
child: AppPullToRefresh( child: AppPullToRefresh(
@@ -140,7 +150,9 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
Widget _grid(List<Drama> dramas, double itemExtent) { Widget _grid(List<Drama> dramas, double itemExtent) {
return SliverPadding( return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: AppDimens.allDramasHorizontal), padding: const EdgeInsets.symmetric(
horizontal: AppDimens.allDramasHorizontal,
),
sliver: SliverGrid( sliver: SliverGrid(
key: const Key('all-dramas-grid'), key: const Key('all-dramas-grid'),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
@@ -229,7 +241,11 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
); );
} }
Widget _optionRow(_FilterRow spec, String languageCode, AppLocalizations l10n) { Widget _optionRow(
_FilterRow spec,
String languageCode,
AppLocalizations l10n,
) {
return _filterRow( return _filterRow(
row: spec.row, row: spec.row,
child: _horizontalChipScroll( child: _horizontalChipScroll(
@@ -302,7 +318,9 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
), ),
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: selected ? AppColors.filterChipSelected : AppColors.transparent, color: selected
? AppColors.filterChipSelected
: AppColors.transparent,
borderRadius: BorderRadius.circular(AppDimens.filterChipRadius), borderRadius: BorderRadius.circular(AppDimens.filterChipRadius),
), ),
child: Text( child: Text(
@@ -323,7 +341,8 @@ class _AllDramasFilterGridState extends State<AllDramasFilterGrid> {
bool _onScroll(ScrollNotification notification) { bool _onScroll(ScrollNotification notification) {
if (notification.depth != 0) return false; if (notification.depth != 0) return false;
if (notification.metrics.pixels <= 0) return false; if (notification.metrics.pixels <= 0) return false;
final reachedBottom = notification.metrics.extentAfter <= _loadMoreThreshold; final reachedBottom =
notification.metrics.extentAfter <= _loadMoreThreshold;
if (reachedBottom && if (reachedBottom &&
(notification is ScrollUpdateNotification || (notification is ScrollUpdateNotification ||
notification is ScrollEndNotification || notification is ScrollEndNotification ||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import '../../../core/data/models/drama.dart'; import '../../../core/data/models/drama.dart';
import '../../../core/data/models/playlist_detail.dart'; import '../../../core/data/models/playlist_detail.dart';
import '../../../core/formatting/content_locale_guard.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/app_pull_to_refresh.dart'; import '../../../core/widgets/app_pull_to_refresh.dart';
@@ -54,8 +55,7 @@ class _PlaylistDetailPageState extends State<PlaylistDetailPage> {
bool _loadingMore = false; bool _loadingMore = false;
bool _failed = false; bool _failed = false;
bool get _hasMore => bool get _hasMore => widget.loadPage != null && _dramas.length < _dramaCount;
widget.loadPage != null && _dramas.length < _dramaCount;
Future<void> _refresh() async { Future<void> _refresh() async {
final loadPage = widget.loadPage; final loadPage = widget.loadPage;
@@ -112,7 +112,8 @@ class _PlaylistDetailPageState extends State<PlaylistDetailPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width; final width = MediaQuery.sizeOf(context).width;
final itemWidth = (width - _pageHorizontal * 2 - _gridGap) / 2; final itemWidth = (width - _pageHorizontal * 2 - _gridGap) / 2;
final itemExtent = itemWidth / AppDimens.cardCoverAspectRatio + _cardBodyExtent; final itemExtent =
itemWidth / AppDimens.cardCoverAspectRatio + _cardBodyExtent;
return Scaffold( return Scaffold(
key: const Key('playlist-detail-page'), key: const Key('playlist-detail-page'),
backgroundColor: AppColors.ink, backgroundColor: AppColors.ink,
@@ -157,6 +158,10 @@ class _PlaylistDetailPageState extends State<PlaylistDetailPage> {
} }
Widget _body(double itemExtent) { Widget _body(double itemExtent) {
final visibleDramas = visibleDramasForLocale(
_dramas,
Localizations.localeOf(context).languageCode,
);
return CustomScrollView( return CustomScrollView(
key: const Key('playlist-detail-scroll'), key: const Key('playlist-detail-scroll'),
physics: AppPullToRefresh.physics, physics: AppPullToRefresh.physics,
@@ -168,7 +173,9 @@ class _PlaylistDetailPageState extends State<PlaylistDetailPage> {
_pageHorizontal, _pageHorizontal,
12, 12,
), ),
sliver: SliverToBoxAdapter(child: _PlaylistCollage(dramas: _dramas)), sliver: SliverToBoxAdapter(
child: _PlaylistCollage(dramas: visibleDramas),
),
), ),
SliverPadding( SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: _pageHorizontal), padding: const EdgeInsets.symmetric(horizontal: _pageHorizontal),
@@ -192,10 +199,11 @@ class _PlaylistDetailPageState extends State<PlaylistDetailPage> {
), ),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) => HomeFeedCard( (context, index) => HomeFeedCard(
drama: _dramas[index], drama: visibleDramas[index],
onTap: () => playDramaFromCatalog(context, _dramas[index]), onTap: () =>
playDramaFromCatalog(context, visibleDramas[index]),
), ),
childCount: _dramas.length, childCount: visibleDramas.length,
), ),
), ),
), ),
@@ -205,9 +213,13 @@ class _PlaylistDetailPageState extends State<PlaylistDetailPage> {
child: PagedListFooter( child: PagedListFooter(
loading: _loadingMore, loading: _loadingMore,
hasMore: _hasMore, hasMore: _hasMore,
hasItems: _dramas.isNotEmpty, hasItems: visibleDramas.isNotEmpty,
errorMessage: _failed ? AppLocalizations.of(context)!.catalogError : null, errorMessage: _failed
onRetry: _loadingMore ? _loadMore : (_hasMore ? _loadMore : _refresh), ? AppLocalizations.of(context)!.catalogError
: null,
onRetry: _loadingMore
? _loadMore
: (_hasMore ? _loadMore : _refresh),
), ),
), ),
), ),
@@ -8,6 +8,7 @@ import '../../../core/data/models/playlist_detail.dart';
import '../../../core/data/services/category_controller.dart'; import '../../../core/data/services/category_controller.dart';
import '../../../core/data/services/home_feed_controller.dart'; import '../../../core/data/services/home_feed_controller.dart';
import '../../../core/data/services/playlist_controller.dart'; import '../../../core/data/services/playlist_controller.dart';
import '../../../core/formatting/content_locale_guard.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/app_pull_to_refresh.dart'; import '../../../core/widgets/app_pull_to_refresh.dart';
@@ -43,7 +44,8 @@ class RecommendSectionsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final source = feedController.dramas; final languageCode = Localizations.localeOf(context).languageCode;
final source = visibleDramasForLocale(feedController.dramas, languageCode);
final remote = playlists; final remote = playlists;
return AppPullToRefresh( return AppPullToRefresh(
key: const Key('recommend-sections-refresh'), key: const Key('recommend-sections-refresh'),
@@ -83,39 +85,39 @@ class RecommendSectionsView extends StatelessWidget {
source: source, source: source,
); );
return [ return [
_section( _section(
context, context,
l10n, l10n,
key: 'hits', key: 'hits',
title: l10n.weeklyHitsTitle, title: l10n.weeklyHitsTitle,
subtitle: l10n.weeklyHitsSubtitle, subtitle: l10n.weeklyHitsSubtitle,
dramas: hits.take(_sectionPreviewCount).toList(), dramas: hits.take(_sectionPreviewCount).toList(),
onMore: () => onMore(RecommendSectionKind.hits), onMore: () => onMore(RecommendSectionKind.hits),
), ),
const SizedBox(height: AppDimens.recommendSectionGap), const SizedBox(height: AppDimens.recommendSectionGap),
_section( _section(
context, context,
l10n, l10n,
key: 'new', key: 'new',
title: l10n.weeklyNewTitle, title: l10n.weeklyNewTitle,
subtitle: l10n.weeklyNewSubtitle, subtitle: l10n.weeklyNewSubtitle,
dramas: newest.take(_sectionPreviewCount).toList(), dramas: newest.take(_sectionPreviewCount).toList(),
onMore: () => onMore(RecommendSectionKind.newReleases), onMore: () => onMore(RecommendSectionKind.newReleases),
), ),
const SizedBox(height: AppDimens.recommendSectionGap), const SizedBox(height: AppDimens.recommendSectionGap),
_section( _section(
context, context,
l10n, l10n,
key: 'costume', key: 'costume',
title: l10n.costumeAwardsTitle, title: l10n.costumeAwardsTitle,
subtitle: l10n.costumeAwardsSubtitle, subtitle: l10n.costumeAwardsSubtitle,
dramas: recommendSectionDramas( dramas: recommendSectionDramas(
kind: RecommendSectionKind.costume, kind: RecommendSectionKind.costume,
source: source, source: source,
categories: categories, categories: categories,
).take(_sectionPreviewCount).toList(), ).take(_sectionPreviewCount).toList(),
onMore: () => onMore(RecommendSectionKind.costume), onMore: () => onMore(RecommendSectionKind.costume),
), ),
]; ];
} }
@@ -163,7 +165,10 @@ class RecommendSectionsView extends StatelessWidget {
subtitle: playlist.localizedDescription( subtitle: playlist.localizedDescription(
Localizations.localeOf(context).languageCode, Localizations.localeOf(context).languageCode,
), ),
dramas: playlist.dramas.take(_sectionPreviewCount).toList(), dramas: visibleDramasForLocale(
playlist.dramas,
Localizations.localeOf(context).languageCode,
).take(_sectionPreviewCount).toList(),
onMore: () => onOpenPlaylist?.call(playlist), onMore: () => onOpenPlaylist?.call(playlist),
), ),
); );
@@ -320,9 +325,15 @@ List<Drama> recommendSectionDramas({
int _newestFirst(Drama leftDrama, Drama rightDrama) { int _newestFirst(Drama leftDrama, Drama rightDrama) {
final left = publishedAtForDrama(leftDrama); final left = publishedAtForDrama(leftDrama);
final right = publishedAtForDrama(rightDrama); final right = publishedAtForDrama(rightDrama);
if (left == null && right == null) return rightDrama.id.compareTo(leftDrama.id); if (left == null && right == null) {
if (left == null) return 1; return rightDrama.id.compareTo(leftDrama.id);
if (right == null) return -1; }
if (left == null) {
return 1;
}
if (right == null) {
return -1;
}
return right.compareTo(left); return right.compareTo(left);
} }
+3 -3
View File
@@ -29,9 +29,9 @@ void main() {
for (final locale in const [Locale('en'), Locale('zh'), Locale('id')]) { for (final locale in const [Locale('en'), Locale('zh'), Locale('id')]) {
final l10n = await AppLocalizations.delegate.load(locale); final l10n = await AppLocalizations.delegate.load(locale);
expect(l10n.appName, 'FruitShorts', reason: '$locale appName'); expect(l10n.appName, 'ymb', reason: '$locale appName');
expect(l10n.brandMark, 'FRUITSHORTS', reason: '$locale brandMark'); expect(l10n.brandMark, 'YMB', reason: '$locale brandMark');
expect(l10n.shareFooter, 'FruitShorts', reason: '$locale shareFooter'); expect(l10n.shareFooter, 'ymb', reason: '$locale shareFooter');
expect(l10n.brandMark.toLowerCase(), isNot(contains('demo'))); expect(l10n.brandMark.toLowerCase(), isNot(contains('demo')));
expect(l10n.shareFooter.toLowerCase(), isNot(contains('demo'))); expect(l10n.shareFooter.toLowerCase(), isNot(contains('demo')));
} }
+68 -20
View File
@@ -1,37 +1,44 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/drama.dart'; import 'package:fruitshorts/core/data/models/drama.dart';
import 'package:fruitshorts/core/formatting/localized_content.dart'; import 'package:fruitshorts/core/formatting/localized_content.dart';
import 'package:fruitshorts/core/formatting/content_locale_guard.dart';
import 'package:fruitshorts/l10n/app_localizations_en.dart'; import 'package:fruitshorts/l10n/app_localizations_en.dart';
import 'package:fruitshorts/l10n/app_localizations_id.dart'; import 'package:fruitshorts/l10n/app_localizations_id.dart';
import 'package:fruitshorts/l10n/app_localizations_zh.dart'; import 'package:fruitshorts/l10n/app_localizations_zh.dart';
Drama _drama({required String id, String title = 'raw', String synopsis = 'raw'}) => Drama _drama({
Drama( required String id,
id: id, String title = 'raw',
title: title, String synopsis = 'raw',
synopsis: synopsis, }) => Drama(
posterUrl: '/files/poster.png', id: id,
episodes: const [], title: title,
); synopsis: synopsis,
posterUrl: '/files/poster.png',
episodes: const [],
);
void main() { void main() {
final en = AppLocalizationsEn(); final en = AppLocalizationsEn();
final zh = AppLocalizationsZh(); final zh = AppLocalizationsZh();
final id = AppLocalizationsId(); final id = AppLocalizationsId();
test('should_map_each_seeded_drama_to_its_own_localized_copy_when_looked_up', () { test(
final signal = _drama(id: 'the-signal-demo'); 'should_map_each_seeded_drama_to_its_own_localized_copy_when_looked_up',
final city = _drama(id: 'city-lights-demo'); () {
final vengeance = _drama(id: 'vengeance-demo'); final signal = _drama(id: 'the-signal-demo');
final city = _drama(id: 'city-lights-demo');
final vengeance = _drama(id: 'vengeance-demo');
expect(localizedDramaTitle(en, signal), en.dramaSignalTitle); expect(localizedDramaTitle(en, signal), en.dramaSignalTitle);
expect(localizedDramaTitle(en, city), en.dramaCityTitle); expect(localizedDramaTitle(en, city), en.dramaCityTitle);
expect(localizedDramaTitle(en, vengeance), en.dramaVengeanceTitle); expect(localizedDramaTitle(en, vengeance), en.dramaVengeanceTitle);
expect(localizedDramaSynopsis(en, signal), en.dramaSignalSynopsis); expect(localizedDramaSynopsis(en, signal), en.dramaSignalSynopsis);
expect(localizedDramaSynopsis(en, city), en.dramaCitySynopsis); expect(localizedDramaSynopsis(en, city), en.dramaCitySynopsis);
expect(localizedDramaSynopsis(en, vengeance), en.dramaVengeanceSynopsis); expect(localizedDramaSynopsis(en, vengeance), en.dramaVengeanceSynopsis);
}); },
);
test('should_keep_two_dramas_apart_when_localized_copy_is_mapped', () { test('should_keep_two_dramas_apart_when_localized_copy_is_mapped', () {
final titles = { final titles = {
@@ -70,6 +77,47 @@ void main() {
expect(localizedDramaSynopsis(zh, custom), 'Custom synopsis'); expect(localizedDramaSynopsis(zh, custom), 'Custom synopsis');
}); });
test('should_not_fall_back_to_chinese_when_indonesian_copy_is_missing', () {
const drama = Drama(
id: 'missing-id-copy',
title: '古城',
synopsis: '中文简介',
titleLocales: {'zh': '古城'},
synopsisLocales: {'zh': '中文简介'},
posterUrl: '/files/poster.png',
);
expect(drama.localizedTitle('id'), isEmpty);
expect(drama.localizedSynopsis('id'), isEmpty);
});
test('should_filter_incomplete_indonesian_content_from_visible_list', () {
const complete = Drama(
id: 'complete-id-copy',
title: 'Kota Lama',
synopsis: 'Kisah dari kota lama.',
titleLocales: {'id': 'Kota Lama'},
synopsisLocales: {'id': 'Kisah dari kota lama.'},
posterUrl: '/files/complete.png',
);
const incomplete = Drama(
id: 'incomplete-id-copy',
title: '古城',
synopsis: '中文简介',
titleLocales: {'zh': '古城'},
synopsisLocales: {'zh': '中文简介'},
posterUrl: '/files/incomplete.png',
);
expect(isDramaCompleteForLocale(complete, 'id'), isTrue);
expect(isDramaCompleteForLocale(incomplete, 'id'), isFalse);
expect(visibleDramasForLocale([complete, incomplete], 'id'), [complete]);
expect(visibleDramasForLocale([complete, incomplete], 'en'), [
complete,
incomplete,
]);
});
test('should_ignore_raw_catalog_text_when_drama_is_seeded', () { test('should_ignore_raw_catalog_text_when_drama_is_seeded', () {
final signal = _drama( final signal = _drama(
id: 'the-signal-demo', id: 'the-signal-demo',
@@ -107,7 +155,7 @@ void main() {
expect(copies, hasLength(3)); expect(copies, hasLength(3));
for (final copy in copies) { for (final copy in copies) {
expect(copy, contains('FruitShorts')); expect(copy, contains('ymb'));
expect(copy.split('\n'), hasLength(3)); expect(copy.split('\n'), hasLength(3));
expect(copy.split('\n').every((line) => line.isNotEmpty), isTrue); expect(copy.split('\n').every((line) => line.isNotEmpty), isTrue);
} }