Coverage for app/venv/lib/python3.14/site-packages/weblate/addons/cdn.py: 50%
60 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
1# Copyright © Michal Čihař <michal@weblate.org>
2#
3# SPDX-License-Identifier: GPL-3.0-or-later
5from __future__ import annotations
7import json
8import os
9from typing import TYPE_CHECKING, ClassVar
10from uuid import uuid4
12from django.conf import settings
13from django.template.loader import render_to_string
14from django.utils.functional import cached_property
15from django.utils.translation import gettext_lazy
17from weblate.addons.base import BaseAddon
18from weblate.addons.events import AddonEvent
19from weblate.addons.forms import CDNJSForm
20from weblate.addons.tasks import cdn_parse_html
21from weblate.utils.state import STATE_TRANSLATED
23if TYPE_CHECKING: 23 ↛ 24line 23 didn't jump to line 24 because the condition on line 23 was never true
24 from weblate.addons.models import Addon
25 from weblate.auth.models import User
26 from weblate.trans.models import Component, Project
29class CDNJSAddon(BaseAddon):
30 events: ClassVar[set[AddonEvent]] = {
31 AddonEvent.EVENT_DAILY,
32 AddonEvent.EVENT_POST_COMMIT,
33 AddonEvent.EVENT_POST_UPDATE,
34 }
35 name = "weblate.cdn.cdnjs"
36 verbose = gettext_lazy("JavaScript localization CDN")
37 description = gettext_lazy(
38 "Publishes translations into content delivery network "
39 "for use in JavaScript or HTML localization."
40 )
41 user_name = "cdn"
42 user_verbose = "CDN add-on"
44 settings_form = CDNJSForm
45 icon = "cloud-upload.svg"
46 stay_on_create = True
48 @classmethod
49 def create_object(
50 cls,
51 *,
52 component: Component | None = None,
53 project: Project | None = None,
54 acting_user: User | None = None,
55 **kwargs,
56 ) -> Addon:
57 # Generate UUID for the CDN
58 if "state" not in kwargs:
59 kwargs["state"] = {"uuid": uuid4().hex}
60 return super().create_object(
61 component=component, project=project, acting_user=acting_user, **kwargs
62 )
64 @classmethod
65 def can_install(cls, component: Component, user: User | None) -> bool:
66 if (
67 not settings.LOCALIZE_CDN_URL
68 or not settings.LOCALIZE_CDN_PATH
69 or not component.has_template()
70 or not component.translation_set.exists()
71 ):
72 return False
73 return super().can_install(component, user)
75 def cdn_path(self, filename: str) -> str:
76 return os.path.join(
77 settings.LOCALIZE_CDN_PATH, self.instance.state["uuid"], filename
78 )
80 @cached_property
81 def cdn_js_url(self) -> str:
82 return os.path.join(
83 settings.LOCALIZE_CDN_URL, self.instance.state["uuid"], "weblate.js"
84 )
86 def post_commit(
87 self, component: Component, store_hash: bool, activity_log_id: int | None = None
88 ) -> None:
89 # Get list of applicable translations
90 threshold = self.instance.configuration["threshold"]
91 translations = [
92 translation
93 for translation in component.translation_set.all()
94 if (not translation.is_source or component.intermediate)
95 and translation.stats.translated > threshold
96 ]
98 # Create output directory
99 dirname = self.cdn_path("")
100 if not os.path.exists(dirname):
101 os.makedirs(dirname)
103 # Generate JavasScript loader
104 with open(self.cdn_path("weblate.js"), "w") as handle:
105 # The languages variable is included inside quotes to make
106 # sure the template is valid JavaScript code as well
107 handle.write(
108 render_to_string(
109 "addons/js/weblate.js.template",
110 {
111 "languages": sorted(
112 translation.language.code for translation in translations
113 ),
114 "url": os.path.join(
115 settings.LOCALIZE_CDN_URL,
116 self.instance.state["uuid"],
117 ),
118 "cookie_name": self.instance.configuration["cookie_name"],
119 "css_selector": self.instance.configuration["css_selector"],
120 },
121 )
122 )
124 # Generate bilingual JSON files
125 for translation in translations:
126 with open(
127 self.cdn_path(f"{translation.language.code}.json"), "w"
128 ) as handle:
129 json.dump(
130 {
131 unit.source: unit.target
132 for unit in translation.unit_set.filter(
133 state__gte=STATE_TRANSLATED
134 )
135 },
136 handle,
137 )
139 def daily(self, component: Component, activity_log_id: int | None = None) -> None:
140 if not self.instance.configuration["files"].strip():
141 return
142 # Trigger parsing files
143 cdn_parse_html.delay(self.instance.id, component.id)
145 def post_update(
146 self,
147 component: Component,
148 previous_head: str,
149 skip_push: bool,
150 activity_log_id: int | None = None,
151 ) -> None:
152 self.daily(component)