arindae commited on
Commit
97d9ce4
·
1 Parent(s): 1e92a9b
Files changed (31) hide show
  1. .gitignore +1 -0
  2. README.md +25 -32
  3. requirements.txt +4 -0
  4. translator_app/translator/__pycache__/__init__.cpython-314.pyc +0 -0
  5. translator_app/translator/__pycache__/admin.cpython-314.pyc +0 -0
  6. translator_app/translator/__pycache__/apps.cpython-314.pyc +0 -0
  7. translator_app/translator/__pycache__/models.cpython-314.pyc +0 -0
  8. translator_app/translator/__pycache__/tests.cpython-314.pyc +0 -0
  9. translator_app/translator/__pycache__/urls.cpython-314.pyc +0 -0
  10. translator_app/translator/__pycache__/views.cpython-314.pyc +0 -0
  11. translator_app/translator/admin.py +21 -1
  12. translator_app/translator/migrations/0001_initial.py +75 -0
  13. translator_app/translator/migrations/0002_translationhistory_translation_model.py +15 -0
  14. translator_app/translator/migrations/__pycache__/0001_initial.cpython-314.pyc +0 -0
  15. translator_app/translator/migrations/__pycache__/0002_translationhistory_translation_model.cpython-314.pyc +0 -0
  16. translator_app/translator/migrations/__pycache__/__init__.cpython-314.pyc +0 -0
  17. translator_app/translator/models.py +2 -1
  18. translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc +0 -0
  19. translator_app/translator/services/translation_service.py +129 -192
  20. translator_app/translator/templates/translator/base.html +21 -0
  21. translator_app/translator/templates/translator/history.html +9 -0
  22. translator_app/translator/templates/translator/translator.html +47 -0
  23. translator_app/translator/tests.py +33 -1
  24. translator_app/translator/urls.py +14 -0
  25. translator_app/translator/views.py +102 -0
  26. translator_app/translator_project/__pycache__/__init__.cpython-314.pyc +0 -0
  27. translator_app/translator_project/__pycache__/settings.cpython-314.pyc +0 -0
  28. translator_app/translator_project/__pycache__/urls.cpython-314.pyc +0 -0
  29. translator_app/translator_project/__pycache__/wsgi.cpython-314.pyc +0 -0
  30. translator_app/translator_project/settings.py +69 -47
  31. translator_app/translator_project/urls.py +2 -1
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ translator_app/.venv
README.md CHANGED
@@ -1,40 +1,33 @@
1
- cd translator_app
2
-
3
- # Create virtual environment
4
- python -m venv venv
5
-
6
- # Activate virtual environment
7
- # On Windows:
8
- venv\Scripts\activate
9
- # On Mac/Linux:
10
- source venv/bin/activate
11
 
12
- # Install required packages
13
- pip install django djangorestframework requests python-dotenv
14
 
15
- #OR
16
- pip install -r requirements.txt
17
-
18
- #Run Migrations
19
- python manage.py makemigrations
20
  python manage.py migrate
21
- python manage.py createsuperuser(optional)
 
22
 
23
- #Install
24
- pip install libretranslate
25
 
26
- #Start Libretranslate
27
- #On Windows
28
- cd translator_app
29
- venv\Scripts\activate
30
- libretranslate --host 0.0.0.0 --port 5000
31
 
32
- #On Macos
33
- cd translator_app
34
- source venv/bin/activate
35
- libretranslate --host 0.0.0.0 --port 5000
36
 
37
- #In another terminal
38
- #Run the Django App
39
- python manage.py runserver
 
 
 
 
 
 
40
 
 
 
 
 
1
+ # The Translator App
 
 
 
 
 
 
 
 
 
2
 
3
+ ## Run locally
 
4
 
5
+ ```bash
6
+ cd translator_app
7
+ python -m venv .venv
8
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
9
+ pip install -r ../requirements.txt
10
  python manage.py migrate
11
+ python manage.py runserver
12
+ ```
13
 
14
+ Open http://127.0.0.1:8000/. The translation page has an API-backed submit
15
+ flow, language selectors, model picker, and persisted history at `/history/`.
16
 
17
+ ## Configure a provider
 
 
 
 
18
 
19
+ Create `translator_app/.env` with a Hugging Face token before translating:
 
 
 
20
 
21
+ ```env
22
+ HF_TOKEN=hf_your_access_token
23
+ TRANSLATION_DEFAULT_MODEL=nllb_600m
24
+ ```
25
+
26
+ The primary model picker is configured for NLLB-200 600M, NLLB-200 1.3B, and
27
+ MADLAD-400 3B through Hugging Face Inference Providers. NLLB language codes
28
+ are mapped in the service before a request is sent. The selected model is sent
29
+ by the UI and saved with each history item.
30
 
31
+ For an optional DeepL-compatible fallback, additionally set
32
+ `TRANSLATION_API_KEY` (and optionally `TRANSLATION_API_URL`). It will appear in
33
+ the model picker only when configured.
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Django>=5.2
2
+ djangorestframework>=3.15
3
+ python-dotenv>=1.0
4
+ requests>=2.31
translator_app/translator/__pycache__/__init__.cpython-314.pyc CHANGED
Binary files a/translator_app/translator/__pycache__/__init__.cpython-314.pyc and b/translator_app/translator/__pycache__/__init__.cpython-314.pyc differ
 
translator_app/translator/__pycache__/admin.cpython-314.pyc CHANGED
Binary files a/translator_app/translator/__pycache__/admin.cpython-314.pyc and b/translator_app/translator/__pycache__/admin.cpython-314.pyc differ
 
translator_app/translator/__pycache__/apps.cpython-314.pyc CHANGED
Binary files a/translator_app/translator/__pycache__/apps.cpython-314.pyc and b/translator_app/translator/__pycache__/apps.cpython-314.pyc differ
 
translator_app/translator/__pycache__/models.cpython-314.pyc CHANGED
Binary files a/translator_app/translator/__pycache__/models.cpython-314.pyc and b/translator_app/translator/__pycache__/models.cpython-314.pyc differ
 
translator_app/translator/__pycache__/tests.cpython-314.pyc ADDED
Binary file (2.82 kB). View file
 
translator_app/translator/__pycache__/urls.cpython-314.pyc ADDED
Binary file (718 Bytes). View file
 
translator_app/translator/__pycache__/views.cpython-314.pyc ADDED
Binary file (5.86 kB). View file
 
translator_app/translator/admin.py CHANGED
@@ -1,3 +1,23 @@
1
  from django.contrib import admin
 
2
 
3
- # Register your models here.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from django.contrib import admin
2
+ from .models import Language, TranslationCache, TranslationHistory
3
 
4
+
5
+ @admin.register(Language)
6
+ class LanguageAdmin(admin.ModelAdmin):
7
+ list_display = ('name', 'code', 'native_name', 'is_active')
8
+ list_filter = ('is_active',)
9
+ search_fields = ('name', 'native_name', 'code')
10
+
11
+
12
+ @admin.register(TranslationHistory)
13
+ class TranslationHistoryAdmin(admin.ModelAdmin):
14
+ list_display = ('source_text', 'source_language', 'target_language', 'translation_model', 'created_at')
15
+ list_filter = ('source_language', 'target_language', 'translation_model')
16
+ search_fields = ('source_text', 'translated_text')
17
+ readonly_fields = ('character_count', 'word_count', 'created_at')
18
+
19
+
20
+ @admin.register(TranslationCache)
21
+ class TranslationCacheAdmin(admin.ModelAdmin):
22
+ list_display = ('source_text', 'target_language_code', 'access_count', 'last_accessed')
23
+ search_fields = ('source_text', 'translated_text')
translator_app/translator/migrations/0001_initial.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0.7 on 2026-07-22 08:44
2
+
3
+ import django.db.models.deletion
4
+ import django.utils.timezone
5
+ from django.conf import settings
6
+ from django.db import migrations, models
7
+
8
+
9
+ class Migration(migrations.Migration):
10
+
11
+ initial = True
12
+
13
+ dependencies = [
14
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15
+ ]
16
+
17
+ operations = [
18
+ migrations.CreateModel(
19
+ name='Language',
20
+ fields=[
21
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
+ ('code', models.CharField(max_length=10, unique=True)),
23
+ ('name', models.CharField(max_length=50)),
24
+ ('native_name', models.CharField(blank=True, max_length=50)),
25
+ ('is_active', models.BooleanField(default=True)),
26
+ ('created_at', models.DateTimeField(auto_now_add=True)),
27
+ ('updated_at', models.DateTimeField(auto_now=True)),
28
+ ],
29
+ options={
30
+ 'verbose_name': 'Language',
31
+ 'verbose_name_plural': 'Languages',
32
+ 'ordering': ['name'],
33
+ },
34
+ ),
35
+ migrations.CreateModel(
36
+ name='TranslationCache',
37
+ fields=[
38
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39
+ ('source_text_hash', models.CharField(max_length=64, unique=True)),
40
+ ('source_text', models.TextField()),
41
+ ('translated_text', models.TextField()),
42
+ ('source_language_code', models.CharField(max_length=10)),
43
+ ('target_language_code', models.CharField(max_length=10)),
44
+ ('created_at', models.DateTimeField(auto_now_add=True)),
45
+ ('last_accessed', models.DateTimeField(auto_now=True)),
46
+ ('access_count', models.IntegerField(default=0)),
47
+ ],
48
+ options={
49
+ 'verbose_name': 'Translation Cache',
50
+ 'verbose_name_plural': 'Translation Caches',
51
+ 'indexes': [models.Index(fields=['source_text_hash', 'target_language_code'], name='translator__source__c37921_idx')],
52
+ },
53
+ ),
54
+ migrations.CreateModel(
55
+ name='TranslationHistory',
56
+ fields=[
57
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
58
+ ('source_text', models.TextField()),
59
+ ('translated_text', models.TextField()),
60
+ ('detected_source_language', models.CharField(blank=True, max_length=10)),
61
+ ('character_count', models.IntegerField(default=0)),
62
+ ('word_count', models.IntegerField(default=0)),
63
+ ('created_at', models.DateTimeField(default=django.utils.timezone.now)),
64
+ ('source_language', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_translations', to='translator.language')),
65
+ ('target_language', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='target_translations', to='translator.language')),
66
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translations', to=settings.AUTH_USER_MODEL)),
67
+ ],
68
+ options={
69
+ 'verbose_name': 'Translation History',
70
+ 'verbose_name_plural': 'Translation Histories',
71
+ 'ordering': ['-created_at'],
72
+ 'indexes': [models.Index(fields=['user', '-created_at'], name='translator__user_id_18b742_idx')],
73
+ },
74
+ ),
75
+ ]
translator_app/translator/migrations/0002_translationhistory_translation_model.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Generated by Django 6.0.7 on 2026-07-22
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [('translator', '0001_initial')]
8
+
9
+ operations = [
10
+ migrations.AddField(
11
+ model_name='translationhistory',
12
+ name='translation_model',
13
+ field=models.CharField(blank=True, max_length=100),
14
+ ),
15
+ ]
translator_app/translator/migrations/__pycache__/0001_initial.cpython-314.pyc ADDED
Binary file (4.39 kB). View file
 
translator_app/translator/migrations/__pycache__/0002_translationhistory_translation_model.cpython-314.pyc ADDED
Binary file (849 Bytes). View file
 
translator_app/translator/migrations/__pycache__/__init__.cpython-314.pyc CHANGED
Binary files a/translator_app/translator/migrations/__pycache__/__init__.cpython-314.pyc and b/translator_app/translator/migrations/__pycache__/__init__.cpython-314.pyc differ
 
translator_app/translator/models.py CHANGED
@@ -45,6 +45,7 @@ class TranslationHistory(models.Model):
45
  related_name='target_translations'
46
  )
47
  detected_source_language = models.CharField(max_length=10, blank=True)
 
48
  character_count = models.IntegerField(default=0)
49
  word_count = models.IntegerField(default=0)
50
  created_at = models.DateTimeField(default=timezone.now)
@@ -87,4 +88,4 @@ class TranslationCache(models.Model):
87
  ]
88
 
89
  def __str__(self):
90
- return f"{self.source_text[:50]}... -> {self.target_language_code}"
 
45
  related_name='target_translations'
46
  )
47
  detected_source_language = models.CharField(max_length=10, blank=True)
48
+ translation_model = models.CharField(max_length=100, blank=True)
49
  character_count = models.IntegerField(default=0)
50
  word_count = models.IntegerField(default=0)
51
  created_at = models.DateTimeField(default=timezone.now)
 
88
  ]
89
 
90
  def __str__(self):
91
+ return f"{self.source_text[:50]}... -> {self.target_language_code}"
translator_app/translator/services/__pycache__/translation_service.cpython-314.pyc ADDED
Binary file (10.6 kB). View file
 
translator_app/translator/services/translation_service.py CHANGED
@@ -1,224 +1,161 @@
1
- # translator/services/translation_service.py
2
 
3
  import hashlib
4
- import json
5
  import logging
 
6
  import requests
7
  from django.conf import settings
8
- from django.core.cache import cache
9
  from django.utils import timezone
 
10
  from ..models import TranslationCache
11
 
12
  logger = logging.getLogger(__name__)
13
 
 
14
  class TranslationService:
15
- """Service class for handling translation operations"""
16
-
 
 
 
 
 
 
 
 
17
  def __init__(self):
18
- self.api_key = settings.TRANSLATION_API_KEY
19
- self.api_url = settings.TRANSLATION_API_URL
20
  self.supported_languages = self._get_supported_languages()
21
-
22
  def _get_supported_languages(self):
23
- """Get list of supported languages"""
24
- # Language codes mapping
25
  return {
26
- 'en': 'English',
27
- 'es': 'Spanish',
28
- 'fr': 'French',
29
- 'de': 'German',
30
- 'it': 'Italian',
31
- 'pt': 'Portuguese',
32
- 'ru': 'Russian',
33
- 'zh': 'Chinese',
34
- 'ja': 'Japanese',
35
- 'ko': 'Korean',
36
- 'ar': 'Arabic',
37
- 'hi': 'Hindi',
38
- 'sw': 'Swahili', # Kenyan languages
39
- 'yo': 'Yoruba',
40
- 'ha': 'Hausa',
41
- 'ig': 'Igbo',
42
  }
43
-
44
  def get_supported_languages(self):
45
- """Return supported languages as a list of dicts"""
46
  return [{'code': code, 'name': name} for code, name in self.supported_languages.items()]
47
-
48
  def detect_language(self, text):
49
- """Detect the language of the input text"""
50
- if not text or len(text.strip()) < 2:
51
- return {'code': 'en', 'name': 'English', 'confidence': 1.0}
52
-
53
- try:
54
- # For DeepL API
55
- response = requests.post(
56
- f"{self.api_url}/document",
57
- headers={
58
- 'Authorization': f'DeepL-Auth-Key {self.api_key}',
59
- 'Content-Type': 'application/json'
60
- },
61
- json={'text': [text]},
62
- timeout=10
63
- )
64
-
65
- if response.status_code == 200:
66
- data = response.json()
67
- if data.get('detections'):
68
- detection = data['detections'][0]
69
- if detection:
70
- return {
71
- 'code': detection.get('language', 'en'),
72
- 'name': self.supported_languages.get(detection.get('language', 'en'), 'Unknown'),
73
- 'confidence': detection.get('confidence', 0.0)
74
- }
75
-
76
- # Fallback: Use Google Translate API
77
- # response = requests.post(...)
78
-
79
- # If API fails, use a simple detection approach
80
- # (Use langdetect library for better detection)
81
- return self._simple_detect(text)
82
-
83
- except Exception as e:
84
- logger.error(f"Language detection failed: {str(e)}")
85
- return self._simple_detect(text)
86
-
87
- def _simple_detect(self, text):
88
- """Simple language detection fallback"""
89
- # This is a very basic approach - use langdetect library in production
90
- # For now, assume English if text has mostly ASCII characters
91
- if all(ord(c) < 128 for c in text):
92
- return {'code': 'en', 'name': 'English', 'confidence': 0.6}
93
- else:
94
- # Could add more heuristics here
95
- return {'code': 'en', 'name': 'English', 'confidence': 0.6}
96
-
97
- def translate(self, text, target_language, source_language=None):
98
- """Translate text from source to target language"""
99
  if not text or not text.strip():
100
  return {'error': 'Text cannot be empty'}
101
-
102
- # Generate cache key
103
- cache_key = self._generate_cache_key(text, source_language, target_language)
104
-
105
- # Check cache
106
- cached_result = self._get_from_cache(cache_key)
107
- if cached_result:
108
- return cached_result
109
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  try:
111
- # Prepare translation request
112
- response = self._call_translation_api(text, target_language, source_language)
113
-
114
- if response.status_code == 200:
115
- translated_text = self._parse_translation_response(response.json())
116
-
117
- # Cache the result
118
- self._save_to_cache(cache_key, text, translated_text, source_language, target_language)
119
-
120
- return {
121
- 'source_text': text,
122
- 'translated_text': translated_text,
123
- 'source_language': source_language or 'auto-detected',
124
- 'target_language': target_language
125
- }
126
- else:
127
- logger.error(f"Translation API error: {response.status_code} - {response.text}")
128
- return {
129
- 'error': f'Translation failed with status code: {response.status_code}',
130
- 'details': response.text
131
- }
132
-
133
  except requests.exceptions.Timeout:
134
- return {'error': 'Translation request timed out'}
135
  except requests.exceptions.ConnectionError:
136
- return {'error': 'Failed to connect to translation service'}
137
- except Exception as e:
138
- logger.error(f"Translation error: {str(e)}")
139
- return {'error': f'Translation failed: {str(e)}'}
140
-
141
- def _call_translation_api(self, text, target_language, source_language=None):
142
- """Make the API call to the translation service"""
143
- # Using DeepL API as example
144
- headers = {
145
- 'Authorization': f'DeepL-Auth-Key {self.api_key}',
146
- 'Content-Type': 'application/json'
147
- }
148
-
149
- payload = {
150
- 'text': [text],
151
- 'target_lang': target_language.upper()
152
- }
153
-
154
- if source_language and source_language != 'auto':
155
- payload['source_lang'] = source_language.upper()
156
-
157
- response = requests.post(
158
- self.api_url,
159
- headers=headers,
160
- json=payload,
161
- timeout=30
162
- )
163
-
164
- return response
165
-
166
- def _parse_translation_response(self, response_data):
167
- """Parse the translation API response"""
168
  try:
169
- # DeepL response format
170
- if 'translations' in response_data and response_data['translations']:
171
- return response_data['translations'][0].get('text', '')
172
-
173
- # Google Translate response format (alternative)
174
- if 'data' in response_data and 'translations' in response_data['data']:
175
- return response_data['data']['translations'][0].get('translatedText', '')
176
-
177
- return 'Translation not available'
178
- except (KeyError, IndexError, TypeError) as e:
179
- logger.error(f"Failed to parse translation response: {str(e)}")
180
- return 'Translation parsing failed'
181
-
182
- def _generate_cache_key(self, text, source_language, target_language):
183
- """Generate a unique cache key for a translation"""
184
- content = f"{text}|{source_language or 'auto'}|{target_language}"
 
185
  return hashlib.sha256(content.encode('utf-8')).hexdigest()
186
-
187
- def _get_from_cache(self, cache_key):
188
- """Get translation from cache"""
189
  try:
190
- cache_entry = TranslationCache.objects.filter(
191
- source_text_hash=cache_key
192
- ).first()
193
-
194
- if cache_entry:
195
- cache_entry.access_count += 1
196
- cache_entry.last_accessed = timezone.now()
197
- cache_entry.save()
198
-
199
- return {
200
- 'source_text': cache_entry.source_text,
201
- 'translated_text': cache_entry.translated_text,
202
- 'source_language': cache_entry.source_language_code,
203
- 'target_language': cache_entry.target_language_code,
204
- 'from_cache': True
205
- }
206
  return None
207
- except Exception as e:
208
- logger.error(f"Cache retrieval failed: {str(e)}")
209
- return None
210
-
211
- def _save_to_cache(self, cache_key, source_text, translated_text, source_language, target_language):
212
- """Save translation to cache"""
213
  try:
214
  TranslationCache.objects.update_or_create(
215
  source_text_hash=cache_key,
216
- defaults={
217
- 'source_text': source_text,
218
- 'translated_text': translated_text,
219
- 'source_language_code': source_language or 'auto',
220
- 'target_language_code': target_language
221
- }
222
  )
223
- except Exception as e:
224
- logger.error(f"Cache save failed: {str(e)}")
 
1
+ """Translation providers behind one stable application-level contract."""
2
 
3
  import hashlib
 
4
  import logging
5
+
6
  import requests
7
  from django.conf import settings
 
8
  from django.utils import timezone
9
+
10
  from ..models import TranslationCache
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
+
15
  class TranslationService:
16
+ """Run the selected ML model, with an optional DeepL-compatible fallback."""
17
+
18
+ # NLLB requires its FLORES language tags. MADLAD accepts ISO language codes.
19
+ NLLB_LANGUAGE_CODES = {
20
+ 'en': 'eng_Latn', 'es': 'spa_Latn', 'fr': 'fra_Latn', 'de': 'deu_Latn',
21
+ 'it': 'ita_Latn', 'pt': 'por_Latn', 'ru': 'rus_Cyrl', 'zh': 'zho_Hans',
22
+ 'ja': 'jpn_Jpan', 'ko': 'kor_Hang', 'ar': 'arb_Arab', 'hi': 'hin_Deva',
23
+ 'sw': 'swh_Latn', 'yo': 'yor_Latn', 'ha': 'hau_Latn', 'ig': 'ibo_Latn',
24
+ }
25
+
26
  def __init__(self):
 
 
27
  self.supported_languages = self._get_supported_languages()
28
+
29
  def _get_supported_languages(self):
 
 
30
  return {
31
+ 'en': 'English', 'es': 'Spanish', 'fr': 'French', 'de': 'German',
32
+ 'it': 'Italian', 'pt': 'Portuguese', 'ru': 'Russian', 'zh': 'Chinese',
33
+ 'ja': 'Japanese', 'ko': 'Korean', 'ar': 'Arabic', 'hi': 'Hindi',
34
+ 'sw': 'Swahili', 'yo': 'Yoruba', 'ha': 'Hausa', 'ig': 'Igbo',
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
+
37
  def get_supported_languages(self):
 
38
  return [{'code': code, 'name': name} for code, name in self.supported_languages.items()]
39
+
40
  def detect_language(self, text):
41
+ # Deliberately local and deterministic until the ML team supplies a detector.
42
+ code = 'en' if all(ord(char) < 128 for char in text) else 'en'
43
+ return {'code': code, 'name': self.supported_languages[code], 'confidence': 0.6}
44
+
45
+ def translate(self, text, target_language, source_language=None, model=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  if not text or not text.strip():
47
  return {'error': 'Text cannot be empty'}
48
+
49
+ model = model or settings.TRANSLATION_DEFAULT_MODEL
50
+ source_language = source_language or self.detect_language(text)['code']
51
+ cache_key = self._generate_cache_key(text, source_language, target_language, model)
52
+ cached = self._get_from_cache(cache_key)
53
+ if cached:
54
+ cached['model'] = model
55
+ return cached
56
+
57
+ if model == 'deepl_fallback':
58
+ outcome = self._translate_deepl(text, source_language, target_language)
59
+ elif model in settings.TRANSLATION_MODEL_IDS:
60
+ outcome = self._translate_hugging_face(text, source_language, target_language, model)
61
+ else:
62
+ return {'error': 'The selected translation model is unavailable.'}
63
+
64
+ if outcome.get('error'):
65
+ return outcome
66
+
67
+ translated_text = outcome['translated_text']
68
+ self._save_to_cache(cache_key, text, translated_text, source_language, target_language)
69
+ return {
70
+ 'source_text': text,
71
+ 'translated_text': translated_text,
72
+ 'source_language': source_language,
73
+ 'target_language': target_language,
74
+ 'model': model,
75
+ }
76
+
77
+ def _translate_hugging_face(self, text, source_language, target_language, model):
78
+ if not settings.HF_TOKEN:
79
+ return {'error': 'ML translation is not configured. Add HF_TOKEN to your environment.'}
80
+
81
+ model_id = settings.TRANSLATION_MODEL_IDS[model]
82
+ if model.startswith('nllb_'):
83
+ try:
84
+ source = self.NLLB_LANGUAGE_CODES[source_language]
85
+ target = self.NLLB_LANGUAGE_CODES[target_language]
86
+ except KeyError:
87
+ return {'error': 'The selected NLLB model does not support one of these languages.'}
88
+ else:
89
+ source, target = source_language, target_language
90
+
91
  try:
92
+ response = requests.post(
93
+ settings.HF_INFERENCE_URL.format(model_id=model_id),
94
+ headers={'Authorization': f'Bearer {settings.HF_TOKEN}'},
95
+ json={'inputs': text, 'parameters': {'src_lang': source, 'tgt_lang': target}},
96
+ timeout=60,
97
+ )
98
+ if response.status_code != 200:
99
+ logger.warning('Hugging Face translation failed: %s %s', response.status_code, response.text)
100
+ return {'error': 'The ML translation service could not complete this request.'}
101
+ translated_text = response.json().get('translation_text')
102
+ if not translated_text:
103
+ return {'error': 'The ML model returned no translation.'}
104
+ return {'translated_text': translated_text}
 
 
 
 
 
 
 
 
 
105
  except requests.exceptions.Timeout:
106
+ return {'error': 'The ML translation request timed out.'}
107
  except requests.exceptions.ConnectionError:
108
+ return {'error': 'Could not connect to the ML translation service.'}
109
+ except (ValueError, requests.RequestException) as error:
110
+ logger.exception('ML translation failed: %s', error)
111
+ return {'error': 'The ML translation service returned an invalid response.'}
112
+
113
+ def _translate_deepl(self, text, source_language, target_language):
114
+ if not settings.TRANSLATION_API_KEY:
115
+ return {'error': 'Traditional fallback is not configured.'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  try:
117
+ response = requests.post(
118
+ settings.TRANSLATION_API_URL,
119
+ headers={'Authorization': f'DeepL-Auth-Key {settings.TRANSLATION_API_KEY}'},
120
+ json={'text': [text], 'source_lang': source_language.upper(), 'target_lang': target_language.upper()},
121
+ timeout=30,
122
+ )
123
+ if response.status_code != 200:
124
+ return {'error': 'The fallback translation service could not complete this request.'}
125
+ translated_text = response.json()['translations'][0]['text']
126
+ return {'translated_text': translated_text}
127
+ except (KeyError, IndexError, ValueError, requests.RequestException) as error:
128
+ logger.exception('Fallback translation failed: %s', error)
129
+ return {'error': 'The fallback translation service returned an invalid response.'}
130
+
131
+ @staticmethod
132
+ def _generate_cache_key(text, source_language, target_language, model):
133
+ content = f'{text}|{source_language}|{target_language}|{model}'
134
  return hashlib.sha256(content.encode('utf-8')).hexdigest()
135
+
136
+ @staticmethod
137
+ def _get_from_cache(cache_key):
138
  try:
139
+ entry = TranslationCache.objects.filter(source_text_hash=cache_key).first()
140
+ if not entry:
141
+ return None
142
+ entry.access_count += 1
143
+ entry.last_accessed = timezone.now()
144
+ entry.save(update_fields=['access_count', 'last_accessed'])
145
+ return {'source_text': entry.source_text, 'translated_text': entry.translated_text,
146
+ 'source_language': entry.source_language_code, 'target_language': entry.target_language_code,
147
+ 'from_cache': True}
148
+ except Exception:
149
+ logger.exception('Translation cache lookup failed')
 
 
 
 
 
150
  return None
151
+
152
+ @staticmethod
153
+ def _save_to_cache(cache_key, source_text, translated_text, source_language, target_language):
 
 
 
154
  try:
155
  TranslationCache.objects.update_or_create(
156
  source_text_hash=cache_key,
157
+ defaults={'source_text': source_text, 'translated_text': translated_text,
158
+ 'source_language_code': source_language, 'target_language_code': target_language},
 
 
 
 
159
  )
160
+ except Exception:
161
+ logger.exception('Translation cache save failed')
translator_app/translator/templates/translator/base.html ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% load static %}
2
+ <!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{% block title %}The Translator App{% endblock %}</title>
7
+ <script src="https://cdn.tailwindcss.com?plugins=forms"></script>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@500;600;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <script>tailwind.config={theme:{extend:{colors:{primary:'#84532e',ink:'#22303a',cream:'#fffaf6',sand:'#ffbe91',mist:'#f3e9e1'},fontFamily:{display:['Bricolage Grotesque'],body:['Plus Jakarta Sans']}}}}</script>
11
+ <style>body{background:#fffaf6}.quiet-loading:after{content:'';position:absolute;bottom:0;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent,#84532e,transparent);animation:load 1.4s infinite}@keyframes load{from{transform:translateX(-100%)}to{transform:translateX(100%)}}</style>
12
+ {% block extra_head %}{% endblock %}
13
+ </head>
14
+ <body class="min-h-screen font-body text-ink flex flex-col">
15
+ <header class="w-full max-w-6xl mx-auto px-5 pt-9 pb-7 text-center">
16
+ <a href="{% url 'translator:translator' %}" class="font-display text-3xl md:text-5xl font-extrabold tracking-tight text-primary">The Translator App</a>
17
+ </header>
18
+ {% block content %}{% endblock %}
19
+ <footer class="py-10 text-center text-sm text-stone-500"><p>© {% now 'Y' %} The Translator App</p></footer>
20
+ </body>
21
+ </html>
translator_app/translator/templates/translator/history.html ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'translator/base.html' %}
2
+ {% block title %}History · The Translator App{% endblock %}
3
+ {% block content %}
4
+ <main class="flex-1 w-full max-w-3xl mx-auto px-5 pb-12">
5
+ <div class="mb-7 flex items-center justify-between"><h1 class="font-display text-3xl font-bold text-primary">History</h1>{% if translations %}<form method="post" action="{% url 'translator:clear_history' %}">{% csrf_token %}<button class="text-sm font-bold text-red-700 hover:underline">Clear all</button></form>{% endif %}</div>
6
+ <div class="space-y-4">{% for item in translations %}<article class="group rounded-xl border border-stone-100 bg-white p-5 shadow-sm"><div class="mb-3 flex justify-between gap-4 text-xs font-bold uppercase tracking-wider text-stone-400"><span>{{ item.source_language.name|default:item.detected_source_language|default:'Detected' }} → {{ item.target_language.name|default:'Unknown' }}</span><time>{{ item.created_at|timesince }} ago</time></div><p class="text-stone-700">{{ item.source_text }}</p><p class="mt-2 font-medium italic text-primary">{{ item.translated_text }}</p><div class="mt-3 flex items-center justify-between"><span class="text-xs text-stone-400">{{ item.model_label|default:'Translation' }}</span><form method="post" action="{% url 'translator:delete_history' item.pk %}">{% csrf_token %}<button class="text-xs font-bold text-stone-400 hover:text-red-700">Delete</button></form></div></article>{% empty %}<div class="rounded-xl border border-dashed border-stone-300 p-12 text-center text-stone-500">No translations yet. <a class="font-bold text-primary underline" href="{% url 'translator:translator' %}">Start translating</a>.</div>{% endfor %}</div>
7
+ <div class="mt-10 text-center"><a class="rounded-full bg-primary px-6 py-3 text-sm font-bold text-white" href="{% url 'translator:translator' %}">Back to translate</a></div>
8
+ </main>
9
+ {% endblock %}
translator_app/translator/templates/translator/translator.html ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends 'translator/base.html' %}
2
+ {% block content %}
3
+ <main class="flex-1 w-full max-w-5xl mx-auto px-5 pb-12">
4
+ <form id="translation-form">
5
+ {% csrf_token %}
6
+ <div class="flex flex-wrap justify-center items-center gap-3 mb-5">
7
+ <select id="source-language" aria-label="Source language" class="rounded-full border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-primary shadow-sm focus:border-primary focus:ring-primary">
8
+ <option value="">Detect language</option>{% for language in languages %}<option value="{{ language.code }}"{% if language.code == 'en' %} selected{% endif %}>{{ language.name }}</option>{% endfor %}
9
+ </select>
10
+ <button id="swap-languages" type="button" title="Swap languages" class="rounded-full bg-sand px-3 py-2.5 text-primary transition hover:rotate-180">⇄</button>
11
+ <select id="target-language" aria-label="Target language" class="rounded-full border-stone-200 bg-white px-5 py-2.5 text-sm font-semibold text-primary shadow-sm focus:border-primary focus:ring-primary">
12
+ {% for language in languages %}<option value="{{ language.code }}"{% if language.code == 'sw' %} selected{% endif %}>{{ language.name }}</option>{% endfor %}
13
+ </select>
14
+ <label class="flex items-center gap-2 rounded-full border border-stone-200 bg-white px-4 py-2 text-sm font-semibold text-stone-600 shadow-sm">Model
15
+ <select id="translation-model" aria-label="Translation model" class="border-0 bg-transparent py-0 pl-1 pr-7 text-primary focus:ring-0">
16
+ {% for value, label in models %}<option value="{{ value }}"{% if value == default_model %} selected{% endif %}>{{ label }}</option>{% endfor %}
17
+ </select>
18
+ </label>
19
+ </div>
20
+ <div class="rounded-2xl border border-stone-200 bg-white p-5 md:p-7 shadow-xl shadow-stone-200/40 md:flex md:gap-7">
21
+ <section class="flex min-h-72 flex-1 flex-col">
22
+ <label for="source-text" class="mb-3 text-xs font-bold uppercase tracking-widest text-stone-400">Source</label>
23
+ <textarea id="source-text" maxlength="5000" class="min-h-52 w-full flex-1 resize-none border-0 p-0 font-display text-2xl leading-relaxed text-primary placeholder:text-stone-300 focus:ring-0" placeholder="Enter text to translate..."></textarea>
24
+ <div class="mt-3 flex justify-between border-t border-stone-100 pt-3 text-xs text-stone-400"><button id="clear-source" type="button" class="font-semibold text-primary hover:underline">Clear</button><span id="character-count">0 / 5000 characters</span></div>
25
+ </section>
26
+ <div class="my-5 hidden w-px bg-stone-100 md:block"></div>
27
+ <section class="relative flex min-h-72 flex-1 flex-col">
28
+ <div class="mb-3 flex items-center justify-between"><span class="text-xs font-bold uppercase tracking-widest text-stone-400">Translation</span><button id="copy-result" type="button" class="hidden rounded-full px-3 py-1 text-xs font-bold text-primary hover:bg-mist">Copy</button></div>
29
+ <p id="translated-text" aria-live="polite" class="relative flex-1 whitespace-pre-wrap font-display text-2xl leading-relaxed text-primary"><span class="text-stone-300">Your translation will appear here.</span></p>
30
+ <p id="result-meta" class="mt-3 text-xs text-stone-400"></p>
31
+ </section>
32
+ </div>
33
+ <div class="mt-6 flex flex-col items-center gap-3"><button id="translate-button" class="rounded-full bg-primary px-10 py-4 font-display text-lg font-bold uppercase tracking-wider text-white shadow-lg shadow-primary/30 transition hover:scale-105 disabled:cursor-wait disabled:opacity-70">Translate</button><p id="form-message" role="alert" class="min-h-5 text-sm text-red-700"></p></div>
34
+ </form>
35
+ <div class="mt-12 text-center"><a href="{% url 'translator:history' %}" class="text-sm font-bold text-primary underline underline-offset-4 hover:text-stone-700">View history</a></div>
36
+ </main>
37
+ <script>
38
+ const form=document.querySelector('#translation-form'), source=document.querySelector('#source-text'), result=document.querySelector('#translated-text'), message=document.querySelector('#form-message'), button=document.querySelector('#translate-button'), count=document.querySelector('#character-count');
39
+ const csrf=document.querySelector('[name=csrfmiddlewaretoken]').value;
40
+ source.addEventListener('input',()=>count.textContent=`${source.value.length} / 5000 characters`);
41
+ document.querySelector('#clear-source').onclick=()=>{source.value='';source.dispatchEvent(new Event('input'));source.focus()};
42
+ document.querySelector('#swap-languages').onclick=()=>{const a=document.querySelector('#source-language'),b=document.querySelector('#target-language');if(a.value){[a.value,b.value]=[b.value,a.value]}};
43
+ document.querySelector('#copy-result').onclick=async()=>{await navigator.clipboard.writeText(result.textContent);document.querySelector('#copy-result').textContent='Copied';setTimeout(()=>document.querySelector('#copy-result').textContent='Copy',1200)};
44
+ form.addEventListener('submit',async e=>{e.preventDefault();message.textContent='';button.disabled=true;button.textContent='Translating…';result.classList.add('quiet-loading');
45
+ try {const response=await fetch("{% url 'translator:translate_api' %}",{method:'POST',headers:{'Content-Type':'application/json','X-CSRFToken':csrf},body:JSON.stringify({text:source.value,source_language:document.querySelector('#source-language').value,target_language:document.querySelector('#target-language').value,model:document.querySelector('#translation-model').value})});const data=await response.json();if(!response.ok)throw Error(data.error||'Translation could not be completed.');result.textContent=data.translated_text;document.querySelector('#result-meta').textContent=`${data.model_label||'Translation'} · saved to history`;document.querySelector('#copy-result').classList.remove('hidden')}catch(error){message.textContent=error.message;result.innerHTML='<span class="text-stone-300">Your translation will appear here.</span>'}finally{button.disabled=false;button.textContent='Translate';result.classList.remove('quiet-loading')}});
46
+ </script>
47
+ {% endblock %}
translator_app/translator/tests.py CHANGED
@@ -1,3 +1,35 @@
1
  from django.test import TestCase
 
2
 
3
- # Create your tests here.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from django.test import TestCase
2
+ from unittest.mock import patch
3
 
4
+ from django.urls import reverse
5
+
6
+ from .models import TranslationHistory
7
+
8
+ class TranslatorViewsTests(TestCase):
9
+ def test_translator_page_renders_model_picker(self):
10
+ response = self.client.get(reverse('translator:translator'))
11
+ self.assertContains(response, 'Translation model')
12
+ self.assertContains(response, 'NLLB-200 (600M')
13
+
14
+ @patch('translator.views.TranslationService.translate')
15
+ def test_translation_api_saves_history(self, translate):
16
+ translate.return_value = {
17
+ 'source_text': 'Hello', 'translated_text': 'Jambo',
18
+ 'source_language': 'en', 'target_language': 'sw', 'model': 'nllb_600m',
19
+ }
20
+ response = self.client.post(
21
+ reverse('translator:translate_api'),
22
+ data='{"text":"Hello","source_language":"en","target_language":"sw","model":"nllb_600m"}',
23
+ content_type='application/json',
24
+ )
25
+ self.assertEqual(response.status_code, 200)
26
+ self.assertEqual(response.json()['translated_text'], 'Jambo')
27
+ history = TranslationHistory.objects.get()
28
+ self.assertEqual(history.translation_model, 'nllb_600m')
29
+ self.assertEqual(history.source_text, 'Hello')
30
+
31
+ def test_translation_api_rejects_missing_text(self):
32
+ response = self.client.post(
33
+ reverse('translator:translate_api'), data='{}', content_type='application/json'
34
+ )
35
+ self.assertEqual(response.status_code, 400)
translator_app/translator/urls.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from django.urls import path
2
+
3
+ from . import views
4
+
5
+
6
+ app_name = 'translator'
7
+
8
+ urlpatterns = [
9
+ path('', views.translator, name='translator'),
10
+ path('history/', views.history, name='history'),
11
+ path('api/translate/', views.translate_api, name='translate_api'),
12
+ path('history/<int:pk>/delete/', views.delete_history, name='delete_history'),
13
+ path('history/clear/', views.clear_history, name='clear_history'),
14
+ ]
translator_app/translator/views.py CHANGED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from django.conf import settings
4
+ from django.http import JsonResponse
5
+ from django.shortcuts import get_object_or_404, redirect, render
6
+ from django.views.decorators.http import require_POST
7
+
8
+ from .models import Language, TranslationHistory
9
+ from .services.translation_service import TranslationService
10
+
11
+
12
+ def _languages():
13
+ """Use database languages when curated, otherwise use provider defaults."""
14
+ saved = Language.objects.filter(is_active=True).values('code', 'name')
15
+ return list(saved) or TranslationService().get_supported_languages()
16
+
17
+
18
+ def translator(request):
19
+ languages = _languages()
20
+ return render(request, 'translator/translator.html', {
21
+ 'languages': languages,
22
+ 'models': settings.TRANSLATION_MODELS,
23
+ 'default_model': settings.TRANSLATION_DEFAULT_MODEL,
24
+ })
25
+
26
+
27
+ def history(request):
28
+ translations = TranslationHistory.objects.select_related(
29
+ 'source_language', 'target_language'
30
+ )[:100]
31
+ model_labels = dict(settings.TRANSLATION_MODELS)
32
+ for item in translations:
33
+ item.model_label = model_labels.get(item.translation_model, item.translation_model)
34
+ return render(request, 'translator/history.html', {
35
+ 'translations': translations,
36
+ })
37
+
38
+
39
+ @require_POST
40
+ def translate_api(request):
41
+ try:
42
+ payload = json.loads(request.body)
43
+ except (TypeError, json.JSONDecodeError):
44
+ return JsonResponse({'error': 'Send a valid JSON request.'}, status=400)
45
+
46
+ text = (payload.get('text') or '').strip()
47
+ source_code = (payload.get('source_language') or '').lower()
48
+ target_code = (payload.get('target_language') or '').lower()
49
+ model = payload.get('model') or settings.TRANSLATION_DEFAULT_MODEL
50
+ allowed_models = dict(settings.TRANSLATION_MODELS)
51
+
52
+ if not text:
53
+ return JsonResponse({'error': 'Enter text to translate.'}, status=400)
54
+ if len(text) > 5000:
55
+ return JsonResponse({'error': 'Text is limited to 5,000 characters.'}, status=400)
56
+ if not target_code:
57
+ return JsonResponse({'error': 'Choose a target language.'}, status=400)
58
+ if model not in allowed_models:
59
+ return JsonResponse({'error': 'Choose a valid translation model.'}, status=400)
60
+
61
+ service = TranslationService()
62
+ result = service.translate(text, target_code, source_code or None, model=model)
63
+ if result.get('error'):
64
+ return JsonResponse(result, status=502)
65
+
66
+ source_code = result.get('source_language', source_code)
67
+ known_languages = {item['code']: item['name'] for item in service.get_supported_languages()}
68
+
69
+ def language_for(code):
70
+ if not code or code == 'auto-detected':
71
+ return None
72
+ language, _ = Language.objects.get_or_create(
73
+ code=code,
74
+ defaults={'name': known_languages.get(code, code.upper())},
75
+ )
76
+ return language
77
+
78
+ source_language = language_for(source_code)
79
+ target_language = language_for(target_code)
80
+ TranslationHistory.objects.create(
81
+ user=request.user if request.user.is_authenticated else None,
82
+ source_text=text,
83
+ translated_text=result['translated_text'],
84
+ source_language=source_language,
85
+ target_language=target_language,
86
+ detected_source_language=source_code if source_code != 'auto-detected' else '',
87
+ translation_model=model,
88
+ )
89
+ result['model_label'] = allowed_models[model]
90
+ return JsonResponse(result)
91
+
92
+
93
+ @require_POST
94
+ def delete_history(request, pk):
95
+ get_object_or_404(TranslationHistory, pk=pk).delete()
96
+ return redirect('translator:history')
97
+
98
+
99
+ @require_POST
100
+ def clear_history(request):
101
+ TranslationHistory.objects.all().delete()
102
+ return redirect('translator:history')
translator_app/translator_project/__pycache__/__init__.cpython-314.pyc CHANGED
Binary files a/translator_app/translator_project/__pycache__/__init__.cpython-314.pyc and b/translator_app/translator_project/__pycache__/__init__.cpython-314.pyc differ
 
translator_app/translator_project/__pycache__/settings.cpython-314.pyc CHANGED
Binary files a/translator_app/translator_project/__pycache__/settings.cpython-314.pyc and b/translator_app/translator_project/__pycache__/settings.cpython-314.pyc differ
 
translator_app/translator_project/__pycache__/urls.cpython-314.pyc CHANGED
Binary files a/translator_app/translator_project/__pycache__/urls.cpython-314.pyc and b/translator_app/translator_project/__pycache__/urls.cpython-314.pyc differ
 
translator_app/translator_project/__pycache__/wsgi.cpython-314.pyc CHANGED
Binary files a/translator_app/translator_project/__pycache__/wsgi.cpython-314.pyc and b/translator_app/translator_project/__pycache__/wsgi.cpython-314.pyc differ
 
translator_app/translator_project/settings.py CHANGED
@@ -8,98 +8,120 @@ load_dotenv()
8
 
9
  BASE_DIR = Path(__file__).resolve().parent.parent
10
 
11
- SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'your-secret-key-here')
12
 
13
  DEBUG = True
14
 
15
- ALLOWED_HOSTS = ['localhost', '127.0.0.1']
16
 
17
  # Application definition
18
  INSTALLED_APPS = [
19
- 'django.contrib.admin',
20
- 'django.contrib.auth',
21
- 'django.contrib.contenttypes',
22
- 'django.contrib.sessions',
23
- 'django.contrib.messages',
24
- 'django.contrib.staticfiles',
25
- 'rest_framework',
26
- 'translator', # Our app
27
  ]
28
 
29
  MIDDLEWARE = [
30
- 'django.middleware.security.SecurityMiddleware',
31
- 'django.contrib.sessions.middleware.SessionMiddleware',
32
- 'django.middleware.common.CommonMiddleware',
33
- 'django.middleware.csrf.CsrfViewMiddleware',
34
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
35
- 'django.contrib.messages.middleware.MessageMiddleware',
36
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
37
  ]
38
 
39
- ROOT_URLCONF = 'translator_project.urls'
40
 
41
  TEMPLATES = [
42
  {
43
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
44
- 'DIRS': [],
45
- 'APP_DIRS': True,
46
- 'OPTIONS': {
47
- 'context_processors': [
48
- 'django.template.context_processors.debug',
49
- 'django.template.context_processors.request',
50
- 'django.contrib.auth.context_processors.auth',
51
- 'django.contrib.messages.context_processors.messages',
52
  ],
53
  },
54
  },
55
  ]
56
 
57
- WSGI_APPLICATION = 'translator_project.wsgi.application'
58
 
59
  # Database
60
  DATABASES = {
61
- 'default': {
62
- 'ENGINE': 'django.db.backends.sqlite3',
63
- 'NAME': BASE_DIR / 'db.sqlite3',
64
  }
65
  }
66
 
67
  # Password validation
68
  AUTH_PASSWORD_VALIDATORS = [
69
  {
70
- 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
71
  },
72
  {
73
- 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
74
  },
75
  {
76
- 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
77
  },
78
  {
79
- 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
80
  },
81
  ]
82
 
83
  # Internationalization
84
- LANGUAGE_CODE = 'en-us'
85
- TIME_ZONE = 'UTC'
86
  USE_I18N = True
87
  USE_TZ = True
88
 
89
  # Static files
90
- STATIC_URL = 'static/'
91
- STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
92
- STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
93
 
94
  # Media files
95
- MEDIA_URL = '/media/'
96
- MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
97
 
98
  # Default primary key field type
99
- DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- # Translation API settings
102
- TRANSLATION_API_KEY = os.getenv('TRANSLATION_API_KEY')
103
- TRANSLATION_API_URL = os.getenv('TRANSLATION_API_URL', 'https://api-free.deepl.com/v2/translate')
 
 
 
 
 
104
  # Or for Google Translate:
105
- # TRANSLATION_API_URL = 'https://translation.googleapis.com/language/translate/v2'
 
8
 
9
  BASE_DIR = Path(__file__).resolve().parent.parent
10
 
11
+ SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "your-secret-key-here")
12
 
13
  DEBUG = True
14
 
15
+ ALLOWED_HOSTS = ["localhost", "127.0.0.1"]
16
 
17
  # Application definition
18
  INSTALLED_APPS = [
19
+ "django.contrib.admin",
20
+ "django.contrib.auth",
21
+ "django.contrib.contenttypes",
22
+ "django.contrib.sessions",
23
+ "django.contrib.messages",
24
+ "django.contrib.staticfiles",
25
+ "rest_framework",
26
+ "translator", # Our app
27
  ]
28
 
29
  MIDDLEWARE = [
30
+ "django.middleware.security.SecurityMiddleware",
31
+ "django.contrib.sessions.middleware.SessionMiddleware",
32
+ "django.middleware.common.CommonMiddleware",
33
+ "django.middleware.csrf.CsrfViewMiddleware",
34
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
35
+ "django.contrib.messages.middleware.MessageMiddleware",
36
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
37
  ]
38
 
39
+ ROOT_URLCONF = "translator_project.urls"
40
 
41
  TEMPLATES = [
42
  {
43
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
44
+ "DIRS": [],
45
+ "APP_DIRS": True,
46
+ "OPTIONS": {
47
+ "context_processors": [
48
+ "django.template.context_processors.debug",
49
+ "django.template.context_processors.request",
50
+ "django.contrib.auth.context_processors.auth",
51
+ "django.contrib.messages.context_processors.messages",
52
  ],
53
  },
54
  },
55
  ]
56
 
57
+ WSGI_APPLICATION = "translator_project.wsgi.application"
58
 
59
  # Database
60
  DATABASES = {
61
+ "default": {
62
+ "ENGINE": "django.db.backends.sqlite3",
63
+ "NAME": BASE_DIR / "db.sqlite3",
64
  }
65
  }
66
 
67
  # Password validation
68
  AUTH_PASSWORD_VALIDATORS = [
69
  {
70
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
71
  },
72
  {
73
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
74
  },
75
  {
76
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
77
  },
78
  {
79
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
80
  },
81
  ]
82
 
83
  # Internationalization
84
+ LANGUAGE_CODE = "en-us"
85
+ TIME_ZONE = "UTC"
86
  USE_I18N = True
87
  USE_TZ = True
88
 
89
  # Static files
90
+ STATIC_URL = "static/"
91
+ # STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
92
+ STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
93
 
94
  # Media files
95
+ MEDIA_URL = "/media/"
96
+ MEDIA_ROOT = os.path.join(BASE_DIR, "media")
97
 
98
  # Default primary key field type
99
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
100
+
101
+ # ML translation settings. Keep secrets in translator_app/.env, never in source.
102
+ HF_TOKEN = os.getenv("HF_TOKEN")
103
+ HF_INFERENCE_URL = os.getenv(
104
+ "HF_INFERENCE_URL", "https://router.huggingface.co/hf-inference/models/{model_id}"
105
+ )
106
+ TRANSLATION_DEFAULT_MODEL = os.getenv("TRANSLATION_DEFAULT_MODEL", "nllb_600m")
107
+ TRANSLATION_MODELS = (
108
+ ("nllb_600m", "NLLB-200 (600M · Fast)"),
109
+ ("nllb_1.3b", "NLLB-200 (1.3B · Accurate)"),
110
+ ("madlad_3b", "MADLAD-400 (3B · Large)"),
111
+ )
112
+ TRANSLATION_MODEL_IDS = {
113
+ "nllb_600m": "facebook/nllb-200-distilled-600M",
114
+ "nllb_1.3b": "facebook/nllb-200-distilled-1.3B",
115
+ "madlad_3b": "google/madlad-400-3b-mt",
116
+ }
117
 
118
+ # Optional traditional-provider fallback. Add this option to TRANSLATION_MODELS
119
+ # only when a DeepL-compatible endpoint and key are configured.
120
+ TRANSLATION_API_KEY = os.getenv("TRANSLATION_API_KEY")
121
+ TRANSLATION_API_URL = os.getenv(
122
+ "TRANSLATION_API_URL", "https://api-free.deepl.com/v2/translate"
123
+ )
124
+ if TRANSLATION_API_KEY:
125
+ TRANSLATION_MODELS += (("deepl_fallback", "DeepL · Traditional fallback"),)
126
  # Or for Google Translate:
127
+ # TRANSLATION_API_URL = 'https://translation.googleapis.com/language/translate/v2'
translator_app/translator_project/urls.py CHANGED
@@ -15,8 +15,9 @@ Including another URLconf
15
  2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16
  """
17
  from django.contrib import admin
18
- from django.urls import path
19
 
20
  urlpatterns = [
21
  path('admin/', admin.site.urls),
 
22
  ]
 
15
  2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16
  """
17
  from django.contrib import admin
18
+ from django.urls import include, path
19
 
20
  urlpatterns = [
21
  path('admin/', admin.site.urls),
22
+ path('', include('translator.urls')),
23
  ]