+# Default date/time display formats (used by templates and localization)
225
+# Example: 09/25/24 14:30
226
+DATE_FORMAT = "m/d/y"
227
+TIME_FORMAT = "h:i"
228
+DATETIME_FORMAT = "m/d/y h:i"
229
+SHORT_DATE_FORMAT = DATE_FORMAT
230
+SHORT_DATETIME_FORMAT = DATETIME_FORMAT
231
+
211 232
 
212 233
 # Static and media files
213 234
 # https://docs.djangoproject.com/en/4.2/howto/static-files/
@@ -242,6 +263,20 @@ SPECTACULAR_SETTINGS = {
242 263
     'SERVE_INCLUDE_SCHEMA': False,
243 264
 }
244 265
 
266
+# Use custom format modules to override locale defaults (e.g., en-us)
267
+FORMAT_MODULE_PATH = [
268
+    'ecoloop.formats',
269
+]
270
+
271
+# Accept HTML5 datetime-local inputs and common alternatives
272
+DATETIME_INPUT_FORMATS = [
273
+    "%Y-%m-%dT%H:%M",  # HTML5 datetime-local
274
+    "%m/%d/%y %H:%M",
275
+    "%m/%d/%Y %H:%M",
276
+    "%Y-%m-%d %H:%M:%S",
277
+    "%Y-%m-%d %H:%M",
278
+]
279
+
245 280
 # Auth redirects
246 281
 LOGIN_URL = '/webadmin/login/'
247 282
 LOGIN_REDIRECT_URL = '/webadmin/'

+ 2 - 0
ecoloop/urls.py

@@ -35,6 +35,8 @@ urlpatterns = [
35 35
     path('webadmin/settings/', include('settings.urls')),
36 36
     path('webadmin/cms/', include('cms.urls')),
37 37
     path('webadmin/recycle/', include('recycle_core.urls')),
38
+    # i18n: language switcher and related utilities
39
+    path('i18n/', include('django.conf.urls.i18n')),
38 40
     path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
39 41
     path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
40 42
     path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),

BIN
locale/ja/LC_MESSAGES/django.mo


+ 371 - 0
locale/ja/LC_MESSAGES/django.po

@@ -0,0 +1,371 @@
1
+# SOME DESCRIPTIVE TITLE.
2
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+# This file is distributed under the same license as the PACKAGE package.
4
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+#
6
+#, fuzzy
7
+msgid ""
8
+msgstr ""
9
+"Project-Id-Version: PACKAGE VERSION\n"
10
+"Report-Msgid-Bugs-To: \n"
11
+"POT-Creation-Date: 2025-09-25 22:51+0700\n"
12
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+"Language-Team: LANGUAGE <LL@li.org>\n"
15
+"Language: ja\n"
16
+"MIME-Version: 1.0\n"
17
+"Content-Type: text/plain; charset=UTF-8\n"
18
+"Content-Transfer-Encoding: 8bit\n"
19
+"Plural-Forms: nplurals=1; plural=0;\n"
20
+
21
+#: admin_frontend/templates/admin_frontend/base.html:76
22
+#: translations/menu_labels.py:12
23
+msgid "Settings"
24
+msgstr "設定"
25
+
26
+#: admin_frontend/templates/admin_frontend/base.html:96
27
+msgid "CMS"
28
+msgstr "CMS"
29
+
30
+#: admin_frontend/templates/admin_frontend/base.html:117
31
+msgid "Frontend"
32
+msgstr "フロントエンド"
33
+
34
+#: admin_frontend/templates/admin_frontend/base.html:137
35
+msgid "Organization"
36
+msgstr "組織"
37
+
38
+#: admin_frontend/templates/admin_frontend/base.html:157
39
+msgid "Billing"
40
+msgstr "請求"
41
+
42
+#: admin_frontend/templates/admin_frontend/base.html:177
43
+msgid "Recycle Ops"
44
+msgstr "リサイクル業務"
45
+
46
+#: public_frontend/templates/public_frontend/base.html:17
47
+#: public_frontend/templates/public_frontend/materials_list.html:3
48
+#: translations/menu_labels.py:18
49
+msgid "Materials"
50
+msgstr "素材"
51
+
52
+#: public_frontend/templates/public_frontend/base.html:18
53
+#: public_frontend/templates/public_frontend/listings_list.html:3
54
+msgid "Listings"
55
+msgstr "出品"
56
+
57
+#: public_frontend/templates/public_frontend/base.html:19
58
+#: public_frontend/templates/public_frontend/pickup_request.html:3
59
+msgid "Request Pickup"
60
+msgstr "回収依頼"
61
+
62
+#: public_frontend/templates/public_frontend/base.html:20
63
+#: public_frontend/templates/public_frontend/blog_list.html:3
64
+msgid "Blog"
65
+msgstr "ブログ"
66
+
67
+#: public_frontend/templates/public_frontend/base.html:21
68
+#: public_frontend/templates/public_frontend/contact.html:3
69
+msgid "Contact"
70
+msgstr "お問い合わせ"
71
+
72
+#: public_frontend/templates/public_frontend/base.html:53
73
+msgid "Staff Login"
74
+msgstr "スタッフログイン"
75
+
76
+#: public_frontend/templates/public_frontend/blog_list.html:14
77
+msgid "No image"
78
+msgstr "画像なし"
79
+
80
+#: public_frontend/templates/public_frontend/blog_list.html:24
81
+#: public_frontend/templates/public_frontend/home.html:92
82
+msgid "No posts yet."
83
+msgstr "まだ投稿はありません。"
84
+
85
+#: public_frontend/templates/public_frontend/contact.html:5
86
+#: public_frontend/templates/public_frontend/home.html:99
87
+msgid "Contact Us"
88
+msgstr "お問い合わせ"
89
+
90
+#: public_frontend/templates/public_frontend/contact.html:10
91
+#: public_frontend/templates/public_frontend/home.html:104
92
+#: public_frontend/templates/public_frontend/materials_list.html:9
93
+#: public_frontend/templates/public_frontend/pickup_request.html:38
94
+msgid "Name"
95
+msgstr "氏名"
96
+
97
+#: public_frontend/templates/public_frontend/contact.html:14
98
+#: public_frontend/templates/public_frontend/home.html:108
99
+#: public_frontend/templates/public_frontend/pickup_request.html:40
100
+msgid "Email"
101
+msgstr "メール"
102
+
103
+#: public_frontend/templates/public_frontend/contact.html:20
104
+#: public_frontend/templates/public_frontend/pickup_request.html:41
105
+msgid "Phone"
106
+msgstr "電話"
107
+
108
+#: public_frontend/templates/public_frontend/contact.html:24
109
+#: public_frontend/templates/public_frontend/home.html:113
110
+msgid "Subject"
111
+msgstr "件名"
112
+
113
+#: public_frontend/templates/public_frontend/contact.html:29
114
+#: public_frontend/templates/public_frontend/home.html:117
115
+msgid "Message"
116
+msgstr "メッセージ"
117
+
118
+#: public_frontend/templates/public_frontend/contact.html:33
119
+msgid "Send"
120
+msgstr "送信"
121
+
122
+#: public_frontend/templates/public_frontend/home.html:3
123
+msgid "Home"
124
+msgstr "ホーム"
125
+
126
+#: public_frontend/templates/public_frontend/home.html:9
127
+msgid "Streamline Your Factory's Recycling"
128
+msgstr "工場のリサイクルを効率化"
129
+
130
+#: public_frontend/templates/public_frontend/home.html:10
131
+msgid ""
132
+"We partner with businesses like yours to manage scrap materials efficiently. "
133
+"Browse listings, request a pickup, and turn your waste into a resource."
134
+msgstr "貴社のような企業と連携し、スクラップ素材を効率的に管理します。リスティングを閲覧し、回収を依頼し、廃棄物を資源へと変えましょう。"
135
+
136
+#: public_frontend/templates/public_frontend/home.html:12
137
+#: public_frontend/templates/public_frontend/pickup_request.html:6
138
+msgid "Request a Pickup"
139
+msgstr "回収を依頼"
140
+
141
+#: public_frontend/templates/public_frontend/home.html:13
142
+msgid "Browse Listings"
143
+msgstr "リスティングを見る"
144
+
145
+#: public_frontend/templates/public_frontend/home.html:21
146
+msgid "Our Services"
147
+msgstr "サービス"
148
+
149
+#: public_frontend/templates/public_frontend/home.html:40
150
+msgid "Materials We Accept"
151
+msgstr "受け入れ可能な素材"
152
+
153
+#: public_frontend/templates/public_frontend/home.html:41
154
+#: public_frontend/templates/public_frontend/home.html:82
155
+msgid "View all"
156
+msgstr "すべて表示"
157
+
158
+#: public_frontend/templates/public_frontend/home.html:51
159
+#: public_frontend/templates/public_frontend/materials_list.html:22
160
+msgid "No materials published."
161
+msgstr "公開中の素材はありません。"
162
+
163
+#: public_frontend/templates/public_frontend/home.html:60
164
+msgid "Open Listings"
165
+msgstr "公開中のリスティング"
166
+
167
+#: public_frontend/templates/public_frontend/home.html:61
168
+msgid "Browse all"
169
+msgstr "すべて見る"
170
+
171
+#: public_frontend/templates/public_frontend/home.html:66
172
+#: public_frontend/templates/public_frontend/listing_detail.html:7
173
+#: public_frontend/templates/public_frontend/listings_list.html:8
174
+msgid "Ends"
175
+msgstr "終了"
176
+
177
+#: public_frontend/templates/public_frontend/home.html:71
178
+msgid "No public listings at the moment."
179
+msgstr "現在公開中のリスティングはありません。"
180
+
181
+#: public_frontend/templates/public_frontend/home.html:81
182
+msgid "Industry Insights"
183
+msgstr "業界インサイト"
184
+
185
+#: public_frontend/templates/public_frontend/home.html:121
186
+msgid "Send Message"
187
+msgstr "メッセージを送信"
188
+
189
+#: public_frontend/templates/public_frontend/listing_detail.html:3
190
+msgid "Listing"
191
+msgstr "リスティング"
192
+
193
+#: public_frontend/templates/public_frontend/listing_detail.html:7
194
+msgid "Status"
195
+msgstr "ステータス"
196
+
197
+#: public_frontend/templates/public_frontend/listing_detail.html:9
198
+msgid "Items"
199
+msgstr "アイテム"
200
+
201
+#: public_frontend/templates/public_frontend/listing_detail.html:14
202
+msgid "No items listed"
203
+msgstr "アイテムはありません"
204
+
205
+#: public_frontend/templates/public_frontend/listings_list.html:13
206
+msgid "No public listings."
207
+msgstr "公開中のリスティングはありません。"
208
+
209
+#: public_frontend/templates/public_frontend/materials_list.html:10
210
+msgid "Category"
211
+msgstr "カテゴリ"
212
+
213
+#: public_frontend/templates/public_frontend/materials_list.html:11
214
+msgid "Default Unit"
215
+msgstr "既定の単位"
216
+
217
+#: public_frontend/templates/public_frontend/pickup_request.html:36
218
+msgid "Review Your Pickup Request"
219
+msgstr "回収依頼内容の確認"
220
+
221
+#: public_frontend/templates/public_frontend/pickup_request.html:43
222
+msgid "Pickup Address"
223
+msgstr "回収住所"
224
+
225
+#: public_frontend/templates/public_frontend/pickup_request.html:45
226
+msgid "Preferred Date/Time"
227
+msgstr "希望日時"
228
+
229
+#: public_frontend/templates/public_frontend/pickup_request.html:47
230
+msgid "Materials / Notes"
231
+msgstr "素材/メモ"
232
+
233
+#: public_frontend/templates/public_frontend/pickup_request.html:49
234
+msgid "Photos"
235
+msgstr "写真"
236
+
237
+#: public_frontend/templates/public_frontend/pickup_request.html:51
238
+msgid "No files attached."
239
+msgstr "添付ファイルはありません。"
240
+
241
+#: public_frontend/templates/public_frontend/pickup_request.html:57
242
+msgid "Edit"
243
+msgstr "編集"
244
+
245
+#: public_frontend/templates/public_frontend/pickup_request.html:58
246
+msgid "Confirm & Send"
247
+msgstr "確認して送信"
248
+
249
+#: public_frontend/templates/public_frontend/pickup_request.html:62
250
+msgid "close"
251
+msgstr "閉じる"
252
+
253
+#: public_frontend/templates/public_frontend/service_detail.html:29
254
+msgid "Back to Home"
255
+msgstr "ホームに戻る"
256
+
257
+#: translations/menu_labels.py:4
258
+msgid "Dashboard"
259
+msgstr "ダッシュボード"
260
+
261
+#: translations/menu_labels.py:5
262
+msgid "Profiles"
263
+msgstr "プロフィール"
264
+
265
+#: translations/menu_labels.py:6
266
+msgid "Opportunities"
267
+msgstr "案件"
268
+
269
+#: translations/menu_labels.py:7
270
+msgid "Intro Requests"
271
+msgstr "紹介リクエスト"
272
+
273
+#: translations/menu_labels.py:8
274
+msgid "Leaderboard"
275
+msgstr "ランキング"
276
+
277
+#: translations/menu_labels.py:9
278
+msgid "Leads"
279
+msgstr "リード"
280
+
281
+#: translations/menu_labels.py:10
282
+msgid "CMS Posts"
283
+msgstr "CMS投稿"
284
+
285
+#: translations/menu_labels.py:11
286
+msgid "CMS Categories"
287
+msgstr "CMSカテゴリ"
288
+
289
+#: translations/menu_labels.py:13
290
+msgid "Advanced Settings"
291
+msgstr "高度な設定"
292
+
293
+#: translations/menu_labels.py:14
294
+msgid "Organization Settings"
295
+msgstr "組織設定"
296
+
297
+#: translations/menu_labels.py:15
298
+msgid "Organization Sites"
299
+msgstr "組織サイト"
300
+
301
+#: translations/menu_labels.py:16
302
+msgid "Permissions"
303
+msgstr "権限"
304
+
305
+#: translations/menu_labels.py:17
306
+msgid "Users"
307
+msgstr "ユーザー"
308
+
309
+#: translations/menu_labels.py:19
310
+msgid "Customers"
311
+msgstr "顧客"
312
+
313
+#: translations/menu_labels.py:20
314
+msgid "Pickups"
315
+msgstr "回収"
316
+
317
+#: translations/menu_labels.py:21
318
+msgid "Weigh Tickets"
319
+msgstr "計量票"
320
+
321
+#: translations/menu_labels.py:22
322
+msgid "Documents"
323
+msgstr "ドキュメント"
324
+
325
+#: translations/menu_labels.py:23
326
+msgid "Provided Services"
327
+msgstr "提供サービス"
328
+
329
+#: translations/menu_labels.py:24
330
+msgid "Scrap Listings"
331
+msgstr "スクラップ出品"
332
+
333
+#: translations/menu_labels.py:25
334
+msgid "Invoices"
335
+msgstr "請求書"
336
+
337
+#: translations/menu_labels.py:26
338
+msgid "Payouts"
339
+msgstr "支払い"
340
+
341
+#: recycle_core/models.py
342
+msgid "Pickup & Logistics"
343
+msgstr "回収と物流"
344
+
345
+#: recycle_core/models.py
346
+msgid "Material Sorting"
347
+msgstr "素材の仕分け"
348
+
349
+#: recycle_core/models.py
350
+msgid "Weighing & Ticketing"
351
+msgstr "計量とチケット処理"
352
+
353
+#: recycle_core/models.py
354
+msgid "Invoicing & Payouts"
355
+msgstr "請求と支払い"
356
+
357
+#: recycle_core/models.py
358
+msgid "Reporting & Analytics"
359
+msgstr "レポートと分析"
360
+
361
+#: recycle_core/models.py
362
+msgid "Marketplace & Bidding"
363
+msgstr "マーケットプレイスと入札"
364
+
365
+#: recycle_core/models.py
366
+msgid "Compliance & Audits"
367
+msgstr "コンプライアンスと監査"
368
+
369
+#: recycle_core/models.py
370
+msgid "Consulting & Training"
371
+msgstr "コンサルティングとトレーニング"

BIN
locale/th/LC_MESSAGES/django.mo


+ 370 - 0
locale/th/LC_MESSAGES/django.po

@@ -0,0 +1,370 @@
1
+# SOME DESCRIPTIVE TITLE.
2
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+# This file is distributed under the same license as the PACKAGE package.
4
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+#
6
+msgid ""
7
+msgstr ""
8
+"Project-Id-Version: PACKAGE VERSION\n"
9
+"Report-Msgid-Bugs-To: \n"
10
+"POT-Creation-Date: 2025-09-25 22:51+0700\n"
11
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
+"Language-Team: LANGUAGE <LL@li.org>\n"
14
+"Language: th\n"
15
+"MIME-Version: 1.0\n"
16
+"Content-Type: text/plain; charset=UTF-8\n"
17
+"Content-Transfer-Encoding: 8bit\n"
18
+"Plural-Forms: nplurals=1; plural=0;\n"
19
+
20
+#: admin_frontend/templates/admin_frontend/base.html:76
21
+#: translations/menu_labels.py:12
22
+msgid "Settings"
23
+msgstr "การตั้งค่า"
24
+
25
+#: admin_frontend/templates/admin_frontend/base.html:96
26
+msgid "CMS"
27
+msgstr "CMS"
28
+
29
+#: admin_frontend/templates/admin_frontend/base.html:117
30
+msgid "Frontend"
31
+msgstr "ส่วนหน้า"
32
+
33
+#: admin_frontend/templates/admin_frontend/base.html:137
34
+msgid "Organization"
35
+msgstr "องค์กร"
36
+
37
+#: admin_frontend/templates/admin_frontend/base.html:157
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/network_report_server - Gogs: Simplico Git Service

Nav apraksta

tum 30f7226d9a first commit 2 gadi atpakaļ
..
License 30f7226d9a first commit 2 gadi atpakaļ
Readme.md 30f7226d9a first commit 2 gadi atpakaļ
package.json 30f7226d9a first commit 2 gadi atpakaļ
yarn.lock 30f7226d9a first commit 2 gadi atpakaļ

Readme.md

combined-stream

A stream that emits multiple other streams one after another.

NB Currently combined-stream works with streams version 1 only. There is ongoing effort to switch this library to streams version 2. Any help is welcome. :) Meanwhile you can explore other libraries that provide streams2 support with more or less compatibility with combined-stream.

  • combined-stream2: A drop-in streams2-compatible replacement for the combined-stream module.

  • multistream: A stream that emits multiple other streams one after another.

Installation

npm install combined-stream

Usage

Here is a simple example that shows how you can use combined-stream to combine two files into one:

var CombinedStream = require('combined-stream');
var fs = require('fs');

var combinedStream = CombinedStream.create();
combinedStream.append(fs.createReadStream('file1.txt'));
combinedStream.append(fs.createReadStream('file2.txt'));

combinedStream.pipe(fs.createWriteStream('combined.txt'));

While the example above works great, it will pause all source streams until they are needed. If you don't want that to happen, you can set pauseStreams to false:

var CombinedStream = require('combined-stream');
var fs = require('fs');

var combinedStream = CombinedStream.create({pauseStreams: false});
combinedStream.append(fs.createReadStream('file1.txt'));
combinedStream.append(fs.createReadStream('file2.txt'));

combinedStream.pipe(fs.createWriteStream('combined.txt'));

However, what if you don't have all the source streams yet, or you don't want to allocate the resources (file descriptors, memory, etc.) for them right away? Well, in that case you can simply provide a callback that supplies the stream by calling a next() function:

var CombinedStream = require('combined-stream');
var fs = require('fs');

var combinedStream = CombinedStream.create();
combinedStream.append(function(next) {
  next(fs.createReadStream('file1.txt'));
});
combinedStream.append(function(next) {
  next(fs.createReadStream('file2.txt'));
});

combinedStream.pipe(fs.createWriteStream('combined.txt'));

API

CombinedStream.create([options])

Returns a new combined stream object. Available options are:

  • maxDataSize
  • pauseStreams

The effect of those options is described below.

combinedStream.pauseStreams = true

Whether to apply back pressure to the underlaying streams. If set to false, the underlaying streams will never be paused. If set to true, the underlaying streams will be paused right after being appended, as well as when delayedStream.pipe() wants to throttle.

combinedStream.maxDataSize = 2 * 1024 * 1024

The maximum amount of bytes (or characters) to buffer for all source streams. If this value is exceeded, combinedStream emits an 'error' event.

combinedStream.dataSize = 0

The amount of bytes (or characters) currently buffered by combinedStream.

combinedStream.append(stream)

Appends the given stream to the combinedStream object. If pauseStreams is set to `true, this stream will also be paused right away.

streams can also be a function that takes one parameter called next. next is a function that must be invoked in order to provide the next stream, see example above.

Regardless of how the stream is appended, combined-stream always attaches an 'error' listener to it, so you don't have to do that manually.

Special case: stream can also be a String or Buffer.

combinedStream.write(data)

You should not call this, combinedStream takes care of piping the appended streams into itself for you.

combinedStream.resume()

Causes combinedStream to start drain the streams it manages. The function is idempotent, and also emits a 'resume' event each time which usually goes to the stream that is currently being drained.

combinedStream.pause();

If combinedStream.pauseStreams is set to false, this does nothing. Otherwise a 'pause' event is emitted, this goes to the stream that is currently being drained, so you can use it to apply back pressure.

combinedStream.end();

Sets combinedStream.writable to false, emits an 'end' event, and removes all streams from the queue.

combinedStream.destroy();

Same as combinedStream.end(), except it emits a 'close' event instead of 'end'.

License

combined-stream is licensed under the MIT license.