12 | | * object per language |
13 | | |
14 | | === Multilingual application with weak relations === |
15 | | Some multilingual applications have weak relationships, so an object just needs to know which language it has and have a reference to objects with the other languages. (eg. wiki, see entry in other language link) |
16 | | |
17 | | Specialities: |
18 | | * each object is available in at least one language |
19 | | * there is not necessary a representation in an other language |
20 | | * if there is no representation in a language, it is not a automatic fallback |
21 | | * if there is a representation in an other language, a relation should be possible |
22 | | |
23 | | Model implementation: |
24 | | * object per language |
25 | | * link to group object, or grouping by groupid |
26 | | |
27 | | === Multilingual application with strong relations === |
28 | | Some multilingual applications share the logic in the object but have different localizations. (eg. customizable workflows, browsergame) |
29 | | |
30 | | Specialities: |
31 | | * each object is available exactly once |
32 | | * each object has at least one but can have more than one representations in different languages |
33 | | * there is not necessary a representation in all selectable languages |
34 | | * if there is no representation in default language, there should be an automatic fallback |
35 | | * it should be possible to display the object in multiple languages on same page |
36 | | |
37 | | Model implementation: |
38 | | * one object serves all languages (no logic duplication) |
39 | | |
40 | | == Approach overview == |
41 | | A brif overview of existing approaches with implementing projects. |
42 | | |
43 | | * propertycolumn is holding all translations (in a map) |
44 | | * [http://code.google.com/p/transdb/ transdb] |
45 | | |
46 | | * collumn per language for every translated proerty |
47 | | * [http://code.google.com/p/django-modeltranslation/ django-modeltranslation] |
48 | | * [http://code.google.com/p/django-transmeta/ django-transmeta] |
49 | | |
50 | | * big table holding all translations |
51 | | * [http://code.google.com/p/django-utils/wiki/TranslationService django-utils] |
52 | | |
53 | | * table per property holding all languages of this property |
54 | | |
55 | | * table for not internationalized properties and an extratable for the localized version |
56 | | * [http://code.google.com/p/django-multilingual/ django-multilingual] |
57 | | * language is saved as integer, which doesn't work for multiple site projects where language ids in setting can differ |
58 | | * [http://code.google.com/p/django-multilingual-model/ django-multilingual-model] |
59 | | * [http://github.com/foxbunny/django-i18n-model/ django-i18n-model] |
60 | | |
61 | | * object per language, the association between objects is provided by a goup-id (an update of the not internationalized fields would overwrite this fields in all other language objects) |
62 | | * [http://blog.rafaljonca.org/2009/03/third-style-of-django-multilingual-data.html a code example] |
63 | | * normal model with one language, additional table with all i18n fields in other languages |
64 | | * [http://code.google.com/p/django-pluggable-model-i18n/ django-pluggable-model-i18n] (status: experimental) |
65 | | |
| 24 | * one object per language |
| 25 | |
| 26 | === Multilingual model with multiple languages in one object === |
| 27 | The translations for all languages are stored on the same object. |
| 28 | |
| 29 | (2) One way could be: |
| 30 | |
| 31 | {{{ |
| 32 | class BlogEntry(models.Model): |
| 33 | title = models.CharField(max_length=255) |
| 34 | title_de = models.CharField(max_length=255) |
| 35 | title_ru = models.CharField(max_length=255) |
| 36 | # etc. |
| 37 | }}} |
| 38 | |
| 39 | Advantages: |
| 40 | * a conceptual object is represented by one real object |
| 41 | |
| 42 | Disadvantages: |
| 43 | * Schema changes necessary for additional languages |
| 44 | * FKs cannot directly reference a specific language and also need a language attribute |
| 45 | |
| 46 | (3) Another way could be: |
| 47 | Translations are stored for each model in another model and are JOINed on demand. |
| 48 | |
| 49 | Advantages: |
| 50 | * No schema changes |
| 51 | |
| 52 | Disadvantages: |
| 53 | * JOINs can be considered evil |
| 54 | |
| 55 | === Multilingual model with one object per language (4) === |
| 56 | The Model is language-aware but also has a indirect reference to the same object in different languages via a common key: |
| 57 | |
| 58 | Example: |
| 59 | |
| 60 | {{{ |
| 61 | class BlogEntry(models.Model): |
| 62 | language = models.CharField(max_length=20, db_index=true) |
| 63 | group_id = models.CharField(max_length=36) # e.g. UUID |
| 64 | # All database objects that represent the same logical object in different languages |
| 65 | # have the same group_id (possibly a UUID, may also be an integer). |
| 66 | |
| 67 | title = models.CharField(max_length=255) |
| 68 | # other fields |
| 69 | |
| 70 | }}} |
| 71 | |
| 72 | Advantages: |
| 73 | * flexible in respect to additional languages |
| 74 | * querying stays easy, since basic model capabilities hold |
| 75 | |
| 76 | Disadvantages: |
| 77 | * multiple model objects for one conceptual object, does not really hurt |
| 78 | * duplication of non-translatable fields (ie fields that stay the same across translations) |
| 79 | |
| 80 | |
| 81 | API for Model Translation in respect to (4) - a proposal/discussion: |
| 82 | |
| 83 | {{{ |
| 84 | class BlogEntry(TranslatableModel): |
| 85 | title = models.CharField(max_length=255) |
| 86 | author = models.ForeignKey(User) |
| 87 | # To mark fields as translatable/untranslatable: |
| 88 | # * title = models.CharField(max_length=255, translatable=True) |
| 89 | # * author = models.CharField(max_length=255, untranslatable=True) |
| 90 | last_modified = models.DateTimeField(auto_now=True) |
| 91 | |
| 92 | # or introduce a field in a Meta class |
| 93 | class Meta: |
| 94 | untranslatable_fields = ('last_modified','author',) |
| 95 | # or translatable_fields = ('title',) |
| 96 | |
| 97 | def __unicode__(self): |
| 98 | return _(u"Blog entry '%s'") % self.title # works of course |
| 99 | |
| 100 | # Relations: |
| 101 | # A foreign key may conceptually be the following: |
| 102 | # 1. it either references an object in a specific language |
| 103 | # 2. or it references a conceptual object in all of its languages |
| 104 | |
| 105 | # 1. |
| 106 | class Image(models.Model): |
| 107 | blog_entry_1 = models.ForeignKey(BlogEntry) |
| 108 | # this ForeignKey can reference the PK of the BlogEntry object, no problem |
| 109 | blog_entry_2 = models.ForeignKey(BlogEntry) |
| 110 | # this ForeignKey may reference language and group_id as a key, more complicated |
| 111 | |
| 112 | # 2. |
| 113 | class Image(models.Model): |
| 114 | blog_entry_1 = models.ForeignKey(BlogEntry) |
| 115 | # this ForeignKey can reference the PK of an BlogEntry object and get all other languages via group_id |
| 116 | blog_entry_2 = models.MultiLanguageForeignKey(BlogEntry) |
| 117 | # this ForeignKey can reference the group_id of a BlogEntry object and get objects in all languages |
| 118 | |
| 119 | |
| 120 | blog_entry = BlogEntry(title="Some title", author=some_user) |
| 121 | blog_entry.save() # saves in current language as default, new group |
| 122 | |
| 123 | # explicitly save for language, creates new group, new group_id is created |
| 124 | blog_entry.save(language="en") |
| 125 | # explicit language save, same group_id as other_blog_entry: |
| 126 | blog_entry.save(language="en",connect_to=other_blog_entry) |
| 127 | |
| 128 | # Manager API that does implicit current language retrieval: |
| 129 | blog_entry = BlogEntry.objects.get(group_id=UUID) |
| 130 | # returns blog entry in current language, |
| 131 | # ie behind the scenes: BlogEntry.objects.get(group_id=UUID, language=CURRENT_LANGUAGE) |
| 132 | |
| 133 | # Updates |
| 134 | blog_entry.author = some_other_user |
| 135 | blog_entry.title = "Some other title" |
| 136 | blog_entry.save() |
| 137 | # author is marked as untranslatable, therefore all instances must be updated |
| 138 | # this can be a single query: UPDATE blog_blogentry SET author_id=<some_other_user_id> WHERE group_id=<UUID> |
| 139 | # but needs #4102 to be fixed |
| 140 | # title is marked translatable, therefore only this instance is updated. |
| 141 | # these are two queries, but they may be reduced to one when changes on fields are tracked after instance creation |
| 142 | # and only one kind of fields (translatable xor translatable) are changed |
| 143 | |
| 144 | # Deletes |
| 145 | blog_entry.delete() # should only delete this language |
| 146 | blog_entry.delete(all_languages=True) # deletes all translated objects all languages |
| 147 | |
| 148 | # Fallbacks |
| 149 | # When a language is requested for which an object does not exist |
| 150 | try: |
| 151 | blog_entry = BlogEntry.objects.get(group_id=UUID) # object does not exist in current language |
| 152 | except BlogEntry.DoesNotExistInLanguage as e: # a subclass of BlogEntry.DoesNotExist |
| 153 | e.available_languages # list of available languages or may even contain the respective objects |
| 154 | # List can be shown to user as an option to view object in other language |
| 155 | |
| 156 | }}} |
| 157 | |
| 158 | ==== Things to note ==== |
| 159 | * any time 'current language' is used it means django.utils.translation.get_language() which uses thread locals |
| 160 | |
| 161 | ==== Important Tickets ==== |
| 162 | * [http://code.djangoproject.com/ticket/4102 #4102] |
| 163 | * many more |
86 | | * as GET request (is sometimes requestet by customers) |
87 | | * as POST (is bad for searchengines) |
88 | | * language databasetable similar to contenttypes databasetable |
89 | | |
| 188 | * as query string ?lang=en |
| 189 | * as cookie (generally bad) |
| 190 | * language database table similar to ContentTypes database table? |
| 191 | |
| 192 | |
| 193 | == Approach overview == |
| 194 | A brief overview of existing approaches with implementing projects. |
| 195 | |
| 196 | * property column is holding all translations (in a map) |
| 197 | * [http://code.google.com/p/transdb/ transdb] |
| 198 | |
| 199 | * column per language for every translated property |
| 200 | * [http://code.google.com/p/django-modeltranslation/ django-modeltranslation] |
| 201 | * [http://code.google.com/p/django-transmeta/ django-transmeta] |
| 202 | |
| 203 | * big table holding all translations |
| 204 | * [http://code.google.com/p/django-utils/wiki/TranslationService django-utils] |
| 205 | |
| 206 | * table per property holding all languages of this property |
| 207 | |
| 208 | * table for not internationalized properties and an extratable for the localized version |
| 209 | * [http://code.google.com/p/django-multilingual/ django-multilingual] |
| 210 | * language is saved as integer, which doesn't work for multiple site projects where language ids in setting can differ |
| 211 | * [http://code.google.com/p/django-multilingual-model/ django-multilingual-model] |
| 212 | * [http://github.com/foxbunny/django-i18n-model/ django-i18n-model] |
| 213 | |
| 214 | * object per language, the association between objects is provided by a goup-id (an update of the not internationalized fields would overwrite this fields in all other language objects) |
| 215 | * [http://blog.rafaljonca.org/2009/03/third-style-of-django-multilingual-data.html a code example] |
| 216 | * normal model with one language, additional table with all i18n fields in other languages |
| 217 | * [http://code.google.com/p/django-pluggable-model-i18n/ django-pluggable-model-i18n] (status: experimental) |
| 218 | * [http://github.com/ojii/django-multilingual-ng] |