pic_translator 1.0.1+2
pic_translator: ^1.0.1+2 copied to clipboard
A Flutter plugin for translating text in images from one language to another. It extracts text from images using ML Kit and overlays the translated text on the original image
example/lib/main.dart
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:image_picker/image_picker.dart';
import 'package:pic_translator/pic_translator.dart';
import 'package:translator/translator.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Text Translator',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const TextTranslatorPage(),
);
}
}
class TextTranslatorPage extends StatefulWidget {
const TextTranslatorPage({super.key});
@override
State<TextTranslatorPage> createState() => _TextTranslatorPageState();
}
class _TextTranslatorPageState extends State<TextTranslatorPage>
with SingleTickerProviderStateMixin {
final PicTranslator _translator = PicTranslator();
final ImagePicker _imagePicker = ImagePicker();
// Text controller for manual text input
final TextEditingController _textInputController = TextEditingController();
// Tab controller
late TabController _tabController;
File? _selectedImage;
String _extractedText = '';
String _translatedText = '';
String _manualInputText = '';
String _manualTranslatedText = '';
bool _isProcessing = false;
String _targetLanguage = 'en'; // Default to English
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_translator.dispose();
_textInputController.dispose();
_tabController.dispose();
super.dispose();
}
Future<void> _pickImageFromGallery() async {
try {
final XFile? pickedFile =
await _imagePicker.pickImage(source: ImageSource.gallery);
if (pickedFile == null) return;
setState(() {
_isProcessing = true;
_selectedImage = File(pickedFile.path);
_extractedText = '';
_translatedText = '';
});
await _processImage();
} catch (e) {
_showSnackBar('Error picking image: ${e.toString()}');
setState(() {
_isProcessing = false;
});
}
}
Future<void> _captureImageFromCamera() async {
try {
final XFile? pickedFile =
await _imagePicker.pickImage(source: ImageSource.camera);
if (pickedFile == null) return;
setState(() {
_isProcessing = true;
_selectedImage = File(pickedFile.path);
_extractedText = '';
_translatedText = '';
});
await _processImage();
} catch (e) {
_showSnackBar('Error capturing image: ${e.toString()}');
setState(() {
_isProcessing = false;
});
}
}
Future<void> _processImage() async {
try {
if (_selectedImage == null) {
setState(() {
_isProcessing = false;
});
return;
}
// Extract text from image
final recognizedText =
await _translator.extractTextFromImage(_selectedImage!);
if (recognizedText.text.isEmpty) {
setState(() {
_extractedText = 'No text detected in the image';
_isProcessing = false;
});
return;
}
setState(() {
_extractedText = recognizedText.text;
});
// Translate the extracted text
final translatedText =
await _translator.translateText(recognizedText, _targetLanguage);
setState(() {
_translatedText = translatedText;
_isProcessing = false;
});
} catch (e) {
_showSnackBar('Error processing image: ${e.toString()}');
setState(() {
_isProcessing = false;
});
}
}
Future<void> _translateManualText() async {
final textToTranslate = _textInputController.text.trim();
if (textToTranslate.isEmpty) {
_showSnackBar('Please enter text to translate');
return;
}
setState(() {
_isProcessing = true;
_manualInputText = textToTranslate;
_manualTranslatedText = '';
});
try {
// Use the GoogleTranslator directly for text-only translation
final translator = GoogleTranslator();
final translation = await translator.translate(
textToTranslate,
to: _targetLanguage,
);
setState(() {
_manualTranslatedText = translation.text;
_isProcessing = false;
});
} catch (e) {
_showSnackBar('Error translating text: ${e.toString()}');
setState(() {
_isProcessing = false;
});
}
}
void _showSnackBar(String message) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Text Translator'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(icon: Icon(Icons.image), text: 'Image Text'),
Tab(icon: Icon(Icons.text_fields), text: 'Direct Text'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
// Image Text Tab
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Language selector
_buildLanguageSelector(),
const SizedBox(height: 20),
// Image preview
Container(
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: _selectedImage != null
? Image.file(_selectedImage!, fit: BoxFit.contain)
: const Center(child: Text('No image selected')),
),
const SizedBox(height: 20),
// Buttons row
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
onPressed:
_isProcessing ? null : _captureImageFromCamera,
icon: const Icon(Icons.camera_alt),
label: const Text('Camera'),
),
ElevatedButton.icon(
onPressed: _isProcessing ? null : _pickImageFromGallery,
icon: const Icon(Icons.photo_library),
label: const Text('Gallery'),
),
],
),
const SizedBox(height: 20),
// Loading indicator
if (_isProcessing)
const Center(
child: Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text('Processing...'),
],
),
),
const SizedBox(height: 20),
// Text results
if (_extractedText.isNotEmpty)
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Extracted Text:',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Text(_extractedText),
),
],
),
),
),
const SizedBox(height: 12),
if (_translatedText.isNotEmpty)
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Translated Text:',
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(_translatedText),
),
],
),
),
),
],
),
),
),
// Direct Text Input Tab
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Language selector
_buildLanguageSelector(),
const SizedBox(height: 20),
// Text input
TextField(
controller: _textInputController,
decoration: InputDecoration(
labelText: 'Enter text to translate',
hintText: 'Type or paste text here',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_textInputController.clear();
},
),
),
maxLines: 5,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _translateManualText(),
),
const SizedBox(height: 20),
// Translate button
ElevatedButton.icon(
onPressed: _isProcessing ? null : _translateManualText,
icon: const Icon(Icons.translate),
label: const Text('Translate'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
const SizedBox(height: 20),
// Loading indicator
if (_isProcessing)
const Center(
child: Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text('Translating...'),
],
),
),
const SizedBox(height: 20),
// Translated result
if (_manualTranslatedText.isNotEmpty)
Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Translated Text:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16),
),
IconButton(
icon: const Icon(Icons.copy),
tooltip: 'Copy to clipboard',
onPressed: () {
// Copy translated text to clipboard
Clipboard.setData(ClipboardData(
text: _manualTranslatedText));
_showSnackBar(
'Translated text copied to clipboard');
},
),
],
),
const SizedBox(height: 8),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Text(_manualTranslatedText),
),
],
),
),
),
],
),
),
),
],
),
);
}
Widget _buildLanguageSelector() {
return Row(
children: [
const Text('Target Language: ',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
Expanded(
child: DropdownButton<String>(
isExpanded: true,
value: _targetLanguage,
onChanged: (value) {
if (value != null) {
setState(() {
_targetLanguage = value;
// Retranslate based on current tab
if (_tabController.index == 0) {
// Image tab
if (_extractedText.isNotEmpty &&
_extractedText != 'No text detected in the image') {
_isProcessing = true;
_processImage();
}
} else {
// Text tab
if (_textInputController.text.isNotEmpty) {
_translateManualText();
}
}
});
}
},
items: PicTranslator.availableLanguages.entries.map((entry) {
return DropdownMenuItem<String>(
value: entry.key,
child: Text(entry.value),
);
}).toList(),
),
),
],
);
}
}