@x47base/pocketbase-addon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/.dockerignore +9 -0
  2. package/Dockerfile +26 -0
  3. package/FEATURES.md +32 -0
  4. package/LICENSE.md +17 -0
  5. package/MIGRATION.md +49 -0
  6. package/NOTICE.md +5 -0
  7. package/README.md +26 -0
  8. package/adapter.go +31 -0
  9. package/admin/register.go +60 -0
  10. package/admin/register_test.go +37 -0
  11. package/backups/backup_encryption_test.go +82 -0
  12. package/backups/encryption.go +138 -0
  13. package/backups/integration_test.go +51 -0
  14. package/backups/register.go +120 -0
  15. package/backups/restore.go +119 -0
  16. package/backups/s3_test.go +52 -0
  17. package/backups/swap.go +53 -0
  18. package/backups/swap_test.go +75 -0
  19. package/backups/upload.go +52 -0
  20. package/bin/pocketbase-extension.mjs +25 -0
  21. package/cmd/edge/main.go +123 -0
  22. package/cmd/import-fork/main.go +37 -0
  23. package/cmd/loadtest/main.go +61 -0
  24. package/cmd/loadtest/sandbox.go +73 -0
  25. package/cmd/loadtest/sandbox_test.go +20 -0
  26. package/cmd/pocketbase/main.go +78 -0
  27. package/deploy/README.md +148 -0
  28. package/deploy/app/hooks/README.md +2 -0
  29. package/deploy/app/migrations/1789000000_notes.js +16 -0
  30. package/deploy/app/public/README.md +2 -0
  31. package/deploy/compose.secrets.yaml +8 -0
  32. package/deploy/compose.yaml +68 -0
  33. package/deploy/edge.json +13 -0
  34. package/edge/gateway.go +295 -0
  35. package/edge/gateway_test.go +296 -0
  36. package/edge/openapi.json +1 -0
  37. package/edge/policy.go +150 -0
  38. package/features/collection_singleton.go +13 -0
  39. package/features/collection_singleton_test.go +46 -0
  40. package/features/dimensions_test.go +65 -0
  41. package/features/duplicate.go +178 -0
  42. package/features/duplicate_test.go +128 -0
  43. package/features/field_color.go +46 -0
  44. package/features/field_date_only.go +39 -0
  45. package/features/field_json_schema.go +92 -0
  46. package/features/field_scalar_extensions_test.go +66 -0
  47. package/features/files.go +36 -0
  48. package/features/filter_has_any_test.go +81 -0
  49. package/features/generate_test.go +72 -0
  50. package/features/has_any_visibility_test.go +62 -0
  51. package/features/json.go +50 -0
  52. package/features/membership.go +64 -0
  53. package/features/register.go +45 -0
  54. package/features/schema_test.go +58 -0
  55. package/features/ui/main.js +133 -0
  56. package/features/ui/settings.js +31 -0
  57. package/go.mod +54 -0
  58. package/go.sum +159 -0
  59. package/internal/archive/create.go +91 -0
  60. package/internal/archive/create_test.go +125 -0
  61. package/internal/archive/extract.go +99 -0
  62. package/internal/archive/extract_test.go +88 -0
  63. package/jsvm/binds.go +1273 -0
  64. package/jsvm/binds_app_reset_test.go +314 -0
  65. package/jsvm/binds_test.go +1870 -0
  66. package/jsvm/form_data.go +149 -0
  67. package/jsvm/form_data_test.go +225 -0
  68. package/jsvm/internal/types/generated/embed.go +6 -0
  69. package/jsvm/internal/types/generated/types.d.ts +24820 -0
  70. package/jsvm/internal/types/types.go +1408 -0
  71. package/jsvm/jsvm.go +587 -0
  72. package/jsvm/mapper.go +67 -0
  73. package/jsvm/mapper_test.go +42 -0
  74. package/jsvm/pool.go +73 -0
  75. package/jsvm/program_source_test.go +24 -0
  76. package/loadtest/loadtest.go +202 -0
  77. package/loadtest/loadtest_test.go +84 -0
  78. package/localization/README.md +23 -0
  79. package/localization/catalogue.json +483 -0
  80. package/localization/localization.go +94 -0
  81. package/localization/localization_test.go +21 -0
  82. package/mail/register.go +80 -0
  83. package/mail/register_test.go +49 -0
  84. package/mail/resolve.go +91 -0
  85. package/migration/import.go +96 -0
  86. package/migration/import_test.go +58 -0
  87. package/otp/otp.go +56 -0
  88. package/otp/otp_test.go +56 -0
  89. package/package.json +51 -0
  90. package/scripts/check-edge.py +42 -0
  91. package/scripts/check.sh +11 -0
  92. package/scripts/sync-jsvm-types.sh +10 -0
  93. package/security/README.md +94 -0
  94. package/security/assurance_test.go +149 -0
  95. package/security/compatibility_test.go +128 -0
  96. package/security/config.go +78 -0
  97. package/security/dashboard_test.go +103 -0
  98. package/security/management.go +169 -0
  99. package/security/openapi.json +508 -0
  100. package/security/review.go +34 -0
  101. package/security/security.go +503 -0
  102. package/security/security_test.go +146 -0
  103. package/security/state.go +116 -0
  104. package/security/ui/dashboard.css +4 -0
  105. package/security/ui/dashboard.js +83 -0
  106. package/security/ui/main.js +15 -0
  107. package/security/ui/model.js +32 -0
  108. package/security/ui/model.test.mjs +25 -0
  109. package/security/ui/registration.test.mjs +10 -0
  110. package/settings/env_test.go +41 -0
  111. package/settings/openapi.json +193 -0
  112. package/settings/settings.go +155 -0
  113. package/settings/settings_test.go +31 -0
  114. package/watcher/watcher.go +192 -0
  115. package/watcher/watcher_test.go +200 -0
  116. package/web/static.go +99 -0
  117. package/web/static_test.go +48 -0
@@ -0,0 +1,483 @@
1
+ {
2
+ "validation_backup_format": [
3
+ "Invalid encrypted backup format"
4
+ ],
5
+ "validation_backup_name_exists": [
6
+ "Backup file with the specified name already exists.",
7
+ "The backup file name is invalid or already exists."
8
+ ],
9
+ "validation_collection_name_exists": [
10
+ "Collection name must be unique (case insensitive)."
11
+ ],
12
+ "validation_collection_name_id_duplicate": [
13
+ "The name must not match an existing collection id."
14
+ ],
15
+ "validation_collection_name_invalid": [
16
+ "The name shouldn't match with an existing internal table."
17
+ ],
18
+ "validation_collection_system_flag_change": [
19
+ "System collection state cannot be changed."
20
+ ],
21
+ "validation_collection_system_name_change": [
22
+ "System collection name cannot be changed."
23
+ ],
24
+ "validation_collection_system_rule_change": [
25
+ "System collection API rule cannot be changed."
26
+ ],
27
+ "validation_collection_type_change": [
28
+ "Collection type cannot be changed."
29
+ ],
30
+ "validation_collections_import_failure": [
31
+ "Failed to import the collections configuration. Raw error:\n"
32
+ ],
33
+ "validation_color": [
34
+ "Must be a hex color (#RGB, #RGBA, #RRGGBB or #RRGGBBAA)"
35
+ ],
36
+ "validation_conflicting_rate_limit_rule": [
37
+ "Rate limit rule configuration with label {{.label}} already exists or conflicts with another rule."
38
+ ],
39
+ "validation_content_size_limit": [
40
+ "The maximum allowed content size is {{.maxSize}} bytes"
41
+ ],
42
+ "validation_date_invalid": [
43
+ "must be a valid date"
44
+ ],
45
+ "validation_date_only": [
46
+ "Must be a calendar date in YYYY-MM-DD format"
47
+ ],
48
+ "validation_date_out_of_range": [
49
+ "the date is out of range"
50
+ ],
51
+ "validation_duplicated_field_name": [
52
+ "Duplicated or invalid field name {{.fieldName}}"
53
+ ],
54
+ "validation_duplicated_index_definition": [
55
+ "The index definition already exists."
56
+ ],
57
+ "validation_duplicated_index_name": [
58
+ "The index name already exists."
59
+ ],
60
+ "validation_duplicated_provider": [
61
+ "The provider {{.name}} is already registered."
62
+ ],
63
+ "validation_email_domain_not_allowed": [
64
+ "Email domain is not allowed"
65
+ ],
66
+ "validation_empty": [
67
+ "must be blank"
68
+ ],
69
+ "validation_existing_index_name": [
70
+ "The index name is already used in {{.usedTableName}} collection."
71
+ ],
72
+ "validation_field_relation_change": [
73
+ "The relation collection cannot be changed."
74
+ ],
75
+ "validation_field_relation_missing_collection": [
76
+ "The relation collection doesn't exist."
77
+ ],
78
+ "validation_field_type_change": [
79
+ "Field type cannot be changed."
80
+ ],
81
+ "validation_file_size_limit": [
82
+ "Failed to upload {{.file}} - the maximum allowed file size is {{.maxSize}} bytes."
83
+ ],
84
+ "validation_forbidden_pk_character": [
85
+ "'{{.ch}}' is not a valid primary key character."
86
+ ],
87
+ "validation_in_invalid": [
88
+ "must be a valid value"
89
+ ],
90
+ "validation_indexes_not_supported": [
91
+ "View collections don't support indexes."
92
+ ],
93
+ "validation_invalid_auth_id": [
94
+ "Invalid or duplicated auth record id."
95
+ ],
96
+ "validation_invalid_collection": [
97
+ "Missing or invalid collection."
98
+ ],
99
+ "validation_invalid_collection_id": [
100
+ "Missing or invalid collection."
101
+ ],
102
+ "validation_invalid_field_value": [
103
+ "Invalid field value."
104
+ ],
105
+ "validation_invalid_file": [
106
+ "Invalid new files: {{.invalidFiles}}."
107
+ ],
108
+ "validation_invalid_format": [
109
+ "Invalid value format",
110
+ "Invalid value format."
111
+ ],
112
+ "validation_invalid_index_expression": [
113
+ "Invalid CREATE INDEX expression."
114
+ ],
115
+ "validation_invalid_json": [
116
+ "Must be a valid json value"
117
+ ],
118
+ "validation_invalid_latitude": [
119
+ "Latitude must be between -90 and 90 degrees."
120
+ ],
121
+ "validation_invalid_longitude": [
122
+ "Longitude must be between -180 and 180 degrees."
123
+ ],
124
+ "validation_invalid_new_email": [
125
+ "Invalid new email address."
126
+ ],
127
+ "validation_invalid_or_existing_id": [
128
+ "The model id is invalid or already exists."
129
+ ],
130
+ "validation_invalid_password": [
131
+ "Missing or invalid auth record password."
132
+ ],
133
+ "validation_invalid_provider": [
134
+ "Provider with name {{.name}} is missing or is not enabled."
135
+ ],
136
+ "validation_invalid_record": [
137
+ "Missing or invalid record."
138
+ ],
139
+ "validation_invalid_rule": [
140
+ "Invalid rule. Raw error: "
141
+ ],
142
+ "validation_invalid_token": [
143
+ "Invalid or expired token."
144
+ ],
145
+ "validation_invalid_token_claims": [
146
+ "Missing email token claim."
147
+ ],
148
+ "validation_invalid_token_email": [
149
+ "The new email address is invalid."
150
+ ],
151
+ "validation_invalid_token_payload": [
152
+ "Invalid token payload - newEmail must be set."
153
+ ],
154
+ "validation_invalid_unique_system_field_index": [
155
+ "Unique index definition on system fields ({{.fieldName}}) is invalid or missing."
156
+ ],
157
+ "validation_invalid_url": [
158
+ "Must be a valid url"
159
+ ],
160
+ "validation_invalid_value": [
161
+ "Invalid value {{.value}}"
162
+ ],
163
+ "validation_invalid_view_query": [
164
+ "Invalid query - "
165
+ ],
166
+ "validation_invlaid_ip_or_subnet": [
167
+ "invalid IP or CIDR subnet"
168
+ ],
169
+ "validation_is utf_letter_numeric": [
170
+ "must contain unicode letters and numbers only"
171
+ ],
172
+ "validation_is_alpha": [
173
+ "must contain English letters only"
174
+ ],
175
+ "validation_is_alphanumeric": [
176
+ "must contain English letters and digits only"
177
+ ],
178
+ "validation_is_ascii": [
179
+ "must contain ASCII characters only"
180
+ ],
181
+ "validation_is_base64": [
182
+ "must be encoded in Base64"
183
+ ],
184
+ "validation_is_country_code_2_letter": [
185
+ "must be a valid two-letter country code"
186
+ ],
187
+ "validation_is_country_code_3_letter": [
188
+ "must be a valid three-letter country code"
189
+ ],
190
+ "validation_is_credit_card": [
191
+ "must be a valid credit card number"
192
+ ],
193
+ "validation_is_currency_code": [
194
+ "must be valid ISO 4217 currency code"
195
+ ],
196
+ "validation_is_data_uri": [
197
+ "must be a Base64-encoded data URI"
198
+ ],
199
+ "validation_is_dial_string": [
200
+ "must be a valid dial string"
201
+ ],
202
+ "validation_is_digit": [
203
+ "must contain digits only"
204
+ ],
205
+ "validation_is_dns_name": [
206
+ "must be a valid DNS name"
207
+ ],
208
+ "validation_is_domain": [
209
+ "must be a valid domain"
210
+ ],
211
+ "validation_is_e164_number": [
212
+ "must be a valid E164 number"
213
+ ],
214
+ "validation_is_email": [
215
+ "must be a valid email address"
216
+ ],
217
+ "validation_is_float": [
218
+ "must be a floating point number"
219
+ ],
220
+ "validation_is_full_width": [
221
+ "must contain full-width characters"
222
+ ],
223
+ "validation_is_half_width": [
224
+ "must contain half-width characters"
225
+ ],
226
+ "validation_is_hex_color": [
227
+ "must be a valid hexadecimal color code"
228
+ ],
229
+ "validation_is_hexadecimal": [
230
+ "must be a valid hexadecimal number"
231
+ ],
232
+ "validation_is_host": [
233
+ "must be a valid IP address or DNS name"
234
+ ],
235
+ "validation_is_int": [
236
+ "must be an integer number"
237
+ ],
238
+ "validation_is_ip": [
239
+ "must be a valid IP address"
240
+ ],
241
+ "validation_is_ipv4": [
242
+ "must be a valid IPv4 address"
243
+ ],
244
+ "validation_is_ipv6": [
245
+ "must be a valid IPv6 address"
246
+ ],
247
+ "validation_is_isbn": [
248
+ "must be a valid ISBN"
249
+ ],
250
+ "validation_is_isbn_10": [
251
+ "must be a valid ISBN-10"
252
+ ],
253
+ "validation_is_isbn_13": [
254
+ "must be a valid ISBN-13"
255
+ ],
256
+ "validation_is_json": [
257
+ "must be in valid JSON format"
258
+ ],
259
+ "validation_is_latitude": [
260
+ "must be a valid latitude"
261
+ ],
262
+ "validation_is_longitude": [
263
+ "must be a valid longitude"
264
+ ],
265
+ "validation_is_lower_case": [
266
+ "must be in lower case"
267
+ ],
268
+ "validation_is_mac_address": [
269
+ "must be a valid MAC address"
270
+ ],
271
+ "validation_is_mongo_id": [
272
+ "must be a valid hex-encoded MongoDB ObjectId"
273
+ ],
274
+ "validation_is_multibyte": [
275
+ "must contain multibyte characters"
276
+ ],
277
+ "validation_is_port": [
278
+ "must be a valid port number"
279
+ ],
280
+ "validation_is_printable_ascii": [
281
+ "must contain printable ASCII characters only"
282
+ ],
283
+ "validation_is_request_url": [
284
+ "must be a valid request URL"
285
+ ],
286
+ "validation_is_rgb_color": [
287
+ "must be a valid RGB color code"
288
+ ],
289
+ "validation_is_semver": [
290
+ "must be a valid semantic version"
291
+ ],
292
+ "validation_is_ssn": [
293
+ "must be a valid social security number"
294
+ ],
295
+ "validation_is_sub_domain": [
296
+ "must be a valid subdomain"
297
+ ],
298
+ "validation_is_upper_case": [
299
+ "must be in upper case"
300
+ ],
301
+ "validation_is_url": [
302
+ "must be a valid URL"
303
+ ],
304
+ "validation_is_utf_digit": [
305
+ "must contain unicode decimal digits only"
306
+ ],
307
+ "validation_is_utf_letter": [
308
+ "must contain unicode letter characters only"
309
+ ],
310
+ "validation_is_utf_numeric": [
311
+ "must contain unicode number characters only"
312
+ ],
313
+ "validation_is_uuid": [
314
+ "must be a valid UUID"
315
+ ],
316
+ "validation_is_uuid_v3": [
317
+ "must be a valid UUID v3"
318
+ ],
319
+ "validation_is_uuid_v4": [
320
+ "must be a valid UUID v4"
321
+ ],
322
+ "validation_is_uuid_v5": [
323
+ "must be a valid UUID v5"
324
+ ],
325
+ "validation_is_variable_width": [
326
+ "must contain both full-width and half-width characters"
327
+ ],
328
+ "validation_json_schema": [
329
+ "Invalid field schema",
330
+ "Invalid or excessively nested JSON value",
331
+ "Value does not match the field JSON schema"
332
+ ],
333
+ "validation_json_size_limit": [
334
+ "The maximum allowed JSON size is {{.maxSize}} bytes"
335
+ ],
336
+ "validation_key_missing": [
337
+ "required key is missing"
338
+ ],
339
+ "validation_key_unexpected": [
340
+ "key not expected"
341
+ ],
342
+ "validation_key_wrong_type": [
343
+ "key not the correct type"
344
+ ],
345
+ "validation_length_empty_required": [
346
+ "the value must be empty"
347
+ ],
348
+ "validation_length_invalid": [
349
+ "the length must be exactly {{.min}}"
350
+ ],
351
+ "validation_length_out_of_range": [
352
+ "the length must be between {{.min}} and {{.max}}"
353
+ ],
354
+ "validation_length_too_long": [
355
+ "the length must be no more than {{.max}}"
356
+ ],
357
+ "validation_length_too_short": [
358
+ "the length must be no less than {{.min}}"
359
+ ],
360
+ "validation_match_invalid": [
361
+ "must be in a valid format"
362
+ ],
363
+ "validation_max_less_equal_than_required": [
364
+ "must be no greater than {{.threshold}}"
365
+ ],
366
+ "validation_max_less_than_required": [
367
+ "must be less than {{.threshold}}"
368
+ ],
369
+ "validation_max_number_constraint": [
370
+ "Must be less or equal than {{.max}}"
371
+ ],
372
+ "validation_max_text_constraint": [
373
+ "Must be no more than {{.max}} character(s)."
374
+ ],
375
+ "validation_mfa_not_enough_auths": [
376
+ "MFA requires at least 2 auth methods to be enabled."
377
+ ],
378
+ "validation_min_greater_equal_than_required": [
379
+ "must be no less than {{.threshold}}"
380
+ ],
381
+ "validation_min_greater_than_required": [
382
+ "must be greater than {{.threshold}}"
383
+ ],
384
+ "validation_min_number_constraint": [
385
+ "Must be greater or equal than {{.min}}"
386
+ ],
387
+ "validation_min_text_constraint": [
388
+ "Must be at least {{.min}} character(s)."
389
+ ],
390
+ "validation_missing_field": [
391
+ "Invalid or missing field {{.fieldName}}"
392
+ ],
393
+ "validation_missing_provider": [
394
+ "Invalid or missing provider with name {{.name}}."
395
+ ],
396
+ "validation_missing_rel_collection": [
397
+ "Relation connection is missing or cannot be accessed"
398
+ ],
399
+ "validation_missing_rel_records": [
400
+ "Failed to find all relation records with the provided ids"
401
+ ],
402
+ "validation_missing_unique_constraint": [
403
+ "The field {{.fieldName}} doesn't have a UNIQUE constraint."
404
+ ],
405
+ "validation_multiple_of_invalid": [
406
+ "must be multiple of {{.base}}"
407
+ ],
408
+ "validation_nil": [
409
+ "must be blank"
410
+ ],
411
+ "validation_nil_or_not_empty_required": [
412
+ "cannot be blank"
413
+ ],
414
+ "validation_not_a_number": [
415
+ "The submitted number is not properly formatted"
416
+ ],
417
+ "validation_not_enough_values": [
418
+ "Select at least {{.minSelect}}"
419
+ ],
420
+ "validation_not_in_invalid": [
421
+ "must not be in list"
422
+ ],
423
+ "validation_not_nil_required": [
424
+ "is required"
425
+ ],
426
+ "validation_not_unique": [
427
+ "Value must be unique"
428
+ ],
429
+ "validation_only_int_constraint": [
430
+ "Decimal numbers are not allowed"
431
+ ],
432
+ "validation_pk_change": [
433
+ "The record primary key cannot be changed."
434
+ ],
435
+ "validation_pk_invalid": [
436
+ "The record primary key is invalid or already exists."
437
+ ],
438
+ "validation_relation_field_non_view_base_collection": [
439
+ "Only view collections are allowed to have relations to other views."
440
+ ],
441
+ "validation_request_is_request_uri": [
442
+ "must be a valid request URI"
443
+ ],
444
+ "validation_required": [
445
+ "cannot be blank"
446
+ ],
447
+ "validation_reserved_field_name": [
448
+ "The field name is reserved and cannot be used."
449
+ ],
450
+ "validation_reserved_pk": [
451
+ "The primary key '{{.reserved}}' is reserved and cannot be used."
452
+ ],
453
+ "validation_system_field_change": [
454
+ "System fields cannot be deleted or renamed."
455
+ ],
456
+ "validation_token_collection_mismatch": [
457
+ "The provided token is for different auth collection."
458
+ ],
459
+ "validation_token_email_mismatch": [
460
+ "The record email doesn't match with the requested token claims."
461
+ ],
462
+ "validation_too_many_files": [
463
+ "The maximum allowed files is {{.maxSelect}}"
464
+ ],
465
+ "validation_too_many_values": [
466
+ "Select no more than {{.maxSelect}}"
467
+ ],
468
+ "validation_unknown_field": [
469
+ "Unknown or invalid field."
470
+ ],
471
+ "validation_unsupported_composite_pk": [
472
+ "Composite PKs are not supported and the collection must have only 1 PK."
473
+ ],
474
+ "validation_unsupported_value_type": [
475
+ "Invalid or unsupported value type."
476
+ ],
477
+ "validation_url_domain_not_allowed": [
478
+ "Url domain is not allowed"
479
+ ],
480
+ "validation_values_mismatch": [
481
+ "Values don't match."
482
+ ]
483
+ }
@@ -0,0 +1,94 @@
1
+ package localization
2
+
3
+ import (
4
+ "errors"
5
+ "maps"
6
+ "strings"
7
+
8
+ "github.com/pocketbase/pocketbase/apis"
9
+ "github.com/pocketbase/pocketbase/core"
10
+ "github.com/pocketbase/pocketbase/tools/hook"
11
+ "github.com/pocketbase/pocketbase/tools/router"
12
+ )
13
+
14
+ type Catalog map[string]map[string]string
15
+
16
+ func Register(app core.App, catalog Catalog) error {
17
+ if len(catalog) > 32 {
18
+ return errors.New("at most 32 locales supported")
19
+ }
20
+ immutable := Catalog{}
21
+ total := 0
22
+ for locale, messages := range catalog {
23
+ if len(locale) > 35 || locale == "" || locale != strings.ToLower(locale) || len(messages) > 4096 {
24
+ return errors.New("invalid locale catalogue")
25
+ }
26
+ immutable[locale] = maps.Clone(messages)
27
+ for code, message := range messages {
28
+ total += len(code) + len(message)
29
+ if len(code) > 128 || len(message) > 4096 {
30
+ return errors.New("invalid translation size")
31
+ }
32
+ }
33
+ }
34
+ if total > 2<<20 {
35
+ return errors.New("translation catalogue exceeds 2 MiB")
36
+ }
37
+ app.OnServe().Bind(&hook.Handler[*core.ServeEvent]{Id: "pb.localization", Func: func(e *core.ServeEvent) error {
38
+ e.Router.Bind(&hook.Handler[*core.RequestEvent]{Id: "pb.localization.errors", Priority: apis.DefaultPanicRecoverMiddlewarePriority + 1, Func: func(e *core.RequestEvent) error {
39
+ err := e.Next()
40
+ var original *router.ApiError
41
+ if err == nil || e.Written() || !errors.As(err, &original) {
42
+ return err
43
+ }
44
+ locale := strings.ToLower(e.Request.URL.Query().Get("lang"))
45
+ if len(locale) > 35 {
46
+ return err
47
+ }
48
+ for locale != "" {
49
+ if messages, ok := immutable[locale]; ok {
50
+ return Translate(original, messages)
51
+ }
52
+ i := strings.LastIndexByte(locale, '-')
53
+ if i < 0 {
54
+ break
55
+ }
56
+ locale = locale[:i]
57
+ }
58
+ return err
59
+ }})
60
+ return e.Next()
61
+ }})
62
+ return nil
63
+ }
64
+ func Translate(original *router.ApiError, messages map[string]string) *router.ApiError {
65
+ result := *original
66
+ result.Data = translateData(original.Data, messages, 0).(map[string]any)
67
+ return &result
68
+ }
69
+ func translateData(value any, messages map[string]string, depth int) any {
70
+ if depth > 32 {
71
+ return value
72
+ }
73
+ switch v := value.(type) {
74
+ case map[string]any:
75
+ result := make(map[string]any, len(v))
76
+ for k, item := range v {
77
+ result[k] = translateData(item, messages, depth+1)
78
+ }
79
+ if code, ok := v["code"].(string); ok {
80
+ if message, ok := messages[code]; ok {
81
+ result["message"] = message
82
+ }
83
+ }
84
+ return result
85
+ case []any:
86
+ result := make([]any, len(v))
87
+ for i, item := range v {
88
+ result[i] = translateData(item, messages, depth+1)
89
+ }
90
+ return result
91
+ default:
92
+ return value
93
+ }
94
+ }
@@ -0,0 +1,21 @@
1
+ package localization
2
+
3
+ import (
4
+ validation "github.com/pocketbase/ozzo-validation/v4"
5
+ "github.com/pocketbase/pocketbase/tools/router"
6
+ "testing"
7
+ )
8
+
9
+ func TestTranslatePreservesPublicErrors(t *testing.T) {
10
+ original := router.NewBadRequestError("Validation failed", validation.Errors{"name": validation.ErrRequired, "nested": validation.Errors{"age": validation.NewError("age_invalid", "Invalid age")}})
11
+ translated := Translate(original, map[string]string{"validation_required": "Pflichtfeld"})
12
+ if original.Data["name"].(map[string]any)["message"] == "Pflichtfeld" {
13
+ t.Fatal("mutated shared error")
14
+ }
15
+ if translated.Data["name"].(map[string]any)["message"] != "Pflichtfeld" || translated.Status != original.Status || translated.Message != original.Message {
16
+ t.Fatal("translation changed response contract")
17
+ }
18
+ if translated.Data["nested"].(map[string]any)["age"].(map[string]any)["code"] != "age_invalid" {
19
+ t.Fatal("lost code")
20
+ }
21
+ }
@@ -0,0 +1,80 @@
1
+ package mail
2
+
3
+ import (
4
+ "github.com/pocketbase/pocketbase/core"
5
+ "github.com/pocketbase/pocketbase/tools/hook"
6
+ "github.com/spink-dev/pocketbase-extension/settings"
7
+ "strings"
8
+ )
9
+
10
+ type localeApp struct {
11
+ core.App
12
+ locale string
13
+ }
14
+
15
+ func WithLocale(app core.App, locale string) core.App { return &localeApp{app, locale} }
16
+ func (a *localeApp) MailLocale() string { return a.locale }
17
+ func Register(app core.App) {
18
+ app.OnServe().BindFunc(func(e *core.ServeEvent) error {
19
+ e.Router.BindFunc(func(r *core.RequestEvent) error {
20
+ lang := r.Request.URL.Query().Get("lang")
21
+ if len(lang) <= 35 && lang != "" {
22
+ r.App = WithLocale(r.App, strings.ToLower(strings.TrimSpace(lang)))
23
+ }
24
+ return r.Next()
25
+ })
26
+ return e.Next()
27
+ })
28
+ bind := func(h *hook.TaggedHook[*core.MailerRecordEvent], kind string) {
29
+ h.BindFunc(func(e *core.MailerRecordEvent) error {
30
+ a, ok := e.App.(interface{ MailLocale() string })
31
+ if !ok {
32
+ return e.Next()
33
+ }
34
+ c, err := settings.Load(e.App)
35
+ if err != nil {
36
+ return err
37
+ }
38
+ kinds := c.EmailLocales[e.Record.Collection().Id]
39
+ if kinds == nil {
40
+ kinds = c.EmailLocales[e.Record.Collection().Name]
41
+ }
42
+ variants := kinds[kind]
43
+ locale := a.MailLocale()
44
+ var v settings.Template
45
+ found := false
46
+ for locale != "" {
47
+ v, found = variants[locale]
48
+ if found {
49
+ break
50
+ }
51
+ i := strings.LastIndexByte(locale, '-')
52
+ if i < 0 {
53
+ break
54
+ }
55
+ locale = locale[:i]
56
+ }
57
+ if !found {
58
+ return e.Next()
59
+ }
60
+ placeholders := map[string]any{}
61
+ for meta, name := range map[string]string{"token": core.EmailPlaceholderToken, "otpId": core.EmailPlaceholderOTPId, "password": core.EmailPlaceholderOTP, "info": core.EmailPlaceholderAlertInfo} {
62
+ if value, ok := e.Meta[meta]; ok {
63
+ placeholders[name] = value
64
+ }
65
+ }
66
+ subject, body, err := resolveEmailTemplate(e.App, e.Record, core.EmailTemplate{Subject: v.Subject, Body: v.Body}, placeholders)
67
+ if err != nil {
68
+ return err
69
+ }
70
+ e.Message.Subject = subject
71
+ e.Message.HTML = body
72
+ return e.Next()
73
+ })
74
+ }
75
+ bind(app.OnMailerRecordOTPSend(), "otp")
76
+ bind(app.OnMailerRecordVerificationSend(), "verification")
77
+ bind(app.OnMailerRecordPasswordResetSend(), "passwordReset")
78
+ bind(app.OnMailerRecordEmailChangeSend(), "emailChange")
79
+ bind(app.OnMailerRecordAuthAlertSend(), "authAlert")
80
+ }