feat: add support for 13 languages with improved language selector

- Rename Crowdin ARB files from locale-REGION to locale format
- Fix @@locale values to match filenames
- Update language selector to bottom sheet picker (supports 13 languages)
- Supported: English, Indonesian, German, Spanish, French, Hindi,
  Japanese, Korean, Dutch, Portuguese, Russian, Chinese (Simplified/Traditional)
- Remove duplicate app_id-ID.arb (keep app_id.arb)
This commit is contained in:
zarzet
2026-01-16 06:38:52 +07:00
parent f128d0caf0
commit d8f73dfa56
24 changed files with 21914 additions and 2595 deletions
+67 -2
View File
@@ -5,8 +5,18 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_hi.dart';
import 'app_localizations_id.dart';
import 'app_localizations_ja.dart';
import 'app_localizations_ko.dart';
import 'app_localizations_nl.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
@@ -94,8 +104,19 @@ abstract class AppLocalizations {
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
Locale('de'),
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('hi'),
Locale('id'),
Locale('ja'),
Locale('ko'),
Locale('nl'),
Locale('pt'),
Locale('ru'),
Locale('zh'),
Locale('zh', 'TW'),
];
/// App name - DO NOT TRANSLATE
@@ -3589,20 +3610,64 @@ class _AppLocalizationsDelegate
}
@override
bool isSupported(Locale locale) =>
<String>['en', 'id'].contains(locale.languageCode);
bool isSupported(Locale locale) => <String>[
'de',
'en',
'es',
'fr',
'hi',
'id',
'ja',
'ko',
'nl',
'pt',
'ru',
'zh',
].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when language+country codes are specified.
switch (locale.languageCode) {
case 'zh':
{
switch (locale.countryCode) {
case 'TW':
return AppLocalizationsZhTw();
}
break;
}
}
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'es':
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'hi':
return AppLocalizationsHi();
case 'id':
return AppLocalizationsId();
case 'ja':
return AppLocalizationsJa();
case 'ko':
return AppLocalizationsKo();
case 'nl':
return AppLocalizationsNl();
case 'pt':
return AppLocalizationsPt();
case 'ru':
return AppLocalizationsRu();
case 'zh':
return AppLocalizationsZh();
}
throw FlutterError(
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
{
"@@locale": "id",
"@@locale": "es",
"@@last_modified": "2026-01-16",
"appName": "SpotiFLAC",
"@appName": {
@@ -1,5 +1,5 @@
{
"@@locale": "es-ES",
"@@locale": "pt",
"@@last_modified": "2026-01-16",
"appName": "SpotiFLAC",
"@appName": {
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
{
"@@locale": "pt-PT",
"@@locale": "zh",
"@@last_modified": "2026-01-16",
"appName": "SpotiFLAC",
"@appName": {
@@ -1,5 +1,5 @@
{
"@@locale": "zh-CN",
"@@locale": "zh_TW",
"@@last_modified": "2026-01-16",
"appName": "SpotiFLAC",
"@appName": {
@@ -709,48 +709,109 @@ class _LanguageSelector extends StatelessWidget {
required this.onChanged,
});
static const _languages = [
('system', 'System Default', Icons.phone_android),
('en', 'English', Icons.language),
('id', 'Bahasa Indonesia', Icons.language),
('de', 'Deutsch', Icons.language),
('es', 'Español', Icons.language),
('fr', 'Français', Icons.language),
('hi', 'हिन्दी', Icons.language),
('ja', '日本語', Icons.language),
('ko', '한국어', Icons.language),
('nl', 'Nederlands', Icons.language),
('pt', 'Português', Icons.language),
('ru', 'Русский', Icons.language),
('zh', '简体中文', Icons.language),
('zh_TW', '繁體中文', Icons.language),
];
String _getLanguageName(String code) {
for (final lang in _languages) {
if (lang.$1 == code) return lang.$2;
}
return code;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, bottom: 8),
child: Text(
context.l10n.appearanceLanguage,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
return ListTile(
leading: Icon(
Icons.language,
color: colorScheme.onSurfaceVariant,
),
title: Text(context.l10n.appearanceLanguage),
subtitle: Text(_getLanguageName(currentLocale)),
trailing: Icon(
Icons.chevron_right,
color: colorScheme.onSurfaceVariant,
),
onTap: () => _showLanguagePicker(context),
);
}
void _showLanguagePicker(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
showModalBottomSheet(
context: context,
backgroundColor: colorScheme.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
context.l10n.appearanceLanguage,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
),
Row(
children: [
_ViewModeChip(
icon: Icons.phone_android,
label: context.l10n.languageSystem,
isSelected: currentLocale == 'system',
onTap: () => onChanged('system'),
const Divider(height: 1),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: _languages.length,
itemBuilder: (context, index) {
final lang = _languages[index];
final isSelected = currentLocale == lang.$1;
return ListTile(
leading: Icon(
lang.$3,
color: isSelected
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
title: Text(
lang.$2,
style: TextStyle(
color: isSelected
? colorScheme.primary
: colorScheme.onSurface,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.normal,
),
),
trailing: isSelected
? Icon(Icons.check, color: colorScheme.primary)
: null,
onTap: () {
onChanged(lang.$1);
Navigator.pop(context);
},
);
},
),
const SizedBox(width: 8),
_ViewModeChip(
icon: Icons.language,
label: context.l10n.languageEnglish,
isSelected: currentLocale == 'en',
onTap: () => onChanged('en'),
),
const SizedBox(width: 8),
_ViewModeChip(
icon: Icons.language,
label: context.l10n.languageIndonesian,
isSelected: currentLocale == 'id',
onTap: () => onChanged('id'),
),
],
),
],
),
const SizedBox(height: 8),
],
),
),
);
}