dd-code nl-38 ol-38"> 38
+msgid "Billing"
39
+msgstr "การเรียกเก็บเงิน"
40
+
41
+#: admin_frontend/templates/admin_frontend/base.html:177
42
+msgid "Recycle Ops"
43
+msgstr "การดำเนินงานรีไซเคิล"
44
+
45
+#: public_frontend/templates/public_frontend/base.html:17
46
+#: public_frontend/templates/public_frontend/materials_list.html:3
47
+#: translations/menu_labels.py:18
48
+msgid "Materials"
49
+msgstr "วัสดุ"
50
+
51
+#: public_frontend/templates/public_frontend/base.html:18
52
+#: public_frontend/templates/public_frontend/listings_list.html:3
53
+msgid "Listings"
54
+msgstr "รายการ"
55
+
56
+#: public_frontend/templates/public_frontend/base.html:19
57
+#: public_frontend/templates/public_frontend/pickup_request.html:3
58
+msgid "Request Pickup"
59
+msgstr "ขอรับของ"
60
+
61
+#: public_frontend/templates/public_frontend/base.html:20
62
+#: public_frontend/templates/public_frontend/blog_list.html:3
63
+msgid "Blog"
64
+msgstr "บล็อก"
65
+
66
+#: public_frontend/templates/public_frontend/base.html:21
67
+#: public_frontend/templates/public_frontend/contact.html:3
68
+msgid "Contact"
69
+msgstr "ติดต่อเรา"
70
+
71
+#: public_frontend/templates/public_frontend/base.html:53
72
+msgid "Staff Login"
73
+msgstr "เข้าสู่ระบบเจ้าหน้าที่"
74
+
75
+#: public_frontend/templates/public_frontend/blog_list.html:14
76
+msgid "No image"
77
+msgstr "ไม่มีรูปภาพ"
78
+
79
+#: public_frontend/templates/public_frontend/blog_list.html:24
80
+#: public_frontend/templates/public_frontend/home.html:92
81
+msgid "No posts yet."
82
+msgstr "ยังไม่มีโพสต์"
83
+
84
+#: public_frontend/templates/public_frontend/contact.html:5
85
+#: public_frontend/templates/public_frontend/home.html:99
86
+msgid "Contact Us"
87
+msgstr "ติดต่อเรา"
88
+
89
+#: public_frontend/templates/public_frontend/contact.html:10
90
+#: public_frontend/templates/public_frontend/home.html:104
91
+#: public_frontend/templates/public_frontend/materials_list.html:9
92
+#: public_frontend/templates/public_frontend/pickup_request.html:38
93
+msgid "Name"
94
+msgstr "ชื่อ"
95
+
96
+#: public_frontend/templates/public_frontend/contact.html:14
97
+#: public_frontend/templates/public_frontend/home.html:108
98
+#: public_frontend/templates/public_frontend/pickup_request.html:40
99
+msgid "Email"
100
+msgstr "อีเมล"
101
+
102
+#: public_frontend/templates/public_frontend/contact.html:20
103
+#: public_frontend/templates/public_frontend/pickup_request.html:41
104
+msgid "Phone"
105
+msgstr "โทรศัพท์"
106
+
107
+#: public_frontend/templates/public_frontend/contact.html:24
108
+#: public_frontend/templates/public_frontend/home.html:113
109
+msgid "Subject"
110
+msgstr "หัวข้อ"
111
+
112
+#: public_frontend/templates/public_frontend/contact.html:29
113
+#: public_frontend/templates/public_frontend/home.html:117
114
+msgid "Message"
115
+msgstr "ข้อความ"
116
+
117
+#: public_frontend/templates/public_frontend/contact.html:33
118
+msgid "Send"
119
+msgstr "ส่ง"
120
+
121
+#: public_frontend/templates/public_frontend/home.html:3
122
+msgid "Home"
123
+msgstr "หน้าแรก"
124
+
125
+#: public_frontend/templates/public_frontend/home.html:9
126
+msgid "Streamline Your Factory's Recycling"
127
+msgstr "ปรับปรุงการรีไซเคิลของโรงงานให้มีประสิทธิภาพ"
128
+
129
+#: public_frontend/templates/public_frontend/home.html:10
130
+msgid ""
131
+"We partner with businesses like yours to manage scrap materials efficiently. "
132
+"Browse listings, request a pickup, and turn your waste into a resource."
133
+msgstr "เราร่วมมือกับธุรกิจเช่นคุณเพื่อจัดการเศษวัสดุอย่างมีประสิทธิภาพ เรียกดูรายการ ขอรับของ และเปลี่ยนของเสียให้เป็นทรัพยากร"
134
+
135
+#: public_frontend/templates/public_frontend/home.html:12
136
+#: public_frontend/templates/public_frontend/pickup_request.html:6
137
+msgid "Request a Pickup"
138
+msgstr "ขอรับของ"
139
+
140
+#: public_frontend/templates/public_frontend/home.html:13
141
+msgid "Browse Listings"
142
+msgstr "ดูรายการ"
143
+
144
+#: public_frontend/templates/public_frontend/home.html:21
145
+msgid "Our Services"
146
+msgstr "บริการของเรา"
147
+
148
+#: public_frontend/templates/public_frontend/home.html:40
149
+msgid "Materials We Accept"
150
+msgstr "วัสดุที่รับ"
151
+
152
+#: public_frontend/templates/public_frontend/home.html:41
153
+#: public_frontend/templates/public_frontend/home.html:82
154
+msgid "View all"
155
+msgstr "ดูทั้งหมด"
156
+
157
+#: public_frontend/templates/public_frontend/home.html:51
158
+#: public_frontend/templates/public_frontend/materials_list.html:22
159
+msgid "No materials published."
160
+msgstr "ยังไม่มีวัสดุเผยแพร่"
161
+
162
+#: public_frontend/templates/public_frontend/home.html:60
163
+msgid "Open Listings"
164
+msgstr "รายการที่เปิดอยู่"
165
+
166
+#: public_frontend/templates/public_frontend/home.html:61
167
+msgid "Browse all"
168
+msgstr "ดูทั้งหมด"
169
+
170
+#: public_frontend/templates/public_frontend/home.html:66
171
+#: public_frontend/templates/public_frontend/listing_detail.html:7
172
+#: public_frontend/templates/public_frontend/listings_list.html:8
173
+msgid "Ends"
174
+msgstr "สิ้นสุด"
175
+
176
+#: public_frontend/templates/public_frontend/home.html:71
177
+msgid "No public listings at the moment."
178
+msgstr "ขณะนี้ยังไม่มีรายการสาธารณะ"
179
+
180
+#: public_frontend/templates/public_frontend/home.html:81
181
+msgid "Industry Insights"
182
+msgstr "ข้อมูลเชิงลึกอุตสาหกรรม"
183
+
184
+#: public_frontend/templates/public_frontend/home.html:121
185
+msgid "Send Message"
186
+msgstr "ส่งข้อความ"
187
+
188
+#: public_frontend/templates/public_frontend/listing_detail.html:3
189
+msgid "Listing"
190
+msgstr "รายการ"
191
+
192
+#: public_frontend/templates/public_frontend/listing_detail.html:7
193
+msgid "Status"
194
+msgstr "สถานะ"
195
+
196
+#: public_frontend/templates/public_frontend/listing_detail.html:9
197
+msgid "Items"
198
+msgstr "รายการ"
199
+
200
+#: public_frontend/templates/public_frontend/listing_detail.html:14
201
+msgid "No items listed"
202
+msgstr "ยังไม่มีรายการ"
203
+
204
+#: public_frontend/templates/public_frontend/listings_list.html:13
205
+msgid "No public listings."
206
+msgstr "ยังไม่มีรายการสาธารณะ"
207
+
208
+#: public_frontend/templates/public_frontend/materials_list.html:10
209
+msgid "Category"
210
+msgstr "หมวดหมู่"
211
+
212
+#: public_frontend/templates/public_frontend/materials_list.html:11
213
+msgid "Default Unit"
214
+msgstr "หน่วยเริ่มต้น"
215
+
216
+#: public_frontend/templates/public_frontend/pickup_request.html:36
217
+msgid "Review Your Pickup Request"
218
+msgstr "ตรวจสอบคำขอรับของของคุณ"
219
+
220
+#: public_frontend/templates/public_frontend/pickup_request.html:43
221
+msgid "Pickup Address"
222
+msgstr "ที่อยู่รับของ"
223
+
224
+#: public_frontend/templates/public_frontend/pickup_request.html:45
225
+msgid "Preferred Date/Time"
226
+msgstr "วันที่/เวลา ที่ต้องการ"
227
+
228
+#: public_frontend/templates/public_frontend/pickup_request.html:47
229
+msgid "Materials / Notes"
230
+msgstr "วัสดุ / หมายเหตุ"
231
+
232
+#: public_frontend/templates/public_frontend/pickup_request.html:49
233
+msgid "Photos"
234
+msgstr "รูปภาพ"
235
+
236
+#: public_frontend/templates/public_frontend/pickup_request.html:51
237
+msgid "No files attached."
238
+msgstr "ไม่มีไฟล์แนบ"
239
+
240
+#: public_frontend/templates/public_frontend/pickup_request.html:57
241
+msgid "Edit"
242
+msgstr "แก้ไข"
243
+
244
+#: public_frontend/templates/public_frontend/pickup_request.html:58
245
+msgid "Confirm & Send"
246
+msgstr "ยืนยันและส่ง"
247
+
248
+#: public_frontend/templates/public_frontend/pickup_request.html:62
249
+msgid "close"
250
+msgstr "ปิด"
251
+
252
+#: public_frontend/templates/public_frontend/service_detail.html:29
253
+msgid "Back to Home"
254
+msgstr "กลับสู่หน้าแรก"
255
+
256
+#: translations/menu_labels.py:4
257
+msgid "Dashboard"
258
+msgstr "แดชบอร์ด"
259
+
260
+#: translations/menu_labels.py:5
261
+msgid "Profiles"
262
+msgstr "โปรไฟล์"
263
+
264
+#: translations/menu_labels.py:6
265
+msgid "Opportunities"
266
+msgstr "โอกาส"
267
+
268
+#: translations/menu_labels.py:7
269
+msgid "Intro Requests"
270
+msgstr "คำขอแนะนำ"
271
+
272
+#: translations/menu_labels.py:8
273
+msgid "Leaderboard"
274
+msgstr "ตารางจัดอันดับ"
275
+
276
+#: translations/menu_labels.py:9
277
+msgid "Leads"
278
+msgstr "ลีด"
279
+
280
+#: translations/menu_labels.py:10
281
+msgid "CMS Posts"
282
+msgstr "โพสต์ CMS"
283
+
284
+#: translations/menu_labels.py:11
285
+msgid "CMS Categories"
286
+msgstr "หมวดหมู่ CMS"
287
+
288
+#: translations/menu_labels.py:13
289
+msgid "Advanced Settings"
290
+msgstr "การตั้งค่าขั้นสูง"
291
+
292
+#: translations/menu_labels.py:14
293
+msgid "Organization Settings"
294
+msgstr "การตั้งค่าองค์กร"
295
+
296
+#: translations/menu_labels.py:15
297
+msgid "Organization Sites"
298
+msgstr "ไซต์ขององค์กร"
299
+
300
+#: translations/menu_labels.py:16
301
+msgid "Permissions"
302
+msgstr "สิทธิ์การใช้งาน"
303
+
304
+#: translations/menu_labels.py:17
305
+msgid "Users"
306
+msgstr "ผู้ใช้"
307
+
308
+#: translations/menu_labels.py:19
309
+msgid "Customers"
310
+msgstr "ลูกค้า"
311
+
312
+#: translations/menu_labels.py:20
313
+msgid "Pickups"
314
+msgstr "การรับของ"
315
+
316
+#: translations/menu_labels.py:21
317
+msgid "Weigh Tickets"
318
+msgstr "ใบชั่งน้ำหนัก"
319
+
320
+#: translations/menu_labels.py:22
321
+msgid "Documents"
322
+msgstr "เอกสาร"
323
+
324
+#: translations/menu_labels.py:23
325
+msgid "Provided Services"
326
+msgstr "บริการที่ให้"
327
+
328
+#: translations/menu_labels.py:24
329
+msgid "Scrap Listings"
330
+msgstr "รายการเศษวัสดุ"
331
+
332
+#: translations/menu_labels.py:25
333
+msgid "Invoices"
334
+msgstr "ใบแจ้งหนี้"
335
+
336
+#: translations/menu_labels.py:26
337
+msgid "Payouts"
338
+msgstr "การจ่ายเงิน"
339
+
340
+#: recycle_core/models.py
341
+msgid "Pickup & Logistics"
342
+msgstr "การรับของและโลจิสติกส์"
343
+
344
+#: recycle_core/models.py
345
+msgid "Material Sorting"
346
+msgstr "การคัดแยกวัสดุ"
347
+
348
+#: recycle_core/models.py
349
+msgid "Weighing & Ticketing"
350
+msgstr "การชั่งน้ำหนักและออกบัตรชั่ง"
351
+
352
+#: recycle_core/models.py
353
+msgid "Invoicing & Payouts"
354
+msgstr "การวางบิลและการจ่ายเงิน"
355
+
356
+#: recycle_core/models.py
357
+msgid "Reporting & Analytics"
358
+msgstr "การรายงานและการวิเคราะห์"
359
+
360
+#: recycle_core/models.py
361
+msgid "Marketplace & Bidding"
362
+msgstr "มาร์เก็ตเพลสและการประมูล"
363
+
364
+#: recycle_core/models.py
365
+msgid "Compliance & Audits"
366
+msgstr "การปฏิบัติตามและการตรวจสอบ"
367
+
368
+#: recycle_core/models.py
369
+msgid "Consulting & Training"
370
+msgstr "ที่ปรึกษาและการฝึกอบรม"

+ 18 - 0
orgs/apps.py

@@ -14,4 +14,22 @@ class OrgsConfig(AppConfig):
14 14
         except Exception:
15 15
             # Avoid startup hard-fail if migrations/apps not ready; re-raise only in debug if needed
16 16
             raise
17
+
18
+        # Monkey-patch User.__str__ to show "<username>, <fullname>"
19
+        # This affects labels in forms and general stringification.
20
+        try:
21
+            from django.contrib.auth import get_user_model
22
+
23
+            User = get_user_model()
24
+
25
+            def _user_str(self):  # type: ignore[override]
26
+                full = self.get_full_name().strip()
27
+                return f"{self.username}, {full}" if full else f"{self.username}"
28
+
29
+            # Assign only if not already customized
30
+            if getattr(User.__str__, "__name__", None) != "_user_str":  # type: ignore[attr-defined]
31
+                User.__str__ = _user_str  # type: ignore[assignment]
32
+        except Exception:
33
+            # Do not block app startup if something goes wrong
34
+            pass
17 35
         return super().ready()

+ 2 - 1
orgs/views_admin.py

@@ -165,7 +165,8 @@ def permissions_edit_group(request, pk: int):
165 165
         final_ids = [pid for pid in sel_ids if pid in allowed_ids]
166 166
         group.permissions.set(Permission.objects.filter(id__in=final_ids))
167 167
         messages.success(request, f"Permissions updated for group '{group.name}'.")
168
-        return redirect("orgs_admin:permissions_overview")
168
+        # Redirect back to the current URL (stay on the edit page)
169
+        return redirect(request.get_full_path())
169 170
 
170 171
     current_ids = set(group.permissions.values_list("id", flat=True))
171 172
 

+ 22 - 8
public_frontend/templates/public_frontend/base.html

@@ -1,6 +1,7 @@
1
-{% load tailwind_tags static %}
1
+{% load tailwind_tags static i18n %}
2 2
 <!DOCTYPE html>
3
-<html lang="en">
3
+{% get_current_language as LANGUAGE_CODE %}
4
+<html lang="{{ LANGUAGE_CODE }}">
4 5
 <head>
5 6
   <meta charset="utf-8" />
6 7
   <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -13,12 +14,25 @@
13 14
     <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 flex items-center justify-between h-16">
14 15
       <a href="/" class="text-lg font-semibold">{% if current_site %}{{ current_site.name }}{% else %}Ecoloop{% endif %}</a>
15 16
       <nav class="hidden md:flex items-center gap-6">
16
-        <a href="/#materials" class="hover:text-black/70">Materials</a>
17
-        <a href="/#listings" class="hover:text-black/70">Listings</a>
18
-        <a href="/#pickup-request" class="hover:text-black/70">Request Pickup</a>
19
-        <a href="/#blog" class="hover:text-black/70">Blog</a>
20
-        <a href="/#contact" class="hover:text-black/70">Contact</a>
17
+        <a href="/#materials" class="hover:text-black/70">{% trans "Materials" %}</a>
18
+        <a href="/#listings" class="hover:text-black/70">{% trans "Listings" %}</a>
19
+        <a href="/#pickup-request" class="hover:text-black/70">{% trans "Request Pickup" %}</a>
20
+        <a href="/#blog" class="hover:text-black/70">{% trans "Blog" %}</a>
21
+        <a href="/#contact" class="hover:text-black/70">{% trans "Contact" %}</a>
21 22
       </nav>
23
+      <form method="post" action="{% url 'set_language' %}" class="hidden md:block ml-4">
24
+        {% csrf_token %}
25
+        <input type="hidden" name="next" value="{{ request.get_full_path }}" />
26
+        {% get_available_languages as langs %}
27
+        {% get_language_info_list for langs as languages %}
28
+        <label for="lang" class="sr-only">Language</label>
29
+        <select id="lang" name="language" class="text-sm border border-gray-300 rounded px-2 py-1 bg-white"
30
+                onchange="this.form.submit()">
31
+          {% for lang in languages %}
32
+            <option value="{{ lang.code }}" {% if lang.code == LANGUAGE_CODE %}selected{% endif %}>{{ lang.name_local }}</option>
33
+          {% endfor %}
34
+        </select>
35
+      </form>
22 36
     </div>
23 37
   </header>
24 38
   <main class="mx-auto max-w-7xl p-4 sm:p-6 lg:p-8 min-h-[80vh]">
@@ -36,7 +50,7 @@
36 50
   <footer class="bg-white border-t">
37 51
     <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between text-sm text-gray-600">
38 52
       <span>&copy; {% now "Y" %} {% if current_site %}{{ current_site.name }}{% else %}Ecoloop{% endif %}</span>
39
-      <a href="/webadmin/" class="hover:underline">Staff Login</a>
53
+      <a href="/webadmin/" class="hover:underline">{% trans "Staff Login" %}</a>
40 54
     </div>
41 55
   </footer>
42 56
 </body>

+ 1 - 0
public_frontend/templates/public_frontend/blog_detail.html

@@ -1,4 +1,5 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
+{% load i18n %}
2 3
 {% block title %}{{ post.title }}{% endblock %}
3 4
 {% block content %}
4 5
 <article class="blog-article max-w-none bg-white rounded shadow p-6">

+ 4 - 3
public_frontend/templates/public_frontend/blog_list.html

@@ -1,5 +1,6 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Blog{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Blog" %}{% endblock %}
3 4
 {% block content %}
4 5
 <div class="grid gap-4">
5 6
   {% for p in posts %}
@@ -10,7 +11,7 @@
10 11
             <img src="{{ p.feature_image.url }}" alt="{{ p.title }}" loading="lazy">
11 12
           </div>
12 13
         {% else %}
13
-          <div class="thumb placeholder">No image</div>
14
+          <div class="thumb placeholder">{% trans "No image" %}</div>
14 15
         {% endif %}
15 16
         <div class="min-w-0">
16 17
           <div class="text-sm text-gray-500">{{ p.published_at|date:'M j, Y' }}</div>
@@ -20,7 +21,7 @@
20 21
       </div>
21 22
     </a>
22 23
   {% empty %}
23
-    <div class="text-gray-500">No posts yet.</div>
24
+    <div class="text-gray-500">{% trans "No posts yet." %}</div>
24 25
   {% endfor %}
25 26
   </div>
26 27
 <div class="mt-4">{% include 'admin_frontend/_pagination.html' %}</div>

+ 9 - 8
public_frontend/templates/public_frontend/contact.html

@@ -1,35 +1,36 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Contact{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Contact" %}{% endblock %}
3 4
 {% block content %}
4
-<h1 class="text-xl font-semibold mb-4">Contact Us{% if org %} — {{ org.name }}{% endif %}</h1>
5
+<h1 class="text-xl font-semibold mb-4">{% trans "Contact Us" %}{% if org %} — {{ org.name }}{% endif %}</h1>
5 6
 <form method="post" class="bg-white rounded shadow p-4 grid gap-4">
6 7
   {% csrf_token %}
7 8
   <div class="grid md:grid-cols-2 gap-4">
8 9
     <div>
9
-      <label class="block text-sm font-medium mb-1">Name</label>
10
+      <label class="block text-sm font-medium mb-1">{% trans "Name" %}</label>
10 11
       <input type="text" name="name" value="{{ form.name.value|default:'' }}" required class="w-full border rounded px-3 py-2">
11 12
     </div>
12 13
     <div>
13
-      <label class="block text-sm font-medium mb-1">Email</label>
14
+      <label class="block text-sm font-medium mb-1">{% trans "Email" %}</label>
14 15
       <input type="email" name="email" value="{{ form.email.value|default:'' }}" required class="w-full border rounded px-3 py-2">
15 16
     </div>
16 17
   </div>
17 18
   <div class="grid md:grid-cols-2 gap-4">
18 19
     <div>
19
-      <label class="block text-sm font-medium mb-1">Phone</label>
20
+      <label class="block text-sm font-medium mb-1">{% trans "Phone" %}</label>
20 21
       <input type="text" name="phone" value="{{ form.phone.value|default:'' }}" class="w-full border rounded px-3 py-2">
21 22
     </div>
22 23
     <div>
23
-      <label class="block text-sm font-medium mb-1">Subject</label>
24
+      <label class="block text-sm font-medium mb-1">{% trans "Subject" %}</label>
24 25
       <input type="text" name="subject" value="{{ form.subject.value|default:'' }}" class="w-full border rounded px-3 py-2">
25 26
     </div>
26 27
   </div>
27 28
   <div>
28
-    <label class="block text-sm font-medium mb-1">Message</label>
29
+    <label class="block text-sm font-medium mb-1">{% trans "Message" %}</label>
29 30
     <textarea name="message" rows="5" class="w-full border rounded px-3 py-2">{{ form.message.value|default:'' }}</textarea>
30 31
   </div>
31 32
   <div>
32
-    <button class="btn-primary" type="submit">Send</button>
33
+    <button class="btn-primary" type="submit">{% trans "Send" %}</button>
33 34
   </div>
34 35
 </form>
35 36
 <style>.btn-primary{background:#1d4ed8;color:white;padding:.5rem .75rem;border-radius:.375rem}</style>

+ 24 - 24
public_frontend/templates/public_frontend/home.html

@@ -1,16 +1,16 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% load static %}
3
-{% block title %}Home{% endblock %}
2
+{% load static i18n %}
3
+{% block title %}{% trans "Home" %}{% endblock %}
4 4
 
5 5
 {% block content %}
6 6
 <div class="grid gap-12 md:gap-16">
7 7
   {# Hero Section #}
8 8
   <section id="hero" class="bg-white rounded-lg shadow-md p-6 md:p-8 text-center">
9
-    <h1 class="text-3xl md:text-4xl font-bold mb-2">Streamline Your Factory's Recycling</h1>
10
-    <p class="text-gray-600 max-w-2xl mx-auto mb-6">We partner with businesses like yours to manage scrap materials efficiently. Browse listings, request a pickup, and turn your waste into a resource.</p>
9
+    <h1 class="text-3xl md:text-4xl font-bold mb-2">{% trans "Streamline Your Factory's Recycling" %}</h1>
10
+    <p class="text-gray-600 max-w-2xl mx-auto mb-6">{% trans "We partner with businesses like yours to manage scrap materials efficiently. Browse listings, request a pickup, and turn your waste into a resource." %}</p>
11 11
     <div class="flex gap-3 justify-center">
12
-      <a href="{% url 'public_frontend:pickup_request' %}" class="btn-primary">Request a Pickup</a>
13
-      <a href="#listings" class="btn-outline">Browse Listings</a>
12
+      <a href="{% url 'public_frontend:pickup_request' %}" class="btn-primary">{% trans "Request a Pickup" %}</a>
13
+      <a href="#listings" class="btn-outline">{% trans "Browse Listings" %}</a>
14 14
     </div>
15 15
   </section>
16 16
 
@@ -18,14 +18,14 @@
18 18
   {% if services %}
19 19
   <section id="services">
20 20
     <div class="flex items-center justify-between mb-3">
21
-      <h2 class="text-2xl font-semibold">Our Services</h2>
21
+      <h2 class="text-2xl font-semibold">{% trans "Our Services" %}</h2>
22 22
     </div>
23 23
     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
24 24
       {% for s in services %}
25 25
         <a href="{% url 'public_frontend:service_detail' s.pk %}" class="block p-4 bg-white rounded-lg shadow-md hover:bg-gray-50 transition flex items-start gap-4">
26 26
           <img src="{% static 'service_icons/' %}{{ s.image_name }}" alt="{{ s.title }} icon" class="w-12 h-12 object-contain"/>
27 27
           <div>
28
-            <div class="font-medium">{{ s.title }}</div>
28
+            <div class="font-medium">{{ s.get_title_display }}</div>
29 29
             <div class="text-sm text-gray-600">{{ s.description }}</div>
30 30
           </div>
31 31
         </a>
@@ -37,8 +37,8 @@
37 37
   {# Materials Section #}
38 38
   <section id="materials">
39 39
     <div class="flex items-center justify-between mb-3">
40
-      <h2 class="text-2xl font-semibold">Materials We Accept</h2>
41
-      <a href="{% url 'public_frontend:materials_list' %}" class="text-blue-700 hover:underline">View all</a>
40
+      <h2 class="text-2xl font-semibold">{% trans "Materials We Accept" %}</h2>
41
+      <a href="{% url 'public_frontend:materials_list' %}" class="text-blue-700 hover:underline">{% trans "View all" %}</a>
42 42
     </div>
43 43
     <div class="bg-white rounded-lg shadow-md p-4">
44 44
         <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@@ -48,7 +48,7 @@
48 48
                     <p class="text-sm text-gray-500">{{ m.category.name }}</p>
49 49
                 </div>
50 50
             {% empty %}
51
-                <p class="text-gray-500 col-span-full">No materials published.</p>
51
+                <p class="text-gray-500 col-span-full">{% trans "No materials published." %}</p>
52 52
             {% endfor %}
53 53
         </div>
54 54
     </div>
@@ -57,18 +57,18 @@
57 57
   {# Open Listings Section #}
58 58
   <section id="listings">
59 59
     <div class="flex items-center justify-between mb-3">
60
-      <h2 class="text-2xl font-semibold">Open Listings</h2>
61
-      <a href="{% url 'public_frontend:listings_list' %}" class="text-blue-700 hover:underline">Browse all</a>
60
+      <h2 class="text-2xl font-semibold">{% trans "Open Listings" %}</h2>
61
+      <a href="{% url 'public_frontend:listings_list' %}" class="text-blue-700 hover:underline">{% trans "Browse all" %}</a>
62 62
     </div>
63 63
     <div class="grid md:grid-cols-3 gap-4">
64 64
       {% for l in listings %}
65 65
         <a href="{% url 'public_frontend:listing_detail' l.id %}" class="block p-4 border rounded bg-white hover:bg-gray-50 transition">
66
-          <div class="text-sm text-gray-500">Ends {{ l.ends_at|default:'—' }}</div>
66
+          <div class="text-sm text-gray-500">{% trans "Ends" %} {{ l.ends_at|default:'—' }}</div>
67 67
           <div class="font-medium">{{ l.title }}</div>
68 68
           <div class="text-sm text-gray-600 line-clamp-2">{{ l.description|truncatechars:120 }}</div>
69 69
         </a>
70 70
       {% empty %}
71
-        <div class="text-gray-500 p-4 bg-white rounded-lg shadow-md col-span-full">No public listings at the moment.</div>
71
+        <div class="text-gray-500 p-4 bg-white rounded-lg shadow-md col-span-full">{% trans "No public listings at the moment." %}</div>
72 72
       {% endfor %}
73 73
     </div>
74 74
   </section>
@@ -78,8 +78,8 @@
78 78
   {# Blog Section #}
79 79
   <section id="blog">
80 80
     <div class="flex items-center justify-between mb-3">
81
-      <h2 class="text-2xl font-semibold">Industry Insights</h2>
82
-      <a href="{% url 'public_frontend:blog_list' %}" class="text-blue-700 hover:underline">View all</a>
81
+      <h2 class="text-2xl font-semibold">{% trans "Industry Insights" %}</h2>
82
+      <a href="{% url 'public_frontend:blog_list' %}" class="text-blue-700 hover:underline">{% trans "View all" %}</a>
83 83
     </div>
84 84
     <div class="grid md:grid-cols-3 gap-4">
85 85
       {% for p in posts %}
@@ -89,36 +89,36 @@
89 89
           <div class="text-sm text-gray-600 line-clamp-2">{{ p.excerpt|default:p.content|striptags|truncatechars:120 }}</div>
90 90
         </a>
91 91
       {% empty %}
92
-        <div class="text-gray-500 p-4 bg-white rounded-lg shadow-md col-span-full">No posts yet.</div>
92
+        <div class="text-gray-500 p-4 bg-white rounded-lg shadow-md col-span-full">{% trans "No posts yet." %}</div>
93 93
       {% endfor %}
94 94
     </div>
95 95
   </section>
96 96
 
97 97
   {# Contact Section #}
98 98
   <section id="contact">
99
-    <h2 class="text-2xl font-semibold mb-3">Contact Us</h2>
99
+    <h2 class="text-2xl font-semibold mb-3">{% trans "Contact Us" %}</h2>
100 100
     <form method="post" action="{% url 'public_frontend:contact' %}" class="bg-white rounded-lg shadow-md p-4 grid gap-4">
101 101
       {% csrf_token %}
102 102
       <div class="grid md:grid-cols-2 gap-4">
103 103
         <div>
104
-          <label class="block text-sm font-medium mb-1">Name</label>
104
+          <label class="block text-sm font-medium mb-1">{% trans "Name" %}</label>
105 105
           <input type="text" name="name" value="{{ contact_form.name.value|default:'' }}" required class="w-full border rounded px-3 py-2">
106 106
         </div>
107 107
         <div>
108
-          <label class="block text-sm font-medium mb-1">Email</label>
108
+          <label class="block text-sm font-medium mb-1">{% trans "Email" %}</label>
109 109
           <input type="email" name="email" value="{{ contact_form.email.value|default:'' }}" required class="w-full border rounded px-3 py-2">
110 110
         </div>
111 111
       </div>
112 112
       <div>
113
-        <label class="block text-sm font-medium mb-1">Subject</label>
113
+        <label class="block text-sm font-medium mb-1">{% trans "Subject" %}</label>
114 114
         <input type="text" name="subject" value="{{ contact_form.subject.value|default:'' }}" class="w-full border rounded px-3 py-2">
115 115
       </div>
116 116
       <div>
117
-        <label class="block text-sm font-medium mb-1">Message</label>
117
+        <label class="block text-sm font-medium mb-1">{% trans "Message" %}</label>
118 118
         <textarea name="message" rows="5" class="w-full border rounded px-3 py-2">{{ contact_form.message.value|default:'' }}</textarea>
119 119
       </div>
120 120
       <div>
121
-        <button class="btn-primary" type="submit">Send Message</button>
121
+        <button class="btn-primary" type="submit">{% trans "Send Message" %}</button>
122 122
       </div>
123 123
     </form>
124 124
   </section>

+ 5 - 4
public_frontend/templates/public_frontend/listing_detail.html

@@ -1,16 +1,17 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Listing {{ listing.id }}{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Listing" %} {{ listing.id }}{% endblock %}
3 4
 {% block content %}
4 5
 <article class="bg-white rounded shadow p-6">
5 6
   <h1 class="text-2xl font-semibold mb-1">{{ listing.title }}</h1>
6
-  <div class="text-sm text-gray-500 mb-4">Status: {{ listing.status }}{% if listing.ends_at %} • Ends {{ listing.ends_at }}{% endif %}</div>
7
+  <div class="text-sm text-gray-500 mb-4">{% trans "Status" %}: {{ listing.status }}{% if listing.ends_at %} • {% trans "Ends" %} {{ listing.ends_at }}{% endif %}</div>
7 8
   <p class="prose max-w-none">{{ listing.description|linebreaks }}</p>
8
-  <h2 class="font-semibold mt-6 mb-2">Items</h2>
9
+  <h2 class="font-semibold mt-6 mb-2">{% trans "Items" %}</h2>
9 10
   <ul class="list-disc ml-6 text-sm text-gray-700">
10 11
   {% for it in listing.items.all %}
11 12
     <li>{{ it.material.name }} — {{ it.quantity_estimate }} {{ it.get_unit_display }}</li>
12 13
   {% empty %}
13
-    <li>No items listed</li>
14
+    <li>{% trans "No items listed" %}</li>
14 15
   {% endfor %}
15 16
   </ul>
16 17
 </article>

+ 4 - 4
public_frontend/templates/public_frontend/listings_list.html

@@ -1,19 +1,19 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Listings{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Listings" %}{% endblock %}
3 4
 {% block content %}
4 5
 <div class="grid md:grid-cols-3 gap-4">
5 6
   {% for l in listings %}
6 7
     <a href="{% url 'public_frontend:listing_detail' l.id %}" class="block p-4 border rounded bg-white hover:bg-gray-50">
7
-      <div class="text-sm text-gray-500">Ends {{ l.ends_at|default:'—' }}</div>
8
+      <div class="text-sm text-gray-500">{% trans "Ends" %} {{ l.ends_at|default:'—' }}</div>
8 9
       <div class="font-medium">{{ l.title }}</div>
9 10
       <div class="text-sm text-gray-600 line-clamp-2">{{ l.description|truncatechars:140 }}</div>
10 11
     </a>
11 12
   {% empty %}
12
-    <div class="text-gray-500">No public listings.</div>
13
+    <div class="text-gray-500">{% trans "No public listings." %}</div>
13 14
   {% endfor %}
14 15
 </div>
15 16
 <div class="mt-4">
16 17
   {% include 'admin_frontend/_pagination.html' %}
17 18
   </div>
18 19
 {% endblock %}
19
-

+ 6 - 5
public_frontend/templates/public_frontend/materials_list.html

@@ -1,13 +1,14 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Materials{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Materials" %}{% endblock %}
3 4
 {% block content %}
4 5
 <div class="bg-white rounded shadow overflow-hidden">
5 6
   <table class="min-w-full text-sm">
6 7
     <thead class="bg-gray-50 text-left">
7 8
       <tr>
8
-        <th class="px-4 py-2">Name</th>
9
-        <th class="px-4 py-2">Category</th>
10
-        <th class="px-4 py-2">Default Unit</th>
9
+        <th class="px-4 py-2">{% trans "Name" %}</th>
10
+        <th class="px-4 py-2">{% trans "Category" %}</th>
11
+        <th class="px-4 py-2">{% trans "Default Unit" %}</th>
11 12
       </tr>
12 13
     </thead>
13 14
     <tbody>
@@ -18,7 +19,7 @@
18 19
           <td class="px-4 py-2">{{ m.get_default_unit_display }}</td>
19 20
         </tr>
20 21
       {% empty %}
21
-        <tr><td colspan="3" class="px-4 py-6 text-center text-gray-500">No materials published.</td></tr>
22
+        <tr><td colspan="3" class="px-4 py-6 text-center text-gray-500">{% trans "No materials published." %}</td></tr>
22 23
       {% endfor %}
23 24
     </tbody>
24 25
   </table>

+ 15 - 14
public_frontend/templates/public_frontend/pickup_request.html

@@ -1,8 +1,9 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Request Pickup{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Request Pickup" %}{% endblock %}
3 4
 {% load crispy_forms_tags widget_tweaks %}
4 5
 {% block content %}
5
-<h1 class="text-xl font-semibold mb-4">Request a Pickup</h1>
6
+<h1 class="text-xl font-semibold mb-4">{% trans "Request a Pickup" %}</h1>
6 7
 <div x-data="pickupForm()">
7 8
   <div class="bg-white rounded shadow p-4" >
8 9
     <form x-ref="form" id="pickup-form" method="post" enctype="multipart/form-data" class="space-y-4">
@@ -32,33 +33,33 @@
32 33
 
33 34
   <dialog x-ref="dialog" id="review_modal" class="modal dark:bg-black/40">
34 35
     <div class="modal-box dark:bg-gray-900 dark:text-gray-100 dark:border dark:border-gray-700">
35
-      <h3 class="font-bold text-lg">Review Your Pickup Request</h3>
36
+      <h3 class="font-bold text-lg">{% trans "Review Your Pickup Request" %}</h3>
36 37
       <div class="py-4 space-y-3">
37
-        <div><div class="text-sm text-gray-600 dark:text-gray-300">Name</div><div class="font-medium" x-text="rv.name || '—'"></div></div>
38
+        <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Name" %}</div><div class="font-medium" x-text="rv.name || '—'"></div></div>
38 39
         <div class="grid md:grid-cols-2 gap-4">
39
-          <div><div class="text-sm text-gray-600 dark:text-gray-300">Email</div><div class="font-medium" x-text="rv.email || '—'"></div></div>
40
-          <div><div class="text-sm text-gray-600 dark:text-gray-300">Phone</div><div class="font-medium" x-text="rv.phone || '—'"></div></div>
40
+          <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Email" %}</div><div class="font-medium" x-text="rv.email || '—'"></div></div>
41
+          <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Phone" %}</div><div class="font-medium" x-text="rv.phone || '—'"></div></div>
41 42
         </div>
42
-        <div><div class="text-sm text-gray-600 dark:text-gray-300">Pickup Address</div><div class="whitespace-pre-line" x-text="rv.address || '—'"></div></div>
43
+        <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Pickup Address" %}</div><div class="whitespace-pre-line" x-text="rv.address || '—'"></div></div>
43 44
         <div class="grid md:grid-cols-2 gap-4">
44
-          <div><div class="text-sm text-gray-600 dark:text-gray-300">Preferred Date/Time</div><div class="font-medium" x-text="rv.preferred_at || '—'"></div></div>
45
+          <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Preferred Date/Time" %}</div><div class="font-medium" x-text="rv.preferred_at || '—'"></div></div>
45 46
         </div>
46
-        <div><div class="text-sm text-gray-600 dark:text-gray-300">Materials / Notes</div><div class="whitespace-pre-line" x-text="rv.materials || '—'"></div></div>
47
+        <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Materials / Notes" %}</div><div class="whitespace-pre-line" x-text="rv.materials || '—'"></div></div>
47 48
         <div>
48
-          <div class="text-sm text-gray-600 dark:text-gray-300">Photos</div>
49
+          <div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Photos" %}</div>
49 50
           <ul class="list-disc list-inside text-sm text-gray-700 dark:text-gray-300">
50
-            <template x-if="(rv.photos||[]).length === 0"><li>No files attached.</li></template>
51
+            <template x-if="(rv.photos||[]).length === 0"><li>{% trans "No files attached." %}</li></template>
51 52
             <template x-for="fname in rv.photos" :key="fname"><li x-text="fname"></li></template>
52 53
           </ul>
53 54
         </div>
54 55
       </div>
55 56
       <div class="modal-action">
56
-        <button class="btn" @click="$refs.dialog.close()">Edit</button>
57
-        <button class="btn btn-primary" @click.prevent="confirm">Confirm &amp; Send</button>
57
+        <button class="btn" @click="$refs.dialog.close()">{% trans "Edit" %}</button>
58
+        <button class="btn btn-primary" @click.prevent="confirm">{% trans "Confirm & Send" %}</button>
58 59
       </div>
59 60
     </div>
60 61
     <form method="dialog" class="modal-backdrop dark:bg-black/60">
61
-      <button class="dark:text-gray-200">close</button>
62
+      <button class="dark:text-gray-200">{% trans "close" %}</button>
62 63
     </form>
63 64
   </dialog>
64 65
 </div>

+ 2 - 2
public_frontend/templates/public_frontend/service_detail.html

@@ -1,4 +1,5 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
+{% load i18n %}
2 3
 {% load static %}
3 4
 {% block title %}{{ service.title }}{% endblock %}
4 5
 
@@ -25,8 +26,7 @@
25 26
   {% endif %}
26 27
 
27 28
   <div>
28
-    <a href="/" class="text-blue-700 hover:underline">← Back to Home</a>
29
+    <a href="/" class="text-blue-700 hover:underline">← {% trans "Back to Home" %}</a>
29 30
   </div>
30 31
 </article>
31 32
 {% endblock %}
32
-

+ 9 - 8
recycle_core/models.py

@@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
6 6
 from django.contrib.contenttypes.models import ContentType
7 7
 from django.db import models
8 8
 from django.utils import timezone
9
+from django.utils.translation import gettext_lazy as _
9 10
 from orgs.models import Organization
10 11
 from markdownfield.models import MarkdownField, RenderedMarkdownField
11 12
 from markdownfield.validators import VALIDATOR_STANDARD
@@ -52,14 +53,14 @@ class MaterialCategory(TimestampedModel):
52 53
 class ProvidedService(TimestampedModel):
53 54
     organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name="services")
54 55
     TITLE_CHOICES = (
55
-        ("Pickup & Logistics", "Pickup & Logistics"),
56
-        ("Material Sorting", "Material Sorting"),
57
-        ("Weighing & Ticketing", "Weighing & Ticketing"),
58
-        ("Invoicing & Payouts", "Invoicing & Payouts"),
59
-        ("Reporting & Analytics", "Reporting & Analytics"),
60
-        ("Marketplace & Bidding", "Marketplace & Bidding"),
61
-        ("Compliance & Audits", "Compliance & Audits"),
62
-        ("Consulting & Training", "Consulting & Training"),
56
+        ("Pickup & Logistics", _("Pickup & Logistics")),
57
+        ("Material Sorting", _("Material Sorting")),
58
+        ("Weighing & Ticketing", _("Weighing & Ticketing")),
59
+        ("Invoicing & Payouts", _("Invoicing & Payouts")),
60
+        ("Reporting & Analytics", _("Reporting & Analytics")),
61
+        ("Marketplace & Bidding", _("Marketplace & Bidding")),
62
+        ("Compliance & Audits", _("Compliance & Audits")),
63
+        ("Consulting & Training", _("Consulting & Training")),
63 64
     )
64 65
     title = models.CharField(max_length=100, choices=TITLE_CHOICES)
65 66
     # Short summary text

+ 10 - 8
recycle_core/templates/recycle_core/pickups_list.html

@@ -8,9 +8,9 @@
8 8
 </div>
9 9
 
10 10
 <div class="bg-white rounded shadow p-4 mb-4">
11
-  <form method="get" class="space-y-3 flex gap-2">
11
+  <form method="get" class="flex flex-wrap gap-3 items-end">
12 12
     {{ filter.form|crispy }}
13
-    <div class="flex items-center gap-2">
13
+    <div class="flex items-center gap-2 w-full">
14 14
       <button class="px-3 py-2 bg-blue-600 text-white rounded">Filter</button>
15 15
       <a href="{% url 'recycle_core:pickups_list' %}" class="btn-outline">Reset</a>
16 16
     </div>
@@ -20,15 +20,16 @@
20 20
   {% endif %}
21 21
 </div>
22 22
 
23
-<div class="bg-white rounded shadow overflow-hidden">
24
-  <table class="min-w-full divide-y divide-gray-200">
23
+<div class="bg-white rounded shadow">
24
+  <div class="overflow-x-auto">
25
+  <table class="min-w-[1100px] divide-y divide-gray-200">
25 26
     <thead class="bg-gray-50">
26 27
       <tr>
27 28
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Customer</th>
28 29
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Site</th>
29 30
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Status</th>
30 31
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Scheduled</th>
31
-        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Driver</th>
32
+        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 w-56 min-w-[220px]">Driver</th>
32 33
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Actions</th>
33 34
       </tr>
34 35
     </thead>
@@ -38,11 +39,11 @@
38 39
         <td class="px-4 py-2">{{ p.customer.name }}</td>
39 40
         <td class="px-4 py-2">{{ p.site.name }}</td>
40 41
         <td class="px-4 py-2"><span class="px-2 py-1 rounded bg-gray-100 text-gray-700">{{ p.status }}</span></td>
41
-        <td class="px-4 py-2 text-gray-500">{{ p.scheduled_at|default:"-" }}</td>
42
-        <td class="px-4 py-2 text-gray-600">{{ p.assigned_driver.username|default:"-" }}</td>
42
+        <td class="px-4 py-2 text-gray-500">{{ p.scheduled_at|date:"SHORT_DATETIME_FORMAT"|default:"-" }}</td>
43
+        <td class="px-4 py-2 text-gray-600 w-56 min-w-[220px] whitespace-nowrap">{{ p.assigned_driver.username|default:"-" }}</td>
43 44
         <td class="px-4 py-2">
44 45
           <div class="flex flex-wrap gap-2 justify-end">
45
-            <a class="btn-outline btn-xs" href="{% url 'recycle_core:pickup_detail' p.id %}">View</a>
46
+            <a class="text-blue-600 hover:text-blue-700 hover:underline text-xs font-medium mr-3" href="{% url 'recycle_core:pickup_detail' p.id %}">View</a>
46 47
             {% if perms.recycle_core.assign_driver %}
47 48
             <form method="post" action="{% url 'recycle_core:pickup_assign' p.id %}" class="flex items-center gap-2">
48 49
               {% csrf_token %}
@@ -78,6 +79,7 @@
78 79
       {% endfor %}
79 80
     </tbody>
80 81
   </table>
82
+  </div>
81 83
   {% include 'admin_frontend/_pagination.html' %}
82 84
 </div>
83 85
 {% endblock %}

+ 22 - 1
recycle_core/views.py

@@ -7,6 +7,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
7 7
 import django_filters as filters
8 8
 
9 9
 from admin_frontend.templatetags.public_urls import public_route
10
+from django import forms
10 11
 from admin_frontend.nav import _nav_items
11 12
 from cms.views import breadcrumbs
12 13
 from orgs.decorators import permissions_required
@@ -421,7 +422,16 @@ def pickups_list(request):
421 422
         site = filters.ModelChoiceFilter(queryset=CustomerSite.objects.all())
422 423
         assigned_driver = filters.CharFilter(field_name="assigned_driver__username", lookup_expr="icontains", label="Driver")
423 424
         status = filters.ChoiceFilter(field_name="status", choices=PickupOrder.STATUS_CHOICES)
424
-        scheduled_at = filters.DateFromToRangeFilter(field_name="scheduled_at", label="Scheduled between")
425
+        scheduled_at = filters.DateTimeFromToRangeFilter(
426
+            field_name="scheduled_at",
427
+            label="Scheduled between",
428
+            widget=filters.widgets.RangeWidget(
429
+                attrs={
430
+                    "type": "datetime-local",
431
+                    "class": "border border-gray-300 rounded px-2 py-1"
432
+                }
433
+            ),
434
+        )
425 435
 
426 436
         class Meta:
427 437
             model = PickupOrder
@@ -448,6 +458,11 @@ def pickups_list(request):
448 458
 
449 459
     # empty forms used in row actions
450 460
     assign_form = PickupAssignForm()
461
+    # Limit driver choices to users with driver role, scoped to org if present
462
+    drivers_qs = get_user_model().objects.filter(recycle_profile__role="driver")
463
+    if org is not None:
464
+        drivers_qs = drivers_qs.filter(recycle_profile__organization=org)
465
+    assign_form.fields["driver"].queryset = drivers_qs.order_by("username")
451 466
     status_form = PickupStatusForm()
452 467
 
453 468
     context = {
@@ -467,6 +482,12 @@ def pickups_list(request):
467 482
 def pickup_assign(request, pk: int):
468 483
     pickup = get_object_or_404(PickupOrder, pk=pk)
469 484
     form = PickupAssignForm(request.POST)
485
+    # Enforce driver role (and org, if present) on POST validation
486
+    org = getattr(request, "org", None)
487
+    drivers_qs = get_user_model().objects.filter(recycle_profile__role="driver")
488
+    if org is not None:
489
+        drivers_qs = drivers_qs.filter(recycle_profile__organization=org)
490
+    form.fields["driver"].queryset = drivers_qs
470 491
     if form.is_valid():
471 492
         pickup.assigned_driver = form.cleaned_data["driver"]
472 493
         pickup.status = PickupOrder.STATUS_SCHEDULED

+ 27 - 0
translations/menu_labels.py

@@ -0,0 +1,27 @@
1
+from django.utils.translation import gettext_lazy as _
2
+
3
+# Mark app menu labels for translation extraction
4
+_("Dashboard")
5
+_("Profiles")
6
+_("Opportunities")
7
+_("Intro Requests")
8
+_("Leaderboard")
9
+_("Leads")
10
+_("CMS Posts")
11
+_("CMS Categories")
12
+_("Settings")
13
+_("Advanced Settings")
14
+_("Organization Settings")
15
+_("Organization Sites")
16
+_("Permissions")
17
+_("Users")
18
+_("Materials")
19
+_("Customers")
20
+_("Pickups")
21
+_("Weigh Tickets")
22
+_("Documents")
23
+_("Provided Services")
24
+_("Scrap Listings")
25
+_("Invoices")
26
+_("Payouts")
27
+

tum/whitesports - Gogs: Simplico Git Service

Ei kuvausta

Entities.php 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. <?php
  2. /**
  3. * SimplePie
  4. *
  5. * A PHP-Based RSS and Atom Feed Framework.
  6. * Takes the hard work out of managing a complete RSS/Atom solution.
  7. *
  8. * Copyright (c) 2004-2016, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
  9. * All rights reserved.
  10. *
  11. * Redistribution and use in source and binary forms, with or without modification, are
  12. * permitted provided that the following conditions are met:
  13. *
  14. * * Redistributions of source code must retain the above copyright notice, this list of
  15. * conditions and the following disclaimer.
  16. *
  17. * * Redistributions in binary form must reproduce the above copyright notice, this list
  18. * of conditions and the following disclaimer in the documentation and/or other materials
  19. * provided with the distribution.
  20. *
  21. * * Neither the name of the SimplePie Team nor the names of its contributors may be used
  22. * to endorse or promote products derived from this software without specific prior
  23. * written permission.
  24. *
  25. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
  26. * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
  27. * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS
  28. * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  29. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  30. * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  31. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
  32. * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  33. * POSSIBILITY OF SUCH DAMAGE.
  34. *
  35. * @package SimplePie
  36. * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
  37. * @author Ryan Parman
  38. * @author Sam Sneddon
  39. * @author Ryan McCue
  40. * @link http://simplepie.org/ SimplePie
  41. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  42. */
  43. /**
  44. * Decode HTML Entities
  45. *
  46. * This implements HTML5 as of revision 967 (2007-06-28)
  47. *
  48. * @deprecated Use DOMDocument instead!
  49. * @package SimplePie
  50. */
  51. class SimplePie_Decode_HTML_Entities
  52. {
  53. /**
  54. * Data to be parsed
  55. *
  56. * @access private
  57. * @var string
  58. */
  59. var $data = '';
  60. /**
  61. * Currently consumed bytes
  62. *
  63. * @access private
  64. * @var string
  65. */
  66. var $consumed = '';
  67. /**
  68. * Position of the current byte being parsed
  69. *
  70. * @access private
  71. * @var int
  72. */
  73. var $position = 0;
  74. /**
  75. * Create an instance of the class with the input data
  76. *
  77. * @access public
  78. * @param string $data Input data
  79. */
  80. public function __construct($data)
  81. {
  82. $this->data = $data;
  83. }
  84. /**
  85. * Parse the input data
  86. *
  87. * @access public
  88. * @return string Output data
  89. */
  90. public function parse()
  91. {
  92. while (($this->position = strpos($this->data, '&', $this->position)) !== false)
  93. {
  94. $this->consume();
  95. $this->entity();
  96. $this->consumed = '';
  97. }
  98. return $this->data;
  99. }
  100. /**
  101. * Consume the next byte
  102. *
  103. * @access private
  104. * @return mixed The next byte, or false, if there is no more data
  105. */
  106. public function consume()
  107. {
  108. if (isset($this->data[$this->position]))
  109. {
  110. $this->consumed .= $this->data[$this->position];
  111. return $this->data[$this->position++];
  112. }
  113. return false;
  114. }
  115. /**
  116. * Consume a range of characters
  117. *
  118. * @access private
  119. * @param string $chars Characters to consume
  120. * @return mixed A series of characters that match the range, or false
  121. */
  122. public function consume_range($chars)
  123. {
  124. if ($len = strspn($this->data, $chars, $this->position))
  125. {
  126. $data = substr($this->data, $this->position, $len);
  127. $this->consumed .= $data;
  128. $this->position += $len;
  129. return $data;
  130. }
  131. return false;
  132. }
  133. /**
  134. * Unconsume one byte
  135. *
  136. * @access private
  137. */
  138. public function unconsume()
  139. {
  140. $this->consumed = substr($this->consumed, 0, -1);
  141. $this->position--;
  142. }
  143. /**
  144. * Decode an entity
  145. *
  146. * @access private
  147. */
  148. public function entity()
  149. {
  150. switch ($this->consume())
  151. {
  152. case "\x09":
  153. case "\x0A":
  154. case "\x0B":
  155. case "\x0C":
  156. case "\x20":
  157. case "\x3C":
  158. case "\x26":
  159. case false:
  160. break;
  161. case "\x23":
  162. switch ($this->consume())
  163. {
  164. case "\x78":
  165. case "\x58":
  166. $range = '0123456789ABCDEFabcdef';
  167. $hex = true;
  168. break;
  169. default:
  170. $range = '0123456789';
  171. $hex = false;
  172. $this->unconsume();
  173. break;
  174. }
  175. if ($codepoint = $this->consume_range($range))
  176. {
  177. static $windows_1252_specials = array(0x0D => "\x0A", 0x80 => "\xE2\x82\xAC", 0x81 => "\xEF\xBF\xBD", 0x82 => "\xE2\x80\x9A", 0x83 => "\xC6\x92", 0x84 => "\xE2\x80\x9E", 0x85 => "\xE2\x80\xA6", 0x86 => "\xE2\x80\xA0", 0x87 => "\xE2\x80\xA1", 0x88 => "\xCB\x86", 0x89 => "\xE2\x80\xB0", 0x8A => "\xC5\xA0", 0x8B => "\xE2\x80\xB9", 0x8C => "\xC5\x92", 0x8D => "\xEF\xBF\xBD", 0x8E => "\xC5\xBD", 0x8F => "\xEF\xBF\xBD", 0x90 => "\xEF\xBF\xBD", 0x91 => "\xE2\x80\x98", 0x92 => "\xE2\x80\x99", 0x93 => "\xE2\x80\x9C", 0x94 => "\xE2\x80\x9D", 0x95 => "\xE2\x80\xA2", 0x96 => "\xE2\x80\x93", 0x97 => "\xE2\x80\x94", 0x98 => "\xCB\x9C", 0x99 => "\xE2\x84\xA2", 0x9A => "\xC5\xA1", 0x9B => "\xE2\x80\xBA", 0x9C => "\xC5\x93", 0x9D => "\xEF\xBF\xBD", 0x9E => "\xC5\xBE", 0x9F => "\xC5\xB8");
  178. if ($hex)
  179. {
  180. $codepoint = hexdec($codepoint);
  181. }
  182. else
  183. {
  184. $codepoint = intval($codepoint);
  185. }
  186. if (isset($windows_1252_specials[$codepoint]))
  187. {
  188. $replacement = $windows_1252_specials[$codepoint];
  189. }
  190. else
  191. {
  192. $replacement = SimplePie_Misc::codepoint_to_utf8($codepoint);
  193. }
  194. if (!in_array($this->consume(), array(';', false), true))
  195. {
  196. $this->unconsume();
  197. }
  198. $consumed_length = strlen($this->consumed);
  199. $this->data = substr_replace($this->data, $replacement, $this->position - $consumed_length, $consumed_length);
  200. $this->position += strlen($replacement) - $consumed_length;
  201. }
  202. break;
  203. default:
  204. static $entities = array(
  205. 'Aacute' => "\xC3\x81",
  206. 'aacute' => "\xC3\xA1",
  207. 'Aacute;' => "\xC3\x81",
  208. 'aacute;' => "\xC3\xA1",
  209. 'Acirc' => "\xC3\x82",
  210. 'acirc' => "\xC3\xA2",
  211. 'Acirc;' => "\xC3\x82",
  212. 'acirc;' => "\xC3\xA2",
  213. 'acute' => "\xC2\xB4",
  214. 'acute;' => "\xC2\xB4",
  215. 'AElig' => "\xC3\x86",
  216. 'aelig' => "\xC3\xA6",
  217. 'AElig;' => "\xC3\x86",
  218. 'aelig;' => "\xC3\xA6",
  219. 'Agrave' => "\xC3\x80",
  220. 'agrave' => "\xC3\xA0",
  221. 'Agrave;' => "\xC3\x80",
  222. 'agrave;' => "\xC3\xA0",
  223. 'alefsym;' => "\xE2\x84\xB5",
  224. 'Alpha;' => "\xCE\x91",
  225. 'alpha;' => "\xCE\xB1",
  226. 'AMP' => "\x26",
  227. 'amp' => "\x26",
  228. 'AMP;' => "\x26",
  229. 'amp;' => "\x26",
  230. 'and;' => "\xE2\x88\xA7",
  231. 'ang;' => "\xE2\x88\xA0",
  232. 'apos;' => "\x27",
  233. 'Aring' => "\xC3\x85",
  234. 'aring' => "\xC3\xA5",
  235. 'Aring;' => "\xC3\x85",
  236. 'aring;' => "\xC3\xA5",
  237. 'asymp;' => "\xE2\x89\x88",
  238. 'Atilde' => "\xC3\x83",
  239. 'atilde' => "\xC3\xA3",
  240. 'Atilde;' => "\xC3\x83",
  241. 'atilde;' => "\xC3\xA3",
  242. 'Auml' => "\xC3\x84",
  243. 'auml' => "\xC3\xA4",
  244. 'Auml;' => "\xC3\x84",
  245. 'auml;' => "\xC3\xA4",
  246. 'bdquo;' => "\xE2\x80\x9E",
  247. 'Beta;' => "\xCE\x92",
  248. 'beta;' => "\xCE\xB2",
  249. 'brvbar' => "\xC2\xA6",
  250. 'brvbar;' => "\xC2\xA6",
  251. 'bull;' => "\xE2\x80\xA2",
  252. 'cap;' => "\xE2\x88\xA9",
  253. 'Ccedil' => "\xC3\x87",
  254. 'ccedil' => "\xC3\xA7",
  255. 'Ccedil;' => "\xC3\x87",
  256. 'ccedil;' => "\xC3\xA7",
  257. 'cedil' => "\xC2\xB8",
  258. 'cedil;' => "\xC2\xB8",
  259. 'cent' => "\xC2\xA2",
  260. 'cent;' => "\xC2\xA2",
  261. 'Chi;' => "\xCE\xA7",
  262. 'chi;' => "\xCF\x87",
  263. 'circ;' => "\xCB\x86",
  264. 'clubs;' => "\xE2\x99\xA3",
  265. 'cong;' => "\xE2\x89\x85",
  266. 'COPY' => "\xC2\xA9",
  267. 'copy' => "\xC2\xA9",
  268. 'COPY;' => "\xC2\xA9",
  269. 'copy;' => "\xC2\xA9",
  270. 'crarr;' => "\xE2\x86\xB5",
  271. 'cup;' => "\xE2\x88\xAA",
  272. 'curren' => "\xC2\xA4",
  273. 'curren;' => "\xC2\xA4",
  274. 'Dagger;' => "\xE2\x80\xA1",
  275. 'dagger;' => "\xE2\x80\xA0",
  276. 'dArr;' => "\xE2\x87\x93",
  277. 'darr;' => "\xE2\x86\x93",
  278. 'deg' => "\xC2\xB0",
  279. 'deg;' => "\xC2\xB0",
  280. 'Delta;' => "\xCE\x94",
  281. 'delta;' => "\xCE\xB4",
  282. 'diams;' => "\xE2\x99\xA6",
  283. 'divide' => "\xC3\xB7",
  284. 'divide;' => "\xC3\xB7",
  285. 'Eacute' => "\xC3\x89",
  286. 'eacute' => "\xC3\xA9",
  287. 'Eacute;' => "\xC3\x89",
  288. 'eacute;' => "\xC3\xA9",
  289. 'Ecirc' => "\xC3\x8A",
  290. 'ecirc' => "\xC3\xAA",
  291. 'Ecirc;' => "\xC3\x8A",
  292. 'ecirc;' => "\xC3\xAA",
  293. 'Egrave' => "\xC3\x88",
  294. 'egrave' => "\xC3\xA8",
  295. 'Egrave;' => "\xC3\x88",
  296. 'egrave;' => "\xC3\xA8",
  297. 'empty;' => "\xE2\x88\x85",
  298. 'emsp;' => "\xE2\x80\x83",
  299. 'ensp;' => "\xE2\x80\x82",
  300. 'Epsilon;' => "\xCE\x95",
  301. 'epsilon;' => "\xCE\xB5",
  302. 'equiv;' => "\xE2\x89\xA1",
  303. 'Eta;' => "\xCE\x97",
  304. 'eta;' => "\xCE\xB7",
  305. 'ETH' => "\xC3\x90",
  306. 'eth' => "\xC3\xB0",
  307. 'ETH;' => "\xC3\x90",
  308. 'eth;' => "\xC3\xB0",
  309. 'Euml' => "\xC3\x8B",
  310. 'euml' => "\xC3\xAB",
  311. 'Euml;' => "\xC3\x8B",
  312. 'euml;' => "\xC3\xAB",
  313. 'euro;' => "\xE2\x82\xAC",
  314. 'exist;' => "\xE2\x88\x83",
  315. 'fnof;' => "\xC6\x92",
  316. 'forall;' => "\xE2\x88\x80",
  317. 'frac12' => "\xC2\xBD",
  318. 'frac12;' => "\xC2\xBD",
  319. 'frac14' => "\xC2\xBC",
  320. 'frac14;' => "\xC2\xBC",
  321. 'frac34' => "\xC2\xBE",
  322. 'frac34;' => "\xC2\xBE",
  323. 'frasl;' => "\xE2\x81\x84",
  324. 'Gamma;' => "\xCE\x93",
  325. 'gamma;' => "\xCE\xB3",
  326. 'ge;' => "\xE2\x89\xA5",
  327. 'GT' => "\x3E",
  328. 'gt' => "\x3E",
  329. 'GT;' => "\x3E",
  330. 'gt;' => "\x3E",
  331. 'hArr;' => "\xE2\x87\x94",
  332. 'harr;' => "\xE2\x86\x94",
  333. 'hearts;' => "\xE2\x99\xA5",
  334. 'hellip;' => "\xE2\x80\xA6",
  335. 'Iacute' => "\xC3\x8D",
  336. 'iacute' => "\xC3\xAD",
  337. 'Iacute;' => "\xC3\x8D",
  338. 'iacute;' => "\xC3\xAD",
  339. 'Icirc' => "\xC3\x8E",
  340. 'icirc' => "\xC3\xAE",
  341. 'Icirc;' => "\xC3\x8E",
  342. 'icirc;' => "\xC3\xAE",
  343. 'iexcl' => "\xC2\xA1",
  344. 'iexcl;' => "\xC2\xA1",
  345. 'Igrave' => "\xC3\x8C",
  346. 'igrave' => "\xC3\xAC",
  347. 'Igrave;' => "\xC3\x8C",
  348. 'igrave;' => "\xC3\xAC",
  349. 'image;' => "\xE2\x84\x91",
  350. 'infin;' => "\xE2\x88\x9E",
  351. 'int;' => "\xE2\x88\xAB",
  352. 'Iota;' => "\xCE\x99",
  353. 'iota;' => "\xCE\xB9",
  354. 'iquest' => "\xC2\xBF",
  355. 'iquest;' => "\xC2\xBF",
  356. 'isin;' => "\xE2\x88\x88",
  357. 'Iuml' => "\xC3\x8F",
  358. 'iuml' => "\xC3\xAF",
  359. 'Iuml;' => "\xC3\x8F",
  360. 'iuml;' => "\xC3\xAF",
  361. 'Kappa;' => "\xCE\x9A",
  362. 'kappa;' => "\xCE\xBA",
  363. 'Lambda;' => "\xCE\x9B",
  364. 'lambda;' => "\xCE\xBB",
  365. 'lang;' => "\xE3\x80\x88",
  366. 'laquo' => "\xC2\xAB",
  367. 'laquo;' => "\xC2\xAB",
  368. 'lArr;' => "\xE2\x87\x90",
  369. 'larr;' => "\xE2\x86\x90",
  370. 'lceil;' => "\xE2\x8C\x88",
  371. 'ldquo;' => "\xE2\x80\x9C",
  372. 'le;' => "\xE2\x89\xA4",
  373. 'lfloor;' => "\xE2\x8C\x8A",
  374. 'lowast;' => "\xE2\x88\x97",
  375. 'loz;' => "\xE2\x97\x8A",
  376. 'lrm;' => "\xE2\x80\x8E",
  377. 'lsaquo;' => "\xE2\x80\xB9",
  378. 'lsquo;' => "\xE2\x80\x98",
  379. 'LT' => "\x3C",
  380. 'lt' => "\x3C",
  381. 'LT;' => "\x3C",
  382. 'lt;' => "\x3C",
  383. 'macr' => "\xC2\xAF",
  384. 'macr;' => "\xC2\xAF",
  385. 'mdash;' => "\xE2\x80\x94",
  386. 'micro' => "\xC2\xB5",
  387. 'micro;' => "\xC2\xB5",
  388. 'middot' => "\xC2\xB7",
  389. 'middot;' => "\xC2\xB7",
  390. 'minus;' => "\xE2\x88\x92",
  391. 'Mu;' => "\xCE\x9C",
  392. 'mu;' => "\xCE\xBC",
  393. 'nabla;' => "\xE2\x88\x87",
  394. 'nbsp' => "\xC2\xA0",
  395. 'nbsp;' => "\xC2\xA0",
  396. 'ndash;' => "\xE2\x80\x93",
  397. 'ne;' => "\xE2\x89\xA0",
  398. 'ni;' => "\xE2\x88\x8B",
  399. 'not' => "\xC2\xAC",
  400. 'not;' => "\xC2\xAC",
  401. 'notin;' => "\xE2\x88\x89",
  402. 'nsub;' => "\xE2\x8A\x84",
  403. 'Ntilde' => "\xC3\x91",
  404. 'ntilde' => "\xC3\xB1",
  405. 'Ntilde;' => "\xC3\x91",
  406. 'ntilde;' => "\xC3\xB1",
  407. 'Nu;' => "\xCE\x9D",
  408. 'nu;' => "\xCE\xBD",
  409. 'Oacute' => "\xC3\x93",
  410. 'oacute' => "\xC3\xB3",
  411. 'Oacute;' => "\xC3\x93",
  412. 'oacute;' => "\xC3\xB3",
  413. 'Ocirc' => "\xC3\x94",
  414. 'ocirc' => "\xC3\xB4",
  415. 'Ocirc;' => "\xC3\x94",
  416. 'ocirc;' => "\xC3\xB4",
  417. 'OElig;' => "\xC5\x92",
  418. 'oelig;' => "\xC5\x93",
  419. 'Ograve' => "\xC3\x92",
  420. 'ograve' => "\xC3\xB2",
  421. 'Ograve;' => "\xC3\x92",
  422. 'ograve;' => "\xC3\xB2",
  423. 'oline;' => "\xE2\x80\xBE",
  424. 'Omega;' => "\xCE\xA9",
  425. 'omega;' => "\xCF\x89",
  426. 'Omicron;' => "\xCE\x9F",
  427. 'omicron;' => "\xCE\xBF",
  428. 'oplus;' => "\xE2\x8A\x95",
  429. 'or;' => "\xE2\x88\xA8",
  430. 'ordf' => "\xC2\xAA",
  431. 'ordf;' => "\xC2\xAA",
  432. 'ordm' => "\xC2\xBA",
  433. 'ordm;' => "\xC2\xBA",
  434. 'Oslash' => "\xC3\x98",
  435. 'oslash' => "\xC3\xB8",
  436. 'Oslash;' => "\xC3\x98",
  437. 'oslash;' => "\xC3\xB8",
  438. 'Otilde' => "\xC3\x95",
  439. 'otilde' => "\xC3\xB5",
  440. 'Otilde;' => "\xC3\x95",
  441. 'otilde;' => "\xC3\xB5",
  442. 'otimes;' => "\xE2\x8A\x97",
  443. 'Ouml' => "\xC3\x96",
  444. 'ouml' => "\xC3\xB6",
  445. 'Ouml;' => "\xC3\x96",
  446. 'ouml;' => "\xC3\xB6",
  447. 'para' => "\xC2\xB6",
  448. 'para;' => "\xC2\xB6",
  449. 'part;' => "\xE2\x88\x82",
  450. 'permil;' => "\xE2\x80\xB0",
  451. 'perp;' => "\xE2\x8A\xA5",
  452. 'Phi;' => "\xCE\xA6",
  453. 'phi;' => "\xCF\x86",
  454. 'Pi;' => "\xCE\xA0",
  455. 'pi;' => "\xCF\x80",
  456. 'piv;' => "\xCF\x96",
  457. 'plusmn' => "\xC2\xB1",
  458. 'plusmn;' => "\xC2\xB1",
  459. 'pound' => "\xC2\xA3",
  460. 'pound;' => "\xC2\xA3",
  461. 'Prime;' => "\xE2\x80\xB3",
  462. 'prime;' => "\xE2\x80\xB2",
  463. 'prod;' => "\xE2\x88\x8F",
  464. 'prop;' => "\xE2\x88\x9D",
  465. 'Psi;' => "\xCE\xA8",
  466. 'psi;' => "\xCF\x88",
  467. 'QUOT' => "\x22",
  468. 'quot' => "\x22",
  469. 'QUOT;' => "\x22",
  470. 'quot;' => "\x22",
  471. 'radic;' => "\xE2\x88\x9A",
  472. 'rang;' => "\xE3\x80\x89",
  473. 'raquo' => "\xC2\xBB",
  474. 'raquo;' => "\xC2\xBB",
  475. 'rArr;' => "\xE2\x87\x92",
  476. 'rarr;' => "\xE2\x86\x92",
  477. 'rceil;' => "\xE2\x8C\x89",
  478. 'rdquo;' => "\xE2\x80\x9D",
  479. 'real;' => "\xE2\x84\x9C",
  480. 'REG' => "\xC2\xAE",
  481. 'reg' => "\xC2\xAE",
  482. 'REG;' => "\xC2\xAE",
  483. 'reg;' => "\xC2\xAE",
  484. 'rfloor;' => "\xE2\x8C\x8B",
  485. 'Rho;' => "\xCE\xA1",
  486. 'rho;' => "\xCF\x81",
  487. 'rlm;' => "\xE2\x80\x8F",
  488. 'rsaquo;' => "\xE2\x80\xBA",
  489. 'rsquo;' => "\xE2\x80\x99",
  490. 'sbquo;' => "\xE2\x80\x9A",
  491. 'Scaron;' => "\xC5\xA0",
  492. 'scaron;' => "\xC5\xA1",
  493. 'sdot;' => "\xE2\x8B\x85",
  494. 'sect' => "\xC2\xA7",
  495. 'sect;' => "\xC2\xA7",
  496. 'shy' => "\xC2\xAD",
  497. 'shy;' => "\xC2\xAD",
  498. 'Sigma;' => "\xCE\xA3",
  499. 'sigma;' => "\xCF\x83",
  500. 'sigmaf;' => "\xCF\x82",
  501. 'sim;' => "\xE2\x88\xBC",
  502. 'spades;' => "\xE2\x99\xA0",
  503. 'sub;' => "\xE2\x8A\x82",
  504. 'sube;' => "\xE2\x8A\x86",
  505. 'sum;' => "\xE2\x88\x91",
  506. 'sup;' => "\xE2\x8A\x83",
  507. 'sup1' => "\xC2\xB9",
  508. 'sup1;' => "\xC2\xB9",
  509. 'sup2' => "\xC2\xB2",
  510. 'sup2;' => "\xC2\xB2",
  511. 'sup3' => "\xC2\xB3",
  512. 'sup3;' => "\xC2\xB3",
  513. 'supe;' => "\xE2\x8A\x87",
  514. 'szlig' => "\xC3\x9F",
  515. 'szlig;' => "\xC3\x9F",
  516. 'Tau;' => "\xCE\xA4",
  517. 'tau;' => "\xCF\x84",
  518. 'there4;' => "\xE2\x88\xB4",
  519. 'Theta;' => "\xCE\x98",
  520. 'theta;' => "\xCE\xB8",
  521. 'thetasym;' => "\xCF\x91",
  522. 'thinsp;' => "\xE2\x80\x89",
  523. 'THORN' => "\xC3\x9E",
  524. 'thorn' => "\xC3\xBE",
  525. 'THORN;' => "\xC3\x9E",
  526. 'thorn;' => "\xC3\xBE",
  527. 'tilde;' => "\xCB\x9C",
  528. 'times' => "\xC3\x97",
  529. 'times;' => "\xC3\x97",
  530. 'TRADE;' => "\xE2\x84\xA2",
  531. 'trade;' => "\xE2\x84\xA2",
  532. 'Uacute' => "\xC3\x9A",
  533. 'uacute' => "\xC3\xBA",
  534. 'Uacute;' => "\xC3\x9A",
  535. 'uacute;' => "\xC3\xBA",
  536. 'uArr;' => "\xE2\x87\x91",
  537. 'uarr;' => "\xE2\x86\x91",
  538. 'Ucirc' => "\xC3\x9B",
  539. 'ucirc' => "\xC3\xBB",
  540. 'Ucirc;' => "\xC3\x9B",
  541. 'ucirc;' => "\xC3\xBB",
  542. 'Ugrave' => "\xC3\x99",
  543. 'ugrave' => "\xC3\xB9",
  544. 'Ugrave;' => "\xC3\x99",
  545. 'ugrave;' => "\xC3\xB9",
  546. 'uml' => "\xC2\xA8",
  547. 'uml;' => "\xC2\xA8",
  548. 'upsih;' => "\xCF\x92",
  549. 'Upsilon;' => "\xCE\xA5",
  550. 'upsilon;' => "\xCF\x85",
  551. 'Uuml' => "\xC3\x9C",
  552. 'uuml' => "\xC3\xBC",
  553. 'Uuml;' => "\xC3\x9C",
  554. 'uuml;' => "\xC3\xBC",
  555. 'weierp;' => "\xE2\x84\x98",
  556. 'Xi;' => "\xCE\x9E",
  557. 'xi;' => "\xCE\xBE",
  558. 'Yacute' => "\xC3\x9D",
  559. 'yacute' => "\xC3\xBD",
  560. 'Yacute;' => "\xC3\x9D",
  561. 'yacute;' => "\xC3\xBD",
  562. 'yen' => "\xC2\xA5",
  563. 'yen;' => "\xC2\xA5",
  564. 'yuml' => "\xC3\xBF",
  565. 'Yuml;' => "\xC5\xB8",
  566. 'yuml;' => "\xC3\xBF",
  567. 'Zeta;' => "\xCE\x96",
  568. 'zeta;' => "\xCE\xB6",
  569. 'zwj;' => "\xE2\x80\x8D",
  570. 'zwnj;' => "\xE2\x80\x8C"
  571. );
  572. for ($i = 0, $match = null; $i < 9 && $this->consume() !== false; $i++)
  573. {
  574. $consumed = substr($this->consumed, 1);
  575. if (isset($entities[$consumed]))
  576. {
  577. $match = $consumed;
  578. }
  579. }
  580. if ($match !== null)
  581. {
  582. $this->data = substr_replace($this->data, $entities[$match], $this->position - strlen($consumed) - 1, strlen($match) + 1);
  583. $this->position += strlen($entities[$match]) - strlen($consumed) - 1;
  584. }
  585. break;
  586. }
  587. }
  588. }