Skip to content

Commit 9f95c95

Browse files
authored
Merge pull request #1 from Nabute/feature/pluggable-translation-service
Add Pluggable Translation Service to `lisan` Package for Dynamic Translations
2 parents 576a6e1 + d44a2fb commit 9f95c95

File tree

6 files changed

+282
-107
lines changed

6 files changed

+282
-107
lines changed

README.md

+96-77
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Lisan - ልሳን
22

3-
**means**: A language
3+
**means**: A language in Amharic (Ethiopian Language)
44

55
**Lisan** is a Django package that simplifies the process of adding translation support to model fields in Django projects. With `Lisan`, you can easily manage multilingual content within your Django models, API, and admin interface.
66

@@ -10,6 +10,27 @@
1010
- **Admin Integration:** Seamlessly manage translations through the Django admin interface.
1111
- **Fallback Mechanism:** Fallback to the default language if a translation is not available.
1212
- **Dynamic Getter Methods:** Automatically generate methods to access translated fields.
13+
- **Pluggable Translation Services:** Support for external services like Google Translate to automatically translate content.
14+
- **Customizable Admin Display:** Configure how translations are displayed in the Django admin interface.
15+
- **Flexible Field Types:** Add translation support for various field types like `CharField`, `TextField`, and `JSONField`.
16+
17+
## Table of Contents
18+
19+
- [Installation](#installation)
20+
- [Lisan Settings](#lisan-settings)
21+
- [Usage](#usage)
22+
- [Adding Translation Support to Models](#1-adding-translation-support-to-models)
23+
- [Managing Translations in Django Admin](#2-managing-translations-in-django-admin)
24+
- [Accessing Translations in Code](#3-accessing-translations-in-code)
25+
- [API Usage](#api-usage)
26+
- [Creating a Snippet with Translations](#1-creating-a-snippet-with-translations)
27+
- [Retrieving a Snippet with a Specific Translation](#2-retrieving-a-snippet-with-a-specific-translation)
28+
- [Handling User Preferences for Translations](#handling-user-preferences-for-translations)
29+
- [Pluggable Translation Services](#pluggable-translation-services)
30+
- [Creating Custom Translation Services](#creating-custom-translation-services)
31+
- [Testing Translations](#testing-translations)
32+
- [Contributing](#contributing)
33+
- [License](#license)
1334

1435
## Installation
1536

@@ -30,6 +51,8 @@ To start using `Lisan` in your project, you need to configure the language setti
3051
```python
3152
LISAN_DEFAULT_LANGUAGE = 'en' # Default language for translations
3253
LISAN_ALLOWED_LANGUAGES = ['en', 'am', 'or', 'tg'] # Languages supported by Lisan
54+
LISAN_FALLBACK_LANGUAGES = ['fr', 'es', 'en'] # Customize fallback languages
55+
LISAN_DEFAULT_TRANSLATION_SERVICE = 'yourapp.google_translate_service.GoogleTranslateService' # Pluggable translation service
3356
```
3457

3558
#### Step 1.1: Add Lisan Middleware
@@ -55,24 +78,13 @@ Example:
5578

5679
```python
5780
from django.db import models
58-
from pygments.lexers import get_all_lexers
59-
from pygments.styles import get_all_styles
6081
from lisan import LisanModelMixin
6182

62-
LEXERS = [item for item in get_all_lexers() if item[1]]
63-
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
64-
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])
65-
66-
# Add the LisanModelMixin mixin
6783
class Snippet(LisanModelMixin, models.Model):
68-
lisan_fields = ['title', 'language', 'style'] # Fields to be translated
69-
70-
created = models.DateTimeField(auto_now_add=True)
84+
lisan_fields = ['title', 'description'] # Fields to be translated
7185
title = models.CharField(max_length=100, blank=True, default='')
72-
code = models.TextField()
73-
linenos = models.BooleanField(default=False)
74-
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
75-
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
86+
description = models.TextField(blank=True, default='')
87+
created = models.DateTimeField(auto_now_add=True)
7688

7789
class Meta:
7890
ordering = ['created']
@@ -100,6 +112,7 @@ from .models import Snippet
100112
class SnippetAdmin(LisanAdminMixin, admin.ModelAdmin):
101113
list_filter = ('created',)
102114
ordering = ('-created',)
115+
lisan_display_format = "{field_name} ({language_code})" # Customizable admin display format
103116
```
104117

105118
### 3. Accessing Translations in Code
@@ -109,17 +122,17 @@ You can set and get translations using the provided methods.
109122
#### Setting a Translation
110123

111124
```python
112-
snippet = Snippet.objects.create(title="Code Snippet Example", code="def example_function():\n return 'Hello, World!'")
113-
snippet.set_lisan('am', title="ኮድ ቅርጸት ምሳሌ")
125+
snippet = Snippet.objects.create(title="Code Snippet Example", description="Example Description")
126+
snippet.set_lisan('am', title="ኮድ ቅርጸት ምሳሌ", description="እንቁ ምሳሌ")
114127
```
115128

116-
#### Getting a Translation
129+
#### Getting a Translation with Fallback and Auto-Translate
117130

118-
You can retrieve translations with a fallback mechanism. For example, if a translation in Amharic is not available, it will default to the base language (e.g., English).
131+
You can retrieve translations with a fallback mechanism or even auto-translate content using an external service.
119132

120133
```python
121-
amharic_title = snippet.get_lisan_field('title', 'am')
122-
print(amharic_title) # Output: ኮድ ቅርጸት ምሳሌ
134+
amharic_title = snippet.get_lisan_field('title', 'am', auto_translate=True)
135+
print(amharic_title) # Output: ኮድ ቅርጸት ምሳሌ (if available) or the auto-translated version
123136

124137
english_title = snippet.get_lisan_field('title', 'en')
125138
print(english_title) # Output: Code Snippet Example
@@ -138,42 +151,20 @@ To create a snippet with translations, send a `POST` request to the appropriate
138151
```json
139152
{
140153
"title": "Code Snippet Example",
141-
"code": "def example_function():\n return 'Hello, World!'",
142-
"linenos": true,
143-
"language": "python",
144-
"style": "friendly",
154+
"description": "Example Description",
145155
"translations": [
146-
{
147-
"language_code": "en",
148-
"title": "Code Snippet Example",
149-
"language": "python",
150-
"style": "friendly"
151-
},
152156
{
153157
"language_code": "am",
154158
"title": "ኮድ ቅርጸት ምሳሌ",
155-
"language": "ፒያዝ",
156-
"style": "ወዳጅ"
157-
},
158-
{
159-
"language_code": "or",
160-
"title": "Miseensa Koodii Fakkeenya",
161-
"language": "python",
162-
"style": "bareedaa"
163-
},
164-
{
165-
"language_code": "tg",
166-
"title": "ምሳሌ ውሂብ ቅርጸት",
167-
"language": "python",
168-
"style": "ናብኣይ"
159+
"description": "እንቁ ምሳሌ"
169160
}
170161
]
171162
}
172163
```
173164

174165
### 2. Retrieving a Snippet with a Specific Translation
175166

176-
To retrieve a snippet in a specific language, send a `GET` request with the appropriate `Accept-Language` header to specify the desired language (e.g., `am` for Amharic, `or` for Oromo).
167+
To retrieve a snippet in a specific language, send a `GET` request with the appropriate `Accept-Language` header to specify the desired language (e.g., `am` for Amharic).
177168

178169
**Request Example**:
179170

@@ -190,57 +181,85 @@ The response will return the snippet information in the requested language if av
190181
{
191182
"id": 1,
192183
"title": "ኮድ ቅርጸት ምሳሌ",
193-
"code": "def example_function():\n return 'Hello, World!'",
194-
"linenos": true,
195-
"language": "ፒያዝ",
196-
"style": "ወዳጅ"
184+
"description": "እንቁ ምሳሌ"
197185
}
198186
```
199187

200-
### 3. Serializer for Snippets
188+
### Handling User Preferences for Translations
201189

202-
Use `LisanSerializerMixin` in your serializer to handle translations.
190+
You can dynamically handle user preferences for translations based on the `Accept-Language` header or custom user settings.
203191

204192
```python
205-
from rest_framework import serializers
206-
from lisan.serializers import LisanSerializerMixin
207-
from .models import Snippet
208-
209193
class SnippetSerializer(LisanSerializerMixin, serializers.ModelSerializer):
210194
class Meta:
211195
model = Snippet
212-
fields = ['id', 'title', 'code', 'linenos', 'language', 'style']
196+
fields = ['id', 'title', 'description']
197+
198+
def to_representation(self, instance):
199+
representation = super().to_representation(instance)
200+
request = self.context.get('request')
201+
language_code = request.headers.get('Accept-Language', 'en')
202+
representation['title'] = instance.get_lisan_field('title', language_code)
203+
return representation
213204
```
214205

215-
### 4. Snippet ViewSet
206+
## Pluggable Translation Services
207+
208+
The `Lisan` package supports pluggable translation services, allowing you to integrate with third-party APIs like Google Translate for automatic translations. You can configure this via the `LISAN_DEFAULT_TRANSLATION_SERVICE` setting.
209+
210+
### Creating Custom Translation Services
216211

217-
Define your `SnippetViewSet` and make sure to pass the request to the serializer context for handling language-specific responses.
212+
You can create custom translation services by implementing the `BaseTranslationService` class.
213+
214+
Example using Google Translate:
218215

219216
```python
220-
from rest_framework import viewsets
221-
from .models import Snippet
222-
from .serializers import SnippetSerializer
223-
224-
class SnippetViewSet(viewsets.ModelViewSet):
225-
queryset = Snippet.objects.all()
226-
serializer_class = SnippetSerializer
227-
228-
def get_serializer_context(self):
229-
"""
230-
Adds custom context to the serializer.
231-
"""
232-
context = super().get_serializer_context()
233-
context['request'] = self.request # Pass request context for translation handling
234-
return context
217+
# google_translate_service.py
218+
from googletrans import Translator
219+
from lisan.translation_services import BaseTranslationService
220+
221+
class GoogleTranslateService(BaseTranslationService):
222+
def __init__(self):
223+
self.translator = Translator()
224+
225+
def translate(self, text, target_language):
226+
return self.translator.translate(text, dest=target_language).text
235227
```
236228

237-
### Summary
229+
### Setting up the Service
230+
231+
To configure your application to use this service, simply set it in the `LISAN_DEFAULT_TRANSLATION_SERVICE` setting in `settings.py`:
232+
233+
```python
234+
LISAN_DEFAULT_TRANSLATION_SERVICE = 'yourapp.google_translate_service.GoogleTranslateService'
235+
```
236+
237+
## Testing Translations
238+
239+
To ensure that your translations are handled correctly, here’s an example of how you can write tests for translated fields:
240+
241+
```python
242+
from django.test import TestCase
243+
from .models import Snippet
244+
245+
class SnippetTestCase(TestCase):
246+
def setUp(self):
247+
self.snippet = Snippet.objects.create(title="Hello World", description="Description Example")
248+
249+
def test_translation_set(self):
250+
self.snippet.set_lisan('fr', title="Bonjour le monde", description="Exemple de description")
251+
self.assertEqual(self.snippet.get_lisan_field('title', 'fr'), "Bonjour le monde")
252+
253+
def test_fallback(self):
254+
# Test that it falls back to English if no French translation is available
255+
self.assertEqual(self.snippet.get_lisan_field('title', 'fr'), "Hello World")
256+
```
238257

239-
This `README.md` provides a comprehensive overview of the `lisan` package, including settings, installation, configuration, and usage instructions. It covers how to create and retrieve translations for Django models and includes API examples for managing translated content.
258+
This testing structure ensures that your translations work as expected, using both direct translations and fallback mechanisms when translations are unavailable.
240259

241260
## Contributing
242261

243-
If you find any issues or have suggestions for improvements, feel free to open an issue or submit a pull request on GitHub.
262+
If you find any issues or have suggestions for improvements, feel free to open an issue or submit a pull request on GitHub. Contributions are always welcome.
244263

245264
## License
246265

lisan/admin.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class LisanAdminMixin(admin.ModelAdmin):
1010
localized content within the Django admin interface.
1111
"""
1212

13+
lisan_display_format = "{field_name} ({language_code})"
14+
1315
def __init__(self, *args, **kwargs):
1416
"""
1517
Initialize the LisanAdminMixin.
@@ -31,12 +33,12 @@ def _generate_lisan_getters(self):
3133
for field_name in self.model.lisan_fields:
3234
method_name = f'get_lisan_{field_name}'
3335
setattr(self, method_name, self._create_lisan_getter(field_name))
34-
35-
# Set a short description for the admin list display
36-
short_description = f'{field_name.capitalize()} (EN)'
36+
37+
short_description = self.lisan_display_format.format(
38+
field_name=field_name.capitalize(), language_code='EN'
39+
)
3740
getattr(self, method_name).short_description = short_description
3841

39-
# Add the generated method to the list_display
4042
if 'list_display' in self.__dict__:
4143
self.list_display += (method_name,)
4244
else:

lisan/middleware.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ def process_request(self, request):
2525
Args:
2626
request: The HTTP request object.
2727
"""
28-
language_code = request.headers.get(
29-
'Accept-Language',
30-
getattr(settings, 'LISAN_DEFAULT_LANGUAGE', 'en')
28+
language_code = (
29+
request.GET.get('lang') or
30+
getattr(request.user, 'profile.language_preference', None) or
31+
request.COOKIES.get('language') or
32+
request.headers.get('Accept-Language') or
33+
'en'
3134
)
3235
request.language_code = language_code

0 commit comments

Comments
 (0)