@ecopex/ecopex-framework 1.0.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.
- package/.env +73 -0
- package/README.md +248 -0
- package/bun.lockb +0 -0
- package/config/swagger/admin.js +44 -0
- package/config/swagger/api.js +19 -0
- package/database/migrations/20240000135243_timezones.js +22 -0
- package/database/migrations/20240000135244_countries.js +23 -0
- package/database/migrations/20240000135244_create_admins_table.js +66 -0
- package/database/migrations/20240000135244_currencies.js +21 -0
- package/database/migrations/20240000135244_languages.js +21 -0
- package/database/migrations/20240000135244_taxes.js +10 -0
- package/database/migrations/20240000135245_sites.js +37 -0
- package/database/migrations/20240000135246_payment_methods.js +33 -0
- package/database/migrations/20251016113547_devices.js +37 -0
- package/database/migrations/20251019192600_users.js +62 -0
- package/database/migrations/20251019213551_language_lines.js +35 -0
- package/database/migrations/20251222214131_category_groups.js +18 -0
- package/database/migrations/20251222214619_categories.js +27 -0
- package/database/migrations/20251222214848_brands.js +23 -0
- package/database/migrations/20251222214946_products.js +30 -0
- package/database/migrations/20251222215428_product_images.js +18 -0
- package/database/migrations/20251222215553_options.js +30 -0
- package/database/migrations/20251222215806_variants.js +23 -0
- package/database/migrations/20251222215940_attributes.js +25 -0
- package/database/migrations/20251222220135_discounts.js +15 -0
- package/database/migrations/20251222220253_reviews.js +22 -0
- package/database/migrations/20251222220341_favorites.js +10 -0
- package/database/migrations/20251222220422_search_logs.js +17 -0
- package/database/migrations/20251222220636_orders.js +16 -0
- package/database/migrations/20251222220806_order_items.js +19 -0
- package/database/migrations/20251222221317_order_statuses.js +10 -0
- package/database/migrations/20251222221446_order_payments.js +13 -0
- package/database/migrations/20251222221654_order_addresses.js +23 -0
- package/database/migrations/20251222221807_order_status_logs.js +13 -0
- package/database/seeds/admins.js +37 -0
- package/database/seeds/countries.js +203 -0
- package/database/seeds/currencies.js +165 -0
- package/database/seeds/languages.js +113 -0
- package/database/seeds/timezones.js +149 -0
- package/ecosystem.config.js +26 -0
- package/env.example +73 -0
- package/knexfile.js +3 -0
- package/libraries/2fa.js +22 -0
- package/libraries/aws.js +63 -0
- package/libraries/bcrypt.js +284 -0
- package/libraries/controls.js +113 -0
- package/libraries/date.js +14 -0
- package/libraries/general.js +8 -0
- package/libraries/image.js +57 -0
- package/libraries/jwt.js +178 -0
- package/libraries/knex.js +7 -0
- package/libraries/slug.js +14 -0
- package/libraries/stores.js +22 -0
- package/libraries/upload.js +194 -0
- package/locales/en/messages.json +4 -0
- package/locales/en/sql.json +3 -0
- package/locales/en/validation.json +52 -0
- package/locales/es/validation.json +52 -0
- package/locales/tr/validation.json +59 -0
- package/package.json +75 -0
- package/routes/admin/auto/admins.json +63 -0
- package/routes/admin/auto/devices.json +37 -0
- package/routes/admin/auto/migrations.json +21 -0
- package/routes/admin/auto/users.json +61 -0
- package/routes/admin/middlewares/index.js +87 -0
- package/routes/admin/spec/auth.js +626 -0
- package/routes/admin/spec/users.js +3 -0
- package/routes/auto/handler.js +635 -0
- package/routes/common/auto/countries.json +28 -0
- package/routes/common/auto/currencies.json +26 -0
- package/routes/common/auto/languages.json +26 -0
- package/routes/common/auto/taxes.json +46 -0
- package/routes/common/auto/timezones.json +29 -0
- package/stores/base.js +73 -0
- package/stores/index.js +195 -0
- package/utils/i18n.js +187 -0
- package/utils/jsonRouteLoader.js +587 -0
- package/utils/middleware.js +154 -0
- package/utils/routeLoader.js +227 -0
- package/workers/admin.js +124 -0
- package/workers/api.js +106 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"table": "languages",
|
|
3
|
+
"primary_key": "language_id",
|
|
4
|
+
"store": "languages",
|
|
5
|
+
"schema": {
|
|
6
|
+
"language_id": { "type": "integer", "example": 1 },
|
|
7
|
+
"code": { "type": "string", "default": "", "example": "en" },
|
|
8
|
+
"flag": { "type": "string", "default": "", "example": "🇬🇧" },
|
|
9
|
+
"name": { "type": "string", "default": "", "example": "English" }
|
|
10
|
+
},
|
|
11
|
+
"routes": [
|
|
12
|
+
{
|
|
13
|
+
"action": "list",
|
|
14
|
+
"method": "GET",
|
|
15
|
+
"path": "/languages",
|
|
16
|
+
"searchable_fields": ["code", "name", "flag"],
|
|
17
|
+
"pagination": true,
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"action": "get",
|
|
22
|
+
"method": "GET",
|
|
23
|
+
"path": "/languages/:language_id"
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"table": "taxes",
|
|
3
|
+
"primary_key": "tax_id",
|
|
4
|
+
"schema": {
|
|
5
|
+
"tax_id": { "type": "integer", "example": 1 },
|
|
6
|
+
"name": { "type": "string", "search_type": "like", "creatable": true, "create_required": true, "updatable": true, "example": "VAT" },
|
|
7
|
+
"rate": { "type": "number", "minimum": 0, "maximum": 1, "creatable": true, "create_required": true, "updatable": true, "example": 0.18 }
|
|
8
|
+
},
|
|
9
|
+
"store": "taxes",
|
|
10
|
+
"routes": [
|
|
11
|
+
{
|
|
12
|
+
"action": "list",
|
|
13
|
+
"method": "GET",
|
|
14
|
+
"path": "/taxes",
|
|
15
|
+
"searchable_fields": ["name", "rate"],
|
|
16
|
+
"pagination": true,
|
|
17
|
+
"additionalProperties": false
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"action": "get",
|
|
21
|
+
"method": "GET",
|
|
22
|
+
"path": "/taxes/:tax_id"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"action": "create",
|
|
26
|
+
"method": "POST",
|
|
27
|
+
"path": "/taxes",
|
|
28
|
+
"security": "auth",
|
|
29
|
+
"additionalProperties": false
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"action": "update",
|
|
33
|
+
"method": "PUT",
|
|
34
|
+
"path": "/taxes/:tax_id",
|
|
35
|
+
"security": "auth",
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"action": "delete",
|
|
40
|
+
"method": "DELETE",
|
|
41
|
+
"path": "/taxes/:tax_id",
|
|
42
|
+
"security": "auth",
|
|
43
|
+
"additionalProperties": false
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"table": "timezones",
|
|
3
|
+
"primary_key": "timezone_id",
|
|
4
|
+
"store": "timezones",
|
|
5
|
+
"schema": {
|
|
6
|
+
"timezone_id": { "type": "integer", "search_type": "equal", "example": 1 },
|
|
7
|
+
"name": { "type": "string", "default": "", "search_type": "like", "example": "UTC" },
|
|
8
|
+
"code": { "type": "string", "default": "", "search_type": "like", "example": "UTC+00:00" },
|
|
9
|
+
"offset": { "type": "string", "default": "", "search_type": "like", "example": "+00:00" },
|
|
10
|
+
"gmt": { "type": "string", "default": "", "search_type": "like", "example": "UTC" }
|
|
11
|
+
},
|
|
12
|
+
"routes": [
|
|
13
|
+
{
|
|
14
|
+
"action": "list",
|
|
15
|
+
"method": "GET",
|
|
16
|
+
"path": "/timezones",
|
|
17
|
+
"searchable_fields": ["name", "code", "offset", "gmt"],
|
|
18
|
+
"pagination": true,
|
|
19
|
+
"additionalProperties": false
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"action": "get",
|
|
23
|
+
"method": "GET",
|
|
24
|
+
"path": "/timezones/:timezone_id",
|
|
25
|
+
"security": "auth",
|
|
26
|
+
"additionalProperties": false
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
}
|
package/stores/base.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const knex = require('@root/libraries/knex');
|
|
2
|
+
|
|
3
|
+
module.exports = class StoreModel {
|
|
4
|
+
constructor(name = 'store_model', primary_key = 'id', schema = {}) {
|
|
5
|
+
this.name = name;
|
|
6
|
+
this.knex = knex;
|
|
7
|
+
this.data = new Map();
|
|
8
|
+
this.primary_key = primary_key;
|
|
9
|
+
this.schema = schema;
|
|
10
|
+
this.initialized = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async init() {
|
|
14
|
+
await this.get_data();
|
|
15
|
+
this.initialized = true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async get_data() {
|
|
19
|
+
const data = await this.knex(this.name).select(Object.keys(this.schema));
|
|
20
|
+
data.forEach(item => {
|
|
21
|
+
this.data.set(item[this.primary_key], item);
|
|
22
|
+
});
|
|
23
|
+
return this.data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async get_data_by_primary_key(primary_key) {
|
|
27
|
+
const data = this.data.get(primary_key);
|
|
28
|
+
if(!data) {
|
|
29
|
+
throw new Error('Data not found');
|
|
30
|
+
}
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async create_data(data) {
|
|
35
|
+
const validated_data = this.validate_data(data);
|
|
36
|
+
const check_data = await this.knex(this.name).where(this.primary_key, validated_data[this.primary_key]).first();
|
|
37
|
+
if(check_data) {
|
|
38
|
+
throw new Error('Data already exists');
|
|
39
|
+
}
|
|
40
|
+
const [primary_key] = await this.knex(this.name).insert(validated_data);
|
|
41
|
+
const data_created = await this.knex(this.name).where(this.primary_key, primary_key).first();
|
|
42
|
+
this.data.set(primary_key, data_created);
|
|
43
|
+
return data_created;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async update_data(primary_key, data) {
|
|
47
|
+
const validated_data = this.validate_data(data);
|
|
48
|
+
const check_data = await this.knex(this.name).where(this.primary_key, primary_key).first();
|
|
49
|
+
if(!check_data) {
|
|
50
|
+
throw new Error('Data not found');
|
|
51
|
+
}
|
|
52
|
+
await this.knex(this.name).where(this.primary_key, primary_key).update(validated_data);
|
|
53
|
+
const updated_data = await this.knex(this.name).where(this.primary_key, primary_key).first();
|
|
54
|
+
this.data.set(primary_key, updated_data);
|
|
55
|
+
return updated_data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async delete_data(primary_key) {
|
|
59
|
+
await this.knex(this.name).where(this.primary_key, primary_key).delete();
|
|
60
|
+
this.data.delete(primary_key);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
validate_data(data) {
|
|
65
|
+
const validated_data = {};
|
|
66
|
+
for(const key in this.schema) {
|
|
67
|
+
if(data[key]) {
|
|
68
|
+
validated_data[key] = data[key];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return validated_data;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/stores/index.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const BaseStore = require('./base');
|
|
2
|
+
|
|
3
|
+
const stores = {}
|
|
4
|
+
|
|
5
|
+
const add_model = async (name, primary_key, schema) => {
|
|
6
|
+
if(stores[name]) {
|
|
7
|
+
return stores[name];
|
|
8
|
+
}
|
|
9
|
+
stores[name] = new BaseStore(name, primary_key, schema);
|
|
10
|
+
await stores[name].init();
|
|
11
|
+
return stores[name];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const get_all = (name, query, reply, routeOptions) => {
|
|
15
|
+
|
|
16
|
+
let { page = 1, limit = 10, search, order } = query;
|
|
17
|
+
const offset = (page - 1) * limit;
|
|
18
|
+
|
|
19
|
+
const store_model = stores[name] || false;
|
|
20
|
+
if(!store_model) {
|
|
21
|
+
return reply.status(404).send({
|
|
22
|
+
status: false,
|
|
23
|
+
message: 'Store not found'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let all_data = [...store_model.data.values()];
|
|
28
|
+
|
|
29
|
+
if(search) {
|
|
30
|
+
all_data = all_data.filter(item => {
|
|
31
|
+
return Object.values(item).some(value => value.toString().toLowerCase().includes(search.toLowerCase()));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if(order) {
|
|
36
|
+
const splitOrder = order.split(':');
|
|
37
|
+
const orderBy = splitOrder[0];
|
|
38
|
+
const orderDirection = splitOrder[1];
|
|
39
|
+
all_data = all_data.sort((a, b) => {
|
|
40
|
+
if(orderDirection === 'desc') {
|
|
41
|
+
return b[orderBy] - a[orderBy];
|
|
42
|
+
}
|
|
43
|
+
return a[orderBy] - b[orderBy];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const filters = routeOptions.filters || {};
|
|
48
|
+
if(Object.keys(filters).length > 0) {
|
|
49
|
+
all_data = all_data.filter(item => {
|
|
50
|
+
return Object.keys(filters).every(key => {
|
|
51
|
+
if(query[key] === undefined) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
let filterStatus = true
|
|
55
|
+
if(filters[key].search_type === 'like') {
|
|
56
|
+
filterStatus = item[key].toString().toLowerCase().includes(query[key].toString().toLowerCase());
|
|
57
|
+
} else if(filters[key].search_type === 'equal') {
|
|
58
|
+
filterStatus = item[key] == query[key];
|
|
59
|
+
} else if(filters[key].search_type === 'in') {
|
|
60
|
+
filterStatus = query[key].includes(item[key]);
|
|
61
|
+
} else if(filters[key].search_type === 'not_in') {
|
|
62
|
+
filterStatus = !query[key].includes(item[key]);
|
|
63
|
+
} else if(filters[key].search_type === 'between') {
|
|
64
|
+
filterStatus = item[key] >= query[key][0] && item[key] <= query[key][1];
|
|
65
|
+
} else if(filters[key].search_type === 'not_between') {
|
|
66
|
+
filterStatus = item[key] < query[key][0] || item[key] > query[key][1];
|
|
67
|
+
}
|
|
68
|
+
return filterStatus;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const totalPages = Math.ceil(all_data.length / parseInt(limit));
|
|
74
|
+
if(page > totalPages && page !== 1) {
|
|
75
|
+
return reply.status(404).send({
|
|
76
|
+
status: false,
|
|
77
|
+
message: 'Page not found'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return reply.send({
|
|
82
|
+
status: true,
|
|
83
|
+
data: all_data.slice(offset, offset + limit),
|
|
84
|
+
pagination: {
|
|
85
|
+
page: parseInt(page),
|
|
86
|
+
limit: parseInt(limit),
|
|
87
|
+
total: all_data.length,
|
|
88
|
+
totalPages: totalPages
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const get_by_primary_key = (name, primary_key, reply, routeOptions = {}) => {
|
|
94
|
+
|
|
95
|
+
const store_model = stores[name] || false;
|
|
96
|
+
if(!store_model) {
|
|
97
|
+
return reply.status(404).send({
|
|
98
|
+
status: false,
|
|
99
|
+
message: 'Store not found'
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const data = store_model.data.get(primary_key);
|
|
104
|
+
if(!data) {
|
|
105
|
+
return reply.status(404).send({
|
|
106
|
+
status: false,
|
|
107
|
+
message: 'Data not found'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return reply.send({
|
|
112
|
+
status: true,
|
|
113
|
+
data: data
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const create_item = async (name, data, reply, routeOptions = {}) => {
|
|
118
|
+
|
|
119
|
+
const store_model = stores[name] || false;
|
|
120
|
+
if(!store_model) {
|
|
121
|
+
return reply.status(404).send({
|
|
122
|
+
status: false,
|
|
123
|
+
message: 'Store not found'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const data_created = await store_model.create_data(data);
|
|
129
|
+
return reply.send({
|
|
130
|
+
status: true,
|
|
131
|
+
data: data_created
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return reply.status(500).send({
|
|
135
|
+
status: false,
|
|
136
|
+
message: error.message
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const update_item = async (name, primary_key, data, reply, routeOptions = {}) => {
|
|
142
|
+
const store_model = stores[name] || false;
|
|
143
|
+
if(!store_model) {
|
|
144
|
+
return reply.status(404).send({
|
|
145
|
+
status: false,
|
|
146
|
+
message: 'Store not found'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const data_updated = await store_model.update_data(primary_key, data);
|
|
152
|
+
return reply.send({
|
|
153
|
+
status: true,
|
|
154
|
+
data: data_updated
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return reply.status(500).send({
|
|
158
|
+
status: false,
|
|
159
|
+
message: error.message
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const delete_item = async (name, primary_key, reply, routeOptions = {}) => {
|
|
165
|
+
const store_model = stores[name] || false;
|
|
166
|
+
if(!store_model) {
|
|
167
|
+
return reply.status(404).send({
|
|
168
|
+
status: false,
|
|
169
|
+
message: 'Store not found'
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const data_deleted = await store_model.delete_data(primary_key);
|
|
175
|
+
return reply.send({
|
|
176
|
+
status: true,
|
|
177
|
+
data: data_deleted
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return reply.status(500).send({
|
|
181
|
+
status: false,
|
|
182
|
+
message: error.message
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
add_model,
|
|
189
|
+
get_all,
|
|
190
|
+
get_by_primary_key,
|
|
191
|
+
create_item,
|
|
192
|
+
update_item,
|
|
193
|
+
delete_item,
|
|
194
|
+
stores
|
|
195
|
+
}
|
package/utils/i18n.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Internationalization utility for multi-language support
|
|
6
|
+
*/
|
|
7
|
+
class I18n {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.locales = {};
|
|
10
|
+
this.defaultLocale = 'en';
|
|
11
|
+
this.supportedLocales = ['en', 'tr', 'es'];
|
|
12
|
+
this.loadLocales();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load all locale files
|
|
17
|
+
*/
|
|
18
|
+
loadLocales() {
|
|
19
|
+
const localesPath = path.join(__dirname, '../locales');
|
|
20
|
+
|
|
21
|
+
for (const locale of this.supportedLocales) {
|
|
22
|
+
const localePath = path.join(localesPath, locale);
|
|
23
|
+
if (fs.existsSync(localePath)) {
|
|
24
|
+
this.locales[locale] = {};
|
|
25
|
+
|
|
26
|
+
// Load all JSON files in the locale directory
|
|
27
|
+
const files = fs.readdirSync(localePath);
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
if (file.endsWith('.json')) {
|
|
30
|
+
const filePath = path.join(localePath, file);
|
|
31
|
+
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
32
|
+
const key = file.replace('.json', '');
|
|
33
|
+
this.locales[locale][key] = content;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get translation for a key
|
|
42
|
+
* @param {string} key - Translation key (e.g., 'validation.required')
|
|
43
|
+
* @param {string} locale - Locale code
|
|
44
|
+
* @param {Object} params - Parameters to replace in the translation
|
|
45
|
+
* @returns {string} Translated string
|
|
46
|
+
*/
|
|
47
|
+
t(key, locale = this.defaultLocale, params = {}) {
|
|
48
|
+
const keys = key.split('.');
|
|
49
|
+
let translation = this.locales[locale] || this.locales[this.defaultLocale];
|
|
50
|
+
|
|
51
|
+
// Navigate through nested keys
|
|
52
|
+
for (const k of keys) {
|
|
53
|
+
if (translation && translation[k]) {
|
|
54
|
+
translation = translation[k];
|
|
55
|
+
} else {
|
|
56
|
+
// Fallback to default locale
|
|
57
|
+
translation = this.locales[this.defaultLocale];
|
|
58
|
+
for (const k of keys) {
|
|
59
|
+
if (translation && translation[k]) {
|
|
60
|
+
translation = translation[k];
|
|
61
|
+
} else {
|
|
62
|
+
return key; // Return key if translation not found
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Replace parameters in the translation
|
|
70
|
+
if (typeof translation === 'string') {
|
|
71
|
+
return this.replaceParams(translation, params);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return key;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Replace parameters in translation string
|
|
79
|
+
* @param {string} str - String with parameters
|
|
80
|
+
* @param {Object} params - Parameters to replace
|
|
81
|
+
* @returns {string} String with replaced parameters
|
|
82
|
+
*/
|
|
83
|
+
replaceParams(str, params) {
|
|
84
|
+
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
|
85
|
+
return params[key] !== undefined ? params[key] : match;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detect language from request headers
|
|
91
|
+
* @param {Object} request - Fastify request object
|
|
92
|
+
* @returns {string} Detected locale
|
|
93
|
+
*/
|
|
94
|
+
detectLanguage(request) {
|
|
95
|
+
// Check Accept-Language header
|
|
96
|
+
const acceptLanguage = request.headers['locale'] || request.headers['Accept-Language'];
|
|
97
|
+
if (acceptLanguage) {
|
|
98
|
+
const languages = acceptLanguage.split(',').map(lang => {
|
|
99
|
+
const [locale, quality] = lang.trim().split(';q=');
|
|
100
|
+
return {
|
|
101
|
+
locale: locale.split('-')[0], // Get language part only
|
|
102
|
+
quality: quality ? parseFloat(quality) : 1.0
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Sort by quality and find first supported locale
|
|
107
|
+
languages.sort((a, b) => b.quality - a.quality);
|
|
108
|
+
for (const lang of languages) {
|
|
109
|
+
if (this.supportedLocales.includes(lang.locale)) {
|
|
110
|
+
return lang.locale;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check custom header
|
|
116
|
+
const customLang = request.headers['locale'];
|
|
117
|
+
if (customLang && this.supportedLocales.includes(customLang)) {
|
|
118
|
+
return customLang;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check query parameter
|
|
122
|
+
const queryLang = request.query?.lang;
|
|
123
|
+
if (queryLang && this.supportedLocales.includes(queryLang)) {
|
|
124
|
+
return queryLang;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return this.defaultLocale;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get validation error message
|
|
132
|
+
* @param {Object} error - Ajv validation error
|
|
133
|
+
* @param {string} locale - Locale code
|
|
134
|
+
* @returns {string} Localized error message
|
|
135
|
+
*/
|
|
136
|
+
getValidationErrorMessage(error, locale = this.defaultLocale) {
|
|
137
|
+
const { keyword, instancePath, params, message } = error;
|
|
138
|
+
const field = instancePath.replace('/', '') || 'field';
|
|
139
|
+
|
|
140
|
+
let errorKey = `validation.${keyword}`;
|
|
141
|
+
|
|
142
|
+
// Special cases for common validation errors
|
|
143
|
+
if (keyword === 'minLength') {
|
|
144
|
+
errorKey = 'validation.minLength';
|
|
145
|
+
} else if (keyword === 'maxLength') {
|
|
146
|
+
errorKey = 'validation.maxLength';
|
|
147
|
+
} else if (keyword === 'minimum') {
|
|
148
|
+
errorKey = 'validation.min';
|
|
149
|
+
} else if (keyword === 'maximum') {
|
|
150
|
+
errorKey = 'validation.max';
|
|
151
|
+
} else if (keyword === 'format' && params?.format === 'email') {
|
|
152
|
+
errorKey = 'validation.invalidEmail';
|
|
153
|
+
} else if (keyword === 'enum') {
|
|
154
|
+
errorKey = 'validation.enum';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const errorParams = {
|
|
158
|
+
field,
|
|
159
|
+
...params,
|
|
160
|
+
enum: params?.allowedValues ? params.allowedValues.join(', ') : ''
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return this.t(errorKey, locale, errorParams);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get all supported locales
|
|
168
|
+
* @returns {Array} Array of supported locale codes
|
|
169
|
+
*/
|
|
170
|
+
getSupportedLocales() {
|
|
171
|
+
return this.supportedLocales;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if locale is supported
|
|
176
|
+
* @param {string} locale - Locale code
|
|
177
|
+
* @returns {boolean} True if supported
|
|
178
|
+
*/
|
|
179
|
+
isLocaleSupported(locale) {
|
|
180
|
+
return this.supportedLocales.includes(locale);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create singleton instance
|
|
185
|
+
const i18n = new I18n();
|
|
186
|
+
|
|
187
|
+
module.exports = i18n;
|