Commit 3fc49958 authored by Alain St-Denis's avatar Alain St-Denis
Browse files

WIP: Fine tuning the translation process

parent 03c91863
...@@ -2,35 +2,40 @@ Requirements ...@@ -2,35 +2,40 @@ Requirements
------------ ------------
pybabel (already installed) pybabel (already installed)
pip install django-babel (TODO: document my fix to extract.py) pip install django-babel
pip install babel-vue-extractor
Create the initial pot file Patch django-babel with (assuming you are using virtualenv):
--------------------------- ```
patch -p1 ~/.virtualenvs/socialhome/lib/python3.9/site-packages/django_babel/extract < ~/socialhome/translate/django_babel.patch
```
TODO: create an issue for this
Workflow
--------
From project directory, From project directory,
Create babel.cfg with the following content: Create or update the pot file using:
``` ```
[django: templates/**.*] PYTHONPATH=. pybabel extract -F babel.cfg -o socialhome/locale/django.pot .
[django: socialhome/*/templates/**.*]
[python: socialhome/**.py]
[babelvueextractor.extract.extract_vue: socialhome/**.vue]
[javascript: socialhome/frontend/*.js]
[javascript: socialhome/frontend/src/**.js]
``` ```
Extract initial pot file using: Translations are hosted at https://hosted.weblate.org/projects/socialhome. During the initial setup, weblate will pull the django.pot file.
Then new translations can be started using weblate's UI. The pattern to configure for po files is socialhome/locale/*/LC_MESSAGES/django.po.
weblate can be configure to automatically compile mo files, but until we figure out how it works, use the following:
``` ```
pybabel extract -F babel.cfg -o socialhome/locale/django.pot . django-admin compilemessages
``` ```
Saving and archiving from the weblate project will push the updated po files to socialhome's repository.
TODO If changes to translation files (pot and po) are made, they should be pushed to the repo and then forced synced from weblate's UI.
Hack
---- ----
Weblate setup. ~/socialhome/translate/extract.py is a wrapper around pybabel's javascript extractor to deal with template literal placehlders and filters.
Compile translations TODO: create an issue for this.
--------------------
django-admin compilemessages
[extractors]
extrajs = translate.extract:extract_extrajs
[django: socialhome/templates/**.*] [django: socialhome/templates/**.*]
[django: socialhome/*/templates/**.*] [django: socialhome/*/templates/**.*]
[python: socialhome/**.py] [python: socialhome/**.py]
[babelvueextractor.extract.extract_vue: socialhome/**.vue] #[javascript: socialhome/static/**.js]
[javascript: socialhome/frontend/*.js] [extrajs: socialhome/static/**.js]
[javascript: socialhome/frontend/src/**.js]
import Vue from "vue"
import StreamElement from "@/components/streams/StreamElement.vue"
import PublicStampedElement from "@/components/streams/stamped_elements/PublicStampedElement.vue"
import FollowedStampedElement from "@/components/streams/stamped_elements/FollowedStampedElement.vue"
import LimitedStampedElement from "@/components/streams/stamped_elements/LimitedStampedElement.vue"
import LocalStampedElement from "@/components/streams/stamped_elements/LocalStampedElement.vue"
import TagStampedElement from "@/components/streams/stamped_elements/TagStampedElement.vue"
import TagsStampedElement from "@/components/streams/stamped_elements/TagsStampedElement.vue"
import ProfileStampedElement from "@/components/streams/stamped_elements/ProfileStampedElement.vue"
import LoadingElement from "@/components/common/LoadingElement.vue"
import ProfileStreamButtons from "@/components/streams/stamped_elements/ProfileStreamButtons"
export default Vue.component("Stream", {
components: {
FollowedStampedElement,
LimitedStampedElement,
LoadingElement,
LocalStampedElement,
ProfileStampedElement,
ProfileStreamButtons,
PublicStampedElement,
StreamElement,
TagStampedElement,
TagsStampedElement,
},
// TODO: Seperate Stream.vue into TagStream.vue, GuidProfile.vue and UsernameProfile.vue, etc. in the future
props: {
contentId: {
type: String, default: "",
},
uuid: {
type: String, default: "",
},
user: {
type: String, default: "",
},
tag: {
type: String, default: "",
},
},
data() {
return {
masonryOptions: {
"item-selector": ".grid-item",
"column-width": ".grid-sizer",
gutter: ".gutter-sizer",
"percent-position": true,
"transition-duration": "0s",
stagger: 0,
},
}
},
computed: {
singleContent() {
if (!this.$store.state.stream.singleContentId) {
return null
}
return this.$store.state.stream.contents[this.$store.state.stream.singleContentId]
},
showProfileStreamButtons() {
return this.streamName === "profile_all" || this.streamName === "profile_pinned"
},
stampedElement() {
switch (this.streamName) {
case "followed":
return "FollowedStampedElement"
case "limited":
return "LimitedStampedElement"
case "local":
return "LocalStampedElement"
case "public":
return "PublicStampedElement"
case "tag":
return "TagStampedElement"
case "tags":
return "TagsStampedElement"
case "profile_all":
case "profile_pinned":
return "ProfileStampedElement"
default:
// eslint-disable-next-line no-console
console.error(`Unsupported stream name ${this.streamName}`)
return ""
}
},
streamName() {
return this.$store.state.stream.stream.name
},
translations() {
const ln = this.unfetchedContentIds.length
s = ngettext(`${ln} new post available`, `${ln} new posts available`, ln)
return {newPostsAvailables: s}
},
unfetchedContentIds() {
return this.$store.state.stream.unfetchedContentIds
},
},
beforeMount() {
if (!this.$store.state.stream.stream.single) {
this.loadStream()
}
},
methods: {
onNewContentClick() {
this.$store.dispatch("stream/newContentAck").then(
() => this.$nextTick( // Wait for new content to be rendered
() => this.$scrollTo("body"),
),
)
},
loadStream() {
const options = {params: {}}
const lastContentId = this.$store.state.stream.currentContentIds[
this.$store.state.stream.currentContentIds.length - 1
]
if (lastContentId && this.$store.state.stream.contents[lastContentId]) {
options.params.lastId = this.$store.state.stream.contents[lastContentId].through
}
switch (this.$store.state.stream.stream.name) {
case "followed":
this.$store.dispatch("stream/getFollowedStream", options)
break
case "limited":
this.$store.dispatch("stream/getLimitedStream", options)
break
case "local":
this.$store.dispatch("stream/getLocalStream", options)
break
case "public":
this.$store.dispatch("stream/getPublicStream", options)
break
case "tag":
options.params.name = this.tag
this.$store.dispatch("stream/getTagStream", options)
break
case "tags":
this.$store.dispatch("stream/getTagsStream", options)
break
case "profile_all":
options.params.uuid = this.$store.state.application.profile.uuid
this.$store.dispatch("stream/getProfileAll", options)
break
case "profile_pinned":
options.params.uuid = this.$store.state.application.profile.uuid
this.$store.dispatch("stream/getProfilePinned", options)
break
default:
break
}
},
},
})
This diff is collapsed.
This diff is collapsed.
*** extract.py.orig 2021-05-21 17:38:03.719644894 +0000
--- extract.py 2021-05-11 16:49:10.059785399 +0000
***************
*** 1,5 ****
# -*- coding: utf-8 -*-
! from django.template.base import Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK
from django.utils.translation import trim_whitespace
from django.utils.encoding import smart_text
--- 1,5 ----
# -*- coding: utf-8 -*-
! from django.template.base import Lexer, TokenType
from django.utils.translation import trim_whitespace
from django.utils.encoding import smart_text
***************
*** 59,65 ****
for t in text_lexer.tokenize():
lineno += t.contents.count('\n')
if intrans:
! if t.token_type == TOKEN_BLOCK:
endbmatch = endblock_re.match(t.contents)
pluralmatch = plural_re.match(t.contents)
if endbmatch:
--- 59,65 ----
for t in text_lexer.tokenize():
lineno += t.contents.count('\n')
if intrans:
! if t.token_type == TokenType.BLOCK:
endbmatch = endblock_re.match(t.contents)
pluralmatch = plural_re.match(t.contents)
if endbmatch:
***************
*** 106,123 ****
else:
raise SyntaxError('Translation blocks must not include '
'other block tags: %s' % t.contents)
! elif t.token_type == TOKEN_VAR:
if inplural:
plural.append('%%(%s)s' % t.contents)
else:
singular.append('%%(%s)s' % t.contents)
! elif t.token_type == TOKEN_TEXT:
if inplural:
plural.append(t.contents)
else:
singular.append(t.contents)
else:
! if t.token_type == TOKEN_BLOCK:
imatch = inline_re.match(t.contents)
bmatch = block_re.match(t.contents)
cmatches = constant_re.findall(t.contents)
--- 106,123 ----
else:
raise SyntaxError('Translation blocks must not include '
'other block tags: %s' % t.contents)
! elif t.token_type == TokenType.VAR:
if inplural:
plural.append('%%(%s)s' % t.contents)
else:
singular.append('%%(%s)s' % t.contents)
! elif t.token_type == TokenType.TEXT:
if inplural:
plural.append(t.contents)
else:
singular.append(t.contents)
else:
! if t.token_type == TokenType.BLOCK:
imatch = inline_re.match(t.contents)
bmatch = block_re.match(t.contents)
cmatches = constant_re.findall(t.contents)
***************
*** 152,158 ****
for cmatch in cmatches:
stripped_cmatch = strip_quotes(cmatch)
yield lineno, None, smart_text(stripped_cmatch), []
! elif t.token_type == TOKEN_VAR:
parts = t.contents.split('|')
cmatch = constant_re.match(parts[0])
if cmatch:
--- 152,158 ----
for cmatch in cmatches:
stripped_cmatch = strip_quotes(cmatch)
yield lineno, None, smart_text(stripped_cmatch), []
! elif t.token_type == TokenType.VAR:
parts = t.contents.split('|')
cmatch = constant_re.match(parts[0])
if cmatch:
***************
*** 167,170 ****
p1 = p1.strip('()')
p1 = strip_quotes(p1)
yield lineno, None, smart_text(p1), []
-
--- 167,169 ----
import re
import functools
from io import BytesIO
from babel.messages.extract import extract_javascript
from babel.messages.extract import DEFAULT_KEYWORDS
del(DEFAULT_KEYWORDS['_'])
del(DEFAULT_KEYWORDS['N_'])
def extract_extrajs(fileobj, keywords, comment_tags, options):
"""Extract template literal placeholders and filters from Javascript files.
:param fileobj: the file-like the messages should be extracted from
:param keywords: a list of keywords (i.e. function names) that should be recognize as translation functions
:param comment_tags: a list of translator tags to search for and include in the results
:param options: a dictionary of additional options (optional)
:return: an iterator over ``(lineno, funcname, message, comments)``
:rtype: ``iterator``
"""
encoding = options.get('encoding', 'utf-8')
c = fileobj.read().decode(encoding=encoding)
filtpat = re.compile('t\._f\("gettext"\)', re.UNICODE)
c = filtpat.sub('gettext', c)
# tickpat = re.compile(r'\x60(.*?)\x60', re.UNICODE)
# res = tickpat.findall(contents)
# comppat = re.compile(r'\$\{(.*?)\}', re.UNICODE)
# for s in res:
# r = comppat.findall(s)
# if r: txt = txt + '\n'.join(r)
# options['template_string'] = False
comppat = re.compile(r'(\$\{gettext\((.*?)\)\})', re.UNICODE)
c = comppat.sub(r'`+gettext(\g<2>)+`', c, re.UNICODE)
for i in extract_javascript(
BytesIO(c.encode(encoding=encoding)),
DEFAULT_KEYWORDS.keys(),
comment_tags,
options):
if i:
yield (i[0], i[1], i[2], i[3])
if __name__ == '__main__':
import sys
print(sys.argv[1])
with open(sys.argv[1], 'rb') as f:
for i in extract_extrajs(f, ['gettext'], [], {}):
print(i)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment