Skip to content

Instantly share code, notes, and snippets.

@tarek360
Created October 22, 2024 10:02
Show Gist options
  • Save tarek360/8f9d90e7b22496e1f30f173906720647 to your computer and use it in GitHub Desktop.
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.
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