feat: add multilanguage support (i18n) for English and Indonesian

- Add flutter_localizations and intl dependencies
- Create l10n.yaml configuration and ARB files (app_en.arb, app_id.arb)
- Add L10n extension for easy context.l10n access
- Localize all active screens:
  - setup_screen, track_metadata_screen, log_screen
  - download_settings_page, options_settings_page, appearance_settings_page
  - extensions_page, extension_detail_page, extension_details_screen
  - about_page, provider_priority_page, metadata_provider_priority_page
  - home_tab, queue_tab, store_tab, main_shell
  - album_screen, artist_screen, playlist_screen
  - downloaded_album_screen, queue_screen
- Localize widgets: update_dialog, download_service_picker
- Technical terms (FLAC, API, Spotify, Tidal, Qobuz, etc.) are NOT translated
- ~900+ localized strings in English, ~660+ in Indonesian
This commit is contained in:
zarzet
2026-01-16 05:50:11 +07:00
parent 7c6705c75c
commit f26af38c1e
34 changed files with 9758 additions and 601 deletions
+4 -3
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/providers/extension_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
/// Built-in service info with quality options
class BuiltInService {
@@ -167,7 +168,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
'Download From',
context.l10n.downloadFrom,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
@@ -202,7 +203,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 8),
child: Text(
'Select Quality',
context.l10n.downloadSelectQuality,
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
),
),
@@ -212,7 +213,7 @@ class _DownloadServicePickerState extends ConsumerState<DownloadServicePicker> {
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 12),
child: Text(
'Actual quality depends on track availability. Hi-Res may not be available for all tracks.',
context.l10n.qualityNote,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
+14 -13
View File
@@ -4,6 +4,7 @@ import 'package:spotiflac_android/constants/app_info.dart';
import 'package:spotiflac_android/services/update_checker.dart';
import 'package:spotiflac_android/services/apk_downloader.dart';
import 'package:spotiflac_android/services/notification_service.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
class UpdateDialog extends StatefulWidget {
final UpdateInfo updateInfo;
@@ -42,7 +43,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
setState(() {
_isDownloading = true;
_progress = 0;
_statusText = 'Starting download...';
_statusText = context.l10n.updateStartingDownload;
});
final notificationService = NotificationService();
@@ -91,11 +92,11 @@ class _UpdateDialogState extends State<UpdateDialog> {
if (mounted) {
setState(() {
_isDownloading = false;
_statusText = 'Download failed';
_statusText = context.l10n.updateDownloadFailed;
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to download update')),
SnackBar(content: Text(context.l10n.updateFailedMessage)),
);
}
}
@@ -131,9 +132,9 @@ class _UpdateDialogState extends State<UpdateDialog> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Update Available', style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
Text(context.l10n.updateAvailable, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 2),
Text('A new version is ready', style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
Text(context.l10n.updateNewVersionReady, style: Theme.of(context).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant)),
],
),
),
@@ -154,11 +155,11 @@ class _UpdateDialogState extends State<UpdateDialog> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_VersionChip(version: AppInfo.version, label: 'Current', colorScheme: colorScheme),
_VersionChip(version: AppInfo.version, label: context.l10n.updateCurrent, colorScheme: colorScheme),
const SizedBox(width: 12),
Icon(Icons.arrow_forward_rounded, size: 20, color: colorScheme.primary),
const SizedBox(width: 12),
_VersionChip(version: widget.updateInfo.version, label: 'New', colorScheme: colorScheme, isNew: true),
_VersionChip(version: widget.updateInfo.version, label: context.l10n.updateNew, colorScheme: colorScheme, isNew: true),
],
),
),
@@ -184,7 +185,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
child: CircularProgressIndicator(strokeWidth: 2, color: colorScheme.primary),
),
const SizedBox(width: 12),
Text('Downloading...', style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
Text(context.l10n.updateDownloading, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 12),
@@ -209,7 +210,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
),
] else ...[
// Changelog section
Text("What's New", style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
Text(context.l10n.updateWhatsNew, style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(maxHeight: 180),
@@ -240,7 +241,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Cancel'),
child: Text(context.l10n.dialogCancel),
),
)
else
@@ -251,7 +252,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
child: FilledButton.icon(
onPressed: _downloadAndInstall,
icon: const Icon(Icons.download_rounded, size: 20),
label: const Text('Download & Install'),
label: Text(context.l10n.updateDownloadInstall),
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
@@ -271,7 +272,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text("Don't remind", style: TextStyle(color: colorScheme.onSurfaceVariant)),
child: Text(context.l10n.updateDontRemind, style: TextStyle(color: colorScheme.onSurfaceVariant)),
),
),
const SizedBox(width: 8),
@@ -285,7 +286,7 @@ class _UpdateDialogState extends State<UpdateDialog> {
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: const Text('Later'),
child: Text(context.l10n.updateLater),
),
),
],