Created
October 22, 2024 10:02
-
-
Save tarek360/8f9d90e7b22496e1f30f173906720647 to your computer and use it in GitHub Desktop.
arb_manager is a desktop application that allows users to view their ARB files in a table view and highlights missing values.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
import 'dart:io'; | |
import 'package:file_picker/file_picker.dart'; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/services.dart'; | |
const defaultLocale = 'en'; | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
theme: ThemeData( | |
brightness: Brightness.dark, | |
), | |
home: const ComparisonPage(), | |
); | |
} | |
} | |
class ComparisonPage extends StatefulWidget { | |
const ComparisonPage({super.key}); | |
@override | |
State<ComparisonPage> createState() => _ComparisonPageState(); | |
} | |
class LocalData { | |
final String locale; | |
double progress = 0; | |
int count = 0; | |
final Map<String, dynamic> content; | |
LocalData(this.locale, this.content); | |
} | |
const _cellWidth = 300.0; | |
const _keyCellWidth = 400.0; | |
class _ComparisonPageState extends State<ComparisonPage> { | |
List<LocalData> translations = []; | |
final ScrollController _scrollController = ScrollController(); | |
final ScrollController _sourceScrollController = ScrollController(); | |
String? _selectedDirectory; | |
@override | |
void initState() { | |
super.initState(); | |
init(); | |
_sourceScrollController.addListener(() { | |
_scrollController.jumpTo(_sourceScrollController.position.pixels); | |
}); | |
} | |
Future<void> init() async { | |
final selectedDirectory = _selectedDirectory; | |
if (selectedDirectory == null) { | |
final result = await FilePicker.platform.getDirectoryPath(); | |
if (result != null) { | |
_selectedDirectory = result; | |
loadTranslations(Directory(result)); | |
} | |
} else { | |
loadTranslations(Directory(selectedDirectory)); | |
} | |
} | |
Future<void> loadTranslations(Directory directory) async { | |
await Future.delayed(const Duration(milliseconds: 100)); | |
if (await directory.exists()) { | |
List<LocalData> locals = directory.listSync().where((element) => element.path.endsWith('.arb')).map((e) { | |
final file = File(e.path); | |
final content = json.decode(file.readAsStringSync()) as Map<String, dynamic>; | |
final locale = content['@@locale']; | |
content.removeWhere((key, value) => key.startsWith('@')); | |
return LocalData(locale, content); | |
}).toList(); | |
final defaultLocalKeysCount = locals.firstWhere((element) => element.locale == defaultLocale).content.length; | |
final defaultLocalData = locals.firstWhere((element) => element.locale == defaultLocale); | |
translations = locals; | |
for (final data in locals) { | |
data.count = data.content.length; | |
data.progress = data.content.length / defaultLocalKeysCount; | |
} | |
locals.remove(defaultLocalData); | |
locals.sort((a, b) => a.progress.compareTo(b.progress)); | |
locals.insert(0, defaultLocalData); | |
setState(() {}); // Refresh UI after loading translations | |
} | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
toolbarHeight: 80, | |
actions: [ | |
IconButton( | |
icon: const Icon(Icons.refresh), | |
onPressed: () { | |
translations.clear(); | |
setState(() {}); | |
init(); | |
}, | |
), | |
], | |
bottom: PreferredSize( | |
preferredSize: const Size.fromHeight(64.0), | |
child: Align(alignment: Alignment.centerLeft, child: _buildTableHeaderRowWidget()), | |
), | |
), | |
body: translations.isEmpty | |
? const Center(child: CircularProgressIndicator()) | |
: SingleChildScrollView( | |
child: SingleChildScrollView( | |
controller: _sourceScrollController, | |
scrollDirection: Axis.horizontal, | |
child: Table( | |
columnWidths: <int, TableColumnWidth>{ | |
0: const FixedColumnWidth(_keyCellWidth), | |
for (var i = 1; i < translations.length + 1; i++) i: const FixedColumnWidth(_cellWidth), | |
}, | |
border: TableBorder.all(width: 1, color: Colors.grey.shade700), | |
defaultVerticalAlignment: TableCellVerticalAlignment.middle, | |
children: _buildRows(), | |
), | |
), | |
), | |
); | |
} | |
Widget _buildTableHeaderRowWidget() { | |
return Padding( | |
padding: const EdgeInsets.only(bottom: 8.0), | |
child: SingleChildScrollView( | |
controller: _scrollController, | |
physics: const NeverScrollableScrollPhysics(), | |
scrollDirection: Axis.horizontal, | |
child: Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
const SizedBox(width: _keyCellWidth), | |
for (var translation in translations) | |
SizedBox( | |
width: _cellWidth, | |
child: Padding( | |
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), | |
child: Container( | |
decoration: BoxDecoration( | |
borderRadius: BorderRadius.circular(16), | |
border: Border.all(color: Colors.grey, width: 1), | |
), | |
child: ListTile( | |
contentPadding: const EdgeInsets.only(left: 16), | |
title: Text( | |
translation.locale, | |
style: const TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold), | |
), | |
subtitle: Row( | |
children: [ | |
Text( | |
'${(translation.progress * 100).toStringAsFixed(0)}%', | |
style: TextStyle( | |
color: translation.progress == 1 ? Colors.green : Colors.red, | |
fontWeight: FontWeight.bold, | |
fontSize: 16.0, | |
), | |
), | |
const SizedBox(width: 8), | |
Text( | |
'(${translation.count})', | |
style: const TextStyle( | |
fontWeight: FontWeight.bold, | |
fontSize: 16.0, | |
), | |
), | |
], | |
), | |
trailing: translation.locale == defaultLocale | |
? null | |
: IconButton( | |
icon: Icon( | |
Icons.close, | |
color: Colors.grey.shade600, | |
), | |
onPressed: () { | |
translations.remove(translation); | |
setState(() {}); | |
}, | |
), | |
), | |
), | |
), | |
), | |
], | |
), | |
), | |
); | |
} | |
List<TableRow> _buildRows() { | |
List<TableRow> rows = []; | |
final baseColor = Theme.of(context).scaffoldBackgroundColor; | |
final defaultKeys = translations.firstWhere((element) => element.locale == defaultLocale).content.keys; | |
for (int i = 0; i < defaultKeys.length; i++) { | |
final key = defaultKeys.elementAt(i); | |
final color = i % 2 == 0 ? baseColor : Colors.blueGrey.shade900; | |
final translated = | |
translations.map((e) => e.content[key] != null ? 1 : 0).reduce((value, element) => value + element); | |
final rowProgress = translated / translations.length; | |
List<Widget> cells = [ | |
GestureDetector( | |
onTap: () { | |
Clipboard.setData(ClipboardData(text: key)); | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar(content: Text('COPIED!')), | |
); | |
}, | |
child: Container( | |
padding: const EdgeInsets.all(8.0), | |
width: _cellWidth, | |
height: 56, | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: [ | |
Expanded(child: Text(key)), | |
const SizedBox(width: 16), | |
Text('$translated/${translations.length}'), | |
const SizedBox(width: 8), | |
rowProgress == 1 | |
? const Icon(Icons.check, color: Colors.green) | |
: const Icon(Icons.error, color: Colors.orange), | |
], | |
), | |
), | |
), | |
]; | |
for (final translation in translations) { | |
final value = translation.content[key] ?? '-------'; // Placeholder for missing translations | |
cells.add( | |
GestureDetector( | |
behavior: HitTestBehavior.translucent, | |
onTap: () { | |
Clipboard.setData(ClipboardData(text: value.toString())); | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar(content: Text('COPIED!')), | |
); | |
}, | |
child: Container( | |
padding: const EdgeInsets.all(8.0), | |
width: _cellWidth, | |
height: 56, | |
child: Text( | |
value, | |
overflow: TextOverflow.ellipsis, | |
style: TextStyle( | |
fontWeight: FontWeight.bold, | |
color: translation.content[key] != null ? Colors.grey.shade300 : Colors.orange, | |
), | |
), | |
), | |
), | |
); | |
} | |
rows.add( | |
TableRow( | |
decoration: BoxDecoration(color: color), | |
children: cells, | |
), | |
); | |
} | |
return rows; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment