@voilabs/oilang 0.0.1
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/README.md +167 -0
- package/package.json +31 -0
- package/src/adapters/PostgreSQL.ts +240 -0
- package/src/index.ts +4 -0
- package/src/oilang.ts +182 -0
- package/src/stores/MemoryStore.ts +147 -0
- package/src/stores/RedisStore.ts +205 -0
- package/tsconfig.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# OILang
|
|
2
|
+
|
|
3
|
+
OILang is a robust internationalization (i18n) handling library designed for performance and flexibility. It employs a dual-layer architecture, utilizing a persistent database as the source of truth and a high-performance in-memory or Redis-based store for fast runtime access. This ensures that your application remains responsive while maintaining data integrity and persistence.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Dual-Layer Architecture**: Combines persistent storage with fast caching.
|
|
8
|
+
- **Flexible Storage**: Choose between in-memory storage for simple use cases or Redis for distributed systems.
|
|
9
|
+
- **Customizable Schemas**: Configurable database schema names to fit existing database structures.
|
|
10
|
+
- **Type-Safe**: Built with TypeScript for reliable development.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
To use OILang, you need to install the package and its peer dependencies.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add @voilabs/oilang
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Here is a basic example of how to initialize and use OILang within your application.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { OILang, PostgreSQL, MemoryStore } from "@voilabs/oilang";
|
|
26
|
+
|
|
27
|
+
// Initialize the library
|
|
28
|
+
const oilang = new OILang({
|
|
29
|
+
database: new PostgreSQL(
|
|
30
|
+
"postgresql://user:password@localhost:5432/dbname",
|
|
31
|
+
{
|
|
32
|
+
schemaNames: {
|
|
33
|
+
keys: "i18n_keys",
|
|
34
|
+
locales: "i18n_locales",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
),
|
|
38
|
+
store: new MemoryStore(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
// Connect to database and load data into store
|
|
43
|
+
await oilang.init();
|
|
44
|
+
|
|
45
|
+
// Create a new locale
|
|
46
|
+
await oilang.createLocale("en-US", "English (US)", "English");
|
|
47
|
+
|
|
48
|
+
// Add a translation key
|
|
49
|
+
await oilang.addTranslation("en-US", {
|
|
50
|
+
key: "greeting",
|
|
51
|
+
value: "Hello, World!",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Retrieve translations
|
|
55
|
+
const translations = await oilang.getAllTranslations("en-US");
|
|
56
|
+
console.log(translations);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
main();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API Reference
|
|
63
|
+
|
|
64
|
+
### OILang
|
|
65
|
+
|
|
66
|
+
The main entry point for the library. It orchestrates the interaction between the database adapter and the store.
|
|
67
|
+
|
|
68
|
+
#### Constructor
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
new OILang(config: AdapterConfig)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- `config`: Configuration object containing initialized `database` and `store` instances.
|
|
75
|
+
|
|
76
|
+
#### Methods
|
|
77
|
+
|
|
78
|
+
- **`init(): Promise<void>`**
|
|
79
|
+
Connects to the database and initializes the store by loading existing locales and translations.
|
|
80
|
+
|
|
81
|
+
- **`createLocale(locale: string, nativeName: string, englishName: string): Promise<Result<Locale>>`**
|
|
82
|
+
Creates a new locale in the database and updates the store.
|
|
83
|
+
|
|
84
|
+
- **`deleteLocale(locale: string): Promise<Result<Locale>>`**
|
|
85
|
+
Deletes a locale and its associated translations from both the database and the store.
|
|
86
|
+
|
|
87
|
+
- **`addTranslation(locale: string, config: { key: string; value: string }): Promise<Result<Translation>>`**
|
|
88
|
+
Adds a translation key-value pair for a specific locale.
|
|
89
|
+
|
|
90
|
+
- **`getAllLocales(): Promise<Result<Locale[]>>`**
|
|
91
|
+
Retrieves all available locales from the store.
|
|
92
|
+
|
|
93
|
+
- **`getAllTranslations(locale: string): Promise<Record<string, string>>`**
|
|
94
|
+
Retrieves all translations for a specific locale from the store. Returns a key-value map.
|
|
95
|
+
|
|
96
|
+
### Adapters
|
|
97
|
+
|
|
98
|
+
#### PostgreSQL
|
|
99
|
+
|
|
100
|
+
Handles persistent storage of locales and translations.
|
|
101
|
+
|
|
102
|
+
**Constructor**
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
new PostgreSQL(connectionString: string | ClientConfig, config: { schemaNames: { keys: string; locales: string } })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `connectionString`: PostgreSQL connection string or configuration object.
|
|
109
|
+
- `config.schemaNames`: Custom table names for keys and locales.
|
|
110
|
+
|
|
111
|
+
### Stores
|
|
112
|
+
|
|
113
|
+
Stores handle the runtime access to data. They act as a cache that is synchronized with the database.
|
|
114
|
+
|
|
115
|
+
#### MemoryStore
|
|
116
|
+
|
|
117
|
+
Stores data in the application's memory. Suitable for single-instance applications or development.
|
|
118
|
+
|
|
119
|
+
**Constructor**
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
new MemoryStore();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### RedisStore
|
|
126
|
+
|
|
127
|
+
Stores data in a Redis instance. essential for distributed applications or when data persistence across restarts (without DB reload) is desired.
|
|
128
|
+
|
|
129
|
+
**Constructor**
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
new RedisStore(connectionString: string, options?: { prefix?: string })
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `connectionString`: Redis connection URL (default: `redis://localhost:6379`).
|
|
136
|
+
- `options.prefix`: specific prefix for Redis keys (default: `oilang:`).
|
|
137
|
+
|
|
138
|
+
## Database Schema
|
|
139
|
+
|
|
140
|
+
The PostgreSQL adapter automatically creates the necessary tables if they do not exist.
|
|
141
|
+
|
|
142
|
+
### Locales Table
|
|
143
|
+
|
|
144
|
+
Stores information about supported languages.
|
|
145
|
+
|
|
146
|
+
- `code` (Primary Key): The locale code (e.g., "en-US").
|
|
147
|
+
- `native_name`: Name of the language in its own script.
|
|
148
|
+
- `english_name`: Name of the language in English.
|
|
149
|
+
- `created_at`: Timestamp of creation.
|
|
150
|
+
- `updated_at`: Timestamp of last update.
|
|
151
|
+
|
|
152
|
+
### Keys Table
|
|
153
|
+
|
|
154
|
+
Stores the translation strings.
|
|
155
|
+
|
|
156
|
+
- `id` (Primary Key): Unique identifier.
|
|
157
|
+
- `key`: The translation key (e.g., "homepage.title").
|
|
158
|
+
- `value`: The translated string.
|
|
159
|
+
- `locale_id` (Foreign Key): References `Locales(code)`.
|
|
160
|
+
|
|
161
|
+
## Return Types
|
|
162
|
+
|
|
163
|
+
Most methods return a result object pattern to handle errors gracefully without throwing.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
type Result<T> = { error: null; data: T } | { error: Error; data: null };
|
|
167
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voilabs/oilang",
|
|
3
|
+
"description": "A robust internationalization (i18n) handling library designed for performance and flexibility.",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"prepublishOnly": "npm run build"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/bun": "latest",
|
|
13
|
+
"@types/node": "^25.2.3",
|
|
14
|
+
"@types/pg": "^8.16.0"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"typescript": "^5.9.3"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://oilang.com",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/voilabs/node-oilang.git"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"ioredis": "^5.9.3",
|
|
29
|
+
"pg": "^8.18.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Client, type ClientConfig } from "pg";
|
|
2
|
+
|
|
3
|
+
export class PostgreSQL {
|
|
4
|
+
client: Client;
|
|
5
|
+
schemaNames: { keys: string; locales: string };
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
private config: string | ClientConfig | undefined,
|
|
9
|
+
customizationConfig: {
|
|
10
|
+
schemaNames: { keys: string; locales: string };
|
|
11
|
+
},
|
|
12
|
+
) {
|
|
13
|
+
this.client = new Client(config);
|
|
14
|
+
this.schemaNames = customizationConfig.schemaNames ?? {
|
|
15
|
+
keys: "keys",
|
|
16
|
+
locales: "locales",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async connect() {
|
|
21
|
+
await this.client.connect();
|
|
22
|
+
|
|
23
|
+
const QUERY = [
|
|
24
|
+
`CREATE SCHEMA IF NOT EXISTS ${this.schemaNames.locales};`,
|
|
25
|
+
`CREATE SCHEMA IF NOT EXISTS ${this.schemaNames.keys};`,
|
|
26
|
+
`
|
|
27
|
+
CREATE TABLE IF NOT EXISTS ${this.schemaNames.locales} (
|
|
28
|
+
code VARCHAR(10) PRIMARY KEY,
|
|
29
|
+
native_name VARCHAR(255) NOT NULL,
|
|
30
|
+
english_name VARCHAR(255) NOT NULL
|
|
31
|
+
);
|
|
32
|
+
`,
|
|
33
|
+
`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS ${this.schemaNames.keys} (
|
|
35
|
+
key VARCHAR(255) NOT NULL,
|
|
36
|
+
value TEXT NOT NULL,
|
|
37
|
+
locale_id VARCHAR(10) NOT NULL,
|
|
38
|
+
FOREIGN KEY (locale_id) REFERENCES ${this.schemaNames.locales}(code)
|
|
39
|
+
);
|
|
40
|
+
`,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
await this.client.query(QUERY.join("\n"));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getSchemaNames() {
|
|
47
|
+
return this.schemaNames;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async addLocale(
|
|
51
|
+
locale: string,
|
|
52
|
+
nativeName: string,
|
|
53
|
+
englishName: string,
|
|
54
|
+
): Promise<
|
|
55
|
+
| {
|
|
56
|
+
success: true;
|
|
57
|
+
data: {
|
|
58
|
+
code: string;
|
|
59
|
+
native_name: string;
|
|
60
|
+
english_name: string;
|
|
61
|
+
created_at: string;
|
|
62
|
+
updated_at: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
success: false;
|
|
67
|
+
error: any;
|
|
68
|
+
}
|
|
69
|
+
> {
|
|
70
|
+
try {
|
|
71
|
+
const existingLocale = await this.client.query(
|
|
72
|
+
`SELECT * FROM ${this.schemaNames.locales} WHERE code = $1`,
|
|
73
|
+
[locale],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (existingLocale.rows.length > 0) {
|
|
77
|
+
const error = new Error("Locale already exists");
|
|
78
|
+
(error as any).code = "LOCALE_ALREADY_EXISTS";
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const result = await this.client.query(
|
|
83
|
+
`INSERT INTO ${this.schemaNames.locales} (code, native_name, english_name) VALUES ($1, $2, $3) RETURNING *`,
|
|
84
|
+
[locale, nativeName, englishName],
|
|
85
|
+
);
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
data: result.rows.at(0),
|
|
89
|
+
};
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
error: error,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getAllLocales(): Promise<
|
|
99
|
+
| {
|
|
100
|
+
success: true;
|
|
101
|
+
data: Array<{
|
|
102
|
+
code: string;
|
|
103
|
+
native_name: string;
|
|
104
|
+
english_name: string;
|
|
105
|
+
created_at: string;
|
|
106
|
+
updated_at: string;
|
|
107
|
+
}>;
|
|
108
|
+
}
|
|
109
|
+
| {
|
|
110
|
+
success: false;
|
|
111
|
+
error: any;
|
|
112
|
+
}
|
|
113
|
+
> {
|
|
114
|
+
try {
|
|
115
|
+
const result = await this.client.query(
|
|
116
|
+
`SELECT * FROM ${this.schemaNames.locales}`,
|
|
117
|
+
);
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
data: result.rows,
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: error,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async getAllTranslations(): Promise<
|
|
131
|
+
| {
|
|
132
|
+
success: true;
|
|
133
|
+
data: Array<{
|
|
134
|
+
locale_id: string;
|
|
135
|
+
key: string;
|
|
136
|
+
value: string;
|
|
137
|
+
}>;
|
|
138
|
+
}
|
|
139
|
+
| {
|
|
140
|
+
success: false;
|
|
141
|
+
error: any;
|
|
142
|
+
}
|
|
143
|
+
> {
|
|
144
|
+
try {
|
|
145
|
+
const result = await this.client.query(
|
|
146
|
+
`SELECT * FROM ${this.schemaNames.keys}`,
|
|
147
|
+
);
|
|
148
|
+
return {
|
|
149
|
+
success: true,
|
|
150
|
+
data: result.rows,
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
success: false,
|
|
155
|
+
error: error,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async deleteLocale(locale: string) {
|
|
161
|
+
try {
|
|
162
|
+
await this.client.query(
|
|
163
|
+
`DELETE FROM ${this.schemaNames.locales} WHERE code = $1`,
|
|
164
|
+
[locale],
|
|
165
|
+
);
|
|
166
|
+
await this.client.query(
|
|
167
|
+
`DELETE FROM ${this.schemaNames.keys} WHERE locale_id = $1`,
|
|
168
|
+
[locale],
|
|
169
|
+
);
|
|
170
|
+
return {
|
|
171
|
+
success: true,
|
|
172
|
+
data: {
|
|
173
|
+
code: locale,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: error,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async addTranslation(
|
|
185
|
+
locale: string,
|
|
186
|
+
key: string,
|
|
187
|
+
value: string,
|
|
188
|
+
): Promise<
|
|
189
|
+
| {
|
|
190
|
+
success: false;
|
|
191
|
+
error: Error & { code: string };
|
|
192
|
+
}
|
|
193
|
+
| {
|
|
194
|
+
success: true;
|
|
195
|
+
data: {
|
|
196
|
+
locale_id: string;
|
|
197
|
+
key: string;
|
|
198
|
+
value: string;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
> {
|
|
202
|
+
try {
|
|
203
|
+
const existingTranslation = await this.client.query(
|
|
204
|
+
`SELECT * FROM ${this.schemaNames.keys} WHERE locale_id = $1 AND key = $2`,
|
|
205
|
+
[locale, key],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (existingTranslation.rows.length > 0) {
|
|
209
|
+
const error = new Error("Translation already exists");
|
|
210
|
+
(error as any).code = "TRANSLATION_ALREADY_EXISTS";
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const existingLocale = await this.client.query(
|
|
215
|
+
`SELECT * FROM ${this.schemaNames.locales} WHERE code = $1`,
|
|
216
|
+
[locale],
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (existingLocale.rows.length === 0) {
|
|
220
|
+
const error = new Error("Locale does not exist");
|
|
221
|
+
(error as any).code = "LOCALE_NOT_FOUND";
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = await this.client.query(
|
|
226
|
+
`INSERT INTO ${this.schemaNames.keys} (locale_id, key, value) VALUES ($1, $2, $3) RETURNING *`,
|
|
227
|
+
[locale, key, value],
|
|
228
|
+
);
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
data: result.rows.at(0),
|
|
232
|
+
};
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return {
|
|
235
|
+
success: false,
|
|
236
|
+
error: error as Error & { code: string },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
package/src/index.ts
ADDED
package/src/oilang.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { MemoryStore } from "./stores/MemoryStore";
|
|
2
|
+
import type { PostgreSQL } from "./adapters/PostgreSQL";
|
|
3
|
+
import type { RedisStore } from "./stores/RedisStore";
|
|
4
|
+
|
|
5
|
+
type AdapterConfig = {
|
|
6
|
+
database: InstanceType<typeof PostgreSQL>;
|
|
7
|
+
store: InstanceType<typeof MemoryStore> | InstanceType<typeof RedisStore>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class OILang {
|
|
11
|
+
private database: AdapterConfig["database"];
|
|
12
|
+
private store: AdapterConfig["store"];
|
|
13
|
+
constructor(private config: AdapterConfig) {
|
|
14
|
+
this.database = config.database;
|
|
15
|
+
this.store = config.store;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async init(): Promise<void> {
|
|
19
|
+
await this.database.connect();
|
|
20
|
+
|
|
21
|
+
const [locales, translations] = await Promise.all([
|
|
22
|
+
this.database.getAllLocales(),
|
|
23
|
+
this.database.getAllTranslations(),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
if (locales.success && translations.success) {
|
|
27
|
+
const loadableLocales = locales.data.map((l) => ({
|
|
28
|
+
code: l.code,
|
|
29
|
+
native_name: l.native_name,
|
|
30
|
+
english_name: l.english_name,
|
|
31
|
+
created_at: l.created_at,
|
|
32
|
+
updated_at: l.updated_at,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const loadableTranslations = loadableLocales.reduce(
|
|
36
|
+
(acc, l) => {
|
|
37
|
+
acc[l.code] = translations.data
|
|
38
|
+
.filter((t) => t.locale_id === l.code)
|
|
39
|
+
.reduce(
|
|
40
|
+
(acc, t) => {
|
|
41
|
+
acc[t.key] = t.value;
|
|
42
|
+
return acc;
|
|
43
|
+
},
|
|
44
|
+
{} as Record<string, string>,
|
|
45
|
+
);
|
|
46
|
+
return acc;
|
|
47
|
+
},
|
|
48
|
+
{} as Record<string, Record<string, string>>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await this.store.load(loadableLocales, loadableTranslations);
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error("Failed to load locales or translations");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async createLocale(
|
|
58
|
+
locale: string,
|
|
59
|
+
nativeName: string,
|
|
60
|
+
englishName: string,
|
|
61
|
+
): Promise<
|
|
62
|
+
| {
|
|
63
|
+
error: Error & { code: string };
|
|
64
|
+
data: null;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
error: null;
|
|
68
|
+
data: {
|
|
69
|
+
code: string;
|
|
70
|
+
native_name: string;
|
|
71
|
+
english_name: string;
|
|
72
|
+
created_at: string;
|
|
73
|
+
updated_at: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
> {
|
|
77
|
+
const response = await this.database.addLocale(
|
|
78
|
+
locale,
|
|
79
|
+
nativeName,
|
|
80
|
+
englishName,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
if (response.success) {
|
|
84
|
+
this.store.set({
|
|
85
|
+
seed: "locales",
|
|
86
|
+
locale: response.data,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
error: null,
|
|
91
|
+
data: response.data,
|
|
92
|
+
};
|
|
93
|
+
} else {
|
|
94
|
+
return {
|
|
95
|
+
error: response.error,
|
|
96
|
+
data: null,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async deleteLocale(locale: string) {
|
|
102
|
+
const response = await this.database.deleteLocale(locale);
|
|
103
|
+
|
|
104
|
+
if (response.success) {
|
|
105
|
+
this.store.remove({
|
|
106
|
+
seed: "locales",
|
|
107
|
+
locale,
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
error: null,
|
|
111
|
+
data: response.data,
|
|
112
|
+
};
|
|
113
|
+
} else {
|
|
114
|
+
return {
|
|
115
|
+
error: response.error,
|
|
116
|
+
data: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async getAllLocales() {
|
|
122
|
+
const response = await this.store.getAll({
|
|
123
|
+
seed: "locales",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
error: null,
|
|
128
|
+
data: response,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getAllTranslations(locale: string) {
|
|
133
|
+
const response = await this.store.getAll({
|
|
134
|
+
seed: "translations",
|
|
135
|
+
locale,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async addTranslation(
|
|
142
|
+
locale: string,
|
|
143
|
+
config: { key: string; value: string },
|
|
144
|
+
): Promise<
|
|
145
|
+
| {
|
|
146
|
+
error: Error & { code: string };
|
|
147
|
+
data: null;
|
|
148
|
+
}
|
|
149
|
+
| {
|
|
150
|
+
error: null;
|
|
151
|
+
data: {
|
|
152
|
+
locale_id: string;
|
|
153
|
+
key: string;
|
|
154
|
+
value: string;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
> {
|
|
158
|
+
const response = await this.database.addTranslation(
|
|
159
|
+
locale,
|
|
160
|
+
config.key,
|
|
161
|
+
config.value,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (response.success) {
|
|
165
|
+
this.store.set({
|
|
166
|
+
seed: "translations",
|
|
167
|
+
locale,
|
|
168
|
+
key: config.key,
|
|
169
|
+
value: config.value,
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
error: null,
|
|
173
|
+
data: response.data,
|
|
174
|
+
};
|
|
175
|
+
} else {
|
|
176
|
+
return {
|
|
177
|
+
error: response.error,
|
|
178
|
+
data: null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export class MemoryStore {
|
|
2
|
+
private translations: Record<string, Record<string, string>> = {};
|
|
3
|
+
private locales: Array<{
|
|
4
|
+
code: string;
|
|
5
|
+
native_name: string;
|
|
6
|
+
english_name: string;
|
|
7
|
+
created_at: string;
|
|
8
|
+
updated_at: string;
|
|
9
|
+
}> = [];
|
|
10
|
+
|
|
11
|
+
async load(
|
|
12
|
+
locales: Array<{
|
|
13
|
+
code: string;
|
|
14
|
+
native_name: string;
|
|
15
|
+
english_name: string;
|
|
16
|
+
created_at: string;
|
|
17
|
+
updated_at: string;
|
|
18
|
+
}>,
|
|
19
|
+
translations: Record<string, Record<string, string>>,
|
|
20
|
+
) {
|
|
21
|
+
this.locales = locales;
|
|
22
|
+
this.translations = translations;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async set(
|
|
26
|
+
config:
|
|
27
|
+
| {
|
|
28
|
+
seed: "translations";
|
|
29
|
+
locale: string;
|
|
30
|
+
key: string;
|
|
31
|
+
value: string;
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
seed: "locales";
|
|
35
|
+
locale: {
|
|
36
|
+
code: string;
|
|
37
|
+
native_name: string;
|
|
38
|
+
english_name: string;
|
|
39
|
+
created_at: string;
|
|
40
|
+
updated_at: string;
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
) {
|
|
44
|
+
if (config.seed === "translations") {
|
|
45
|
+
(this.translations[config.locale] as any)[config.key] =
|
|
46
|
+
config.value;
|
|
47
|
+
} else {
|
|
48
|
+
this.locales.push(config.locale);
|
|
49
|
+
this.translations[config.locale.code] = {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async get(
|
|
54
|
+
config:
|
|
55
|
+
| {
|
|
56
|
+
seed: "locales";
|
|
57
|
+
code: string;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
seed: "translations";
|
|
61
|
+
locale: string;
|
|
62
|
+
key: string;
|
|
63
|
+
},
|
|
64
|
+
) {
|
|
65
|
+
if (config.seed === "translations") {
|
|
66
|
+
return this.translations[config.locale]?.[config.key];
|
|
67
|
+
} else {
|
|
68
|
+
return this.locales.find((l) => l.code === config.code);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async getAll(
|
|
73
|
+
config:
|
|
74
|
+
| {
|
|
75
|
+
seed: "locales";
|
|
76
|
+
}
|
|
77
|
+
| {
|
|
78
|
+
seed: "translations";
|
|
79
|
+
locale: string;
|
|
80
|
+
},
|
|
81
|
+
) {
|
|
82
|
+
if (config.seed === "translations") {
|
|
83
|
+
return this.translations[config.locale];
|
|
84
|
+
} else {
|
|
85
|
+
return this.locales;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async remove(
|
|
90
|
+
config:
|
|
91
|
+
| {
|
|
92
|
+
seed: "translations";
|
|
93
|
+
locale: string;
|
|
94
|
+
key: string;
|
|
95
|
+
}
|
|
96
|
+
| {
|
|
97
|
+
seed: "locales";
|
|
98
|
+
locale: string;
|
|
99
|
+
},
|
|
100
|
+
) {
|
|
101
|
+
if (config.seed === "translations") {
|
|
102
|
+
if (
|
|
103
|
+
this.translations[config.locale] &&
|
|
104
|
+
this.translations[config.locale]?.[config.key]
|
|
105
|
+
) {
|
|
106
|
+
delete this.translations[config.locale]?.[config.key];
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
this.locales = this.locales.filter((l) => l.code !== config.locale);
|
|
110
|
+
delete this.translations[config.locale];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async update(
|
|
115
|
+
config:
|
|
116
|
+
| {
|
|
117
|
+
seed: "translations";
|
|
118
|
+
locale: string;
|
|
119
|
+
key: string;
|
|
120
|
+
value: string;
|
|
121
|
+
}
|
|
122
|
+
| {
|
|
123
|
+
seed: "locales";
|
|
124
|
+
code: string;
|
|
125
|
+
locale: {
|
|
126
|
+
native_name: string;
|
|
127
|
+
english_name: string;
|
|
128
|
+
created_at: string;
|
|
129
|
+
updated_at: string;
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
) {
|
|
133
|
+
if (config.seed === "translations") {
|
|
134
|
+
return this.set({
|
|
135
|
+
seed: "translations",
|
|
136
|
+
locale: config.locale,
|
|
137
|
+
key: config.key,
|
|
138
|
+
value: config.value,
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
this.locales = this.locales.map((l) =>
|
|
142
|
+
l.code === config.code ? { ...l, ...config.locale } : l,
|
|
143
|
+
);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import Redis from "ioredis";
|
|
2
|
+
|
|
3
|
+
export class RedisStore {
|
|
4
|
+
private client: Redis;
|
|
5
|
+
|
|
6
|
+
constructor(
|
|
7
|
+
connectionString: string = "redis://localhost:6379",
|
|
8
|
+
private options?: { prefix?: string },
|
|
9
|
+
) {
|
|
10
|
+
this.client = new Redis(connectionString);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private get prefix() {
|
|
14
|
+
return this.options?.prefix ?? "oilang:";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async load(
|
|
18
|
+
locales: Array<{
|
|
19
|
+
code: string;
|
|
20
|
+
native_name: string;
|
|
21
|
+
english_name: string;
|
|
22
|
+
created_at: string;
|
|
23
|
+
updated_at: string;
|
|
24
|
+
}>,
|
|
25
|
+
translations: Record<string, Record<string, string>>,
|
|
26
|
+
) {
|
|
27
|
+
const existingLocales = await this.client.hvals(
|
|
28
|
+
`${this.prefix}locales`,
|
|
29
|
+
);
|
|
30
|
+
const pipeline = this.client.pipeline();
|
|
31
|
+
|
|
32
|
+
if (existingLocales.length > 0) {
|
|
33
|
+
existingLocales.forEach((lStr) => {
|
|
34
|
+
const l = JSON.parse(lStr);
|
|
35
|
+
pipeline.del(`${this.prefix}translations:${l.code}`);
|
|
36
|
+
});
|
|
37
|
+
pipeline.del(`${this.prefix}locales`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (locales.length > 0) {
|
|
41
|
+
const localeMap: Record<string, string> = {};
|
|
42
|
+
for (const locale of locales) {
|
|
43
|
+
localeMap[locale.code] = JSON.stringify(locale);
|
|
44
|
+
}
|
|
45
|
+
pipeline.hset(`${this.prefix}locales`, localeMap);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [locale, trans] of Object.entries(translations)) {
|
|
49
|
+
if (Object.keys(trans).length > 0) {
|
|
50
|
+
pipeline.hset(`${this.prefix}translations:${locale}`, trans);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await pipeline.exec();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async set(
|
|
58
|
+
config:
|
|
59
|
+
| {
|
|
60
|
+
seed: "translations";
|
|
61
|
+
locale: string;
|
|
62
|
+
key: string;
|
|
63
|
+
value: string;
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
seed: "locales";
|
|
67
|
+
locale: {
|
|
68
|
+
code: string;
|
|
69
|
+
native_name: string;
|
|
70
|
+
english_name: string;
|
|
71
|
+
created_at: string;
|
|
72
|
+
updated_at: string;
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
) {
|
|
76
|
+
if (config.seed === "translations") {
|
|
77
|
+
await this.client.hset(
|
|
78
|
+
`${this.prefix}translations:${config.locale}`,
|
|
79
|
+
config.key,
|
|
80
|
+
config.value,
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
await this.client.hset(
|
|
84
|
+
`${this.prefix}locales`,
|
|
85
|
+
config.locale.code,
|
|
86
|
+
JSON.stringify(config.locale),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async get(
|
|
92
|
+
config:
|
|
93
|
+
| {
|
|
94
|
+
seed: "locales";
|
|
95
|
+
code: string;
|
|
96
|
+
}
|
|
97
|
+
| {
|
|
98
|
+
seed: "translations";
|
|
99
|
+
locale: string;
|
|
100
|
+
key: string;
|
|
101
|
+
},
|
|
102
|
+
) {
|
|
103
|
+
if (config.seed === "translations") {
|
|
104
|
+
const val = await this.client.hget(
|
|
105
|
+
`${this.prefix}translations:${config.locale}`,
|
|
106
|
+
config.key,
|
|
107
|
+
);
|
|
108
|
+
return val ?? undefined;
|
|
109
|
+
} else {
|
|
110
|
+
const val = await this.client.hget(
|
|
111
|
+
`${this.prefix}locales`,
|
|
112
|
+
config.code,
|
|
113
|
+
);
|
|
114
|
+
return val ? JSON.parse(val) : undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getAll(
|
|
119
|
+
config:
|
|
120
|
+
| {
|
|
121
|
+
seed: "locales";
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
seed: "translations";
|
|
125
|
+
locale: string;
|
|
126
|
+
},
|
|
127
|
+
) {
|
|
128
|
+
if (config.seed === "translations") {
|
|
129
|
+
return await this.client.hgetall(
|
|
130
|
+
`${this.prefix}translations:${config.locale}`,
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
const locales = await this.client.hvals(`${this.prefix}locales`);
|
|
134
|
+
return locales.map((l: string) => JSON.parse(l));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async remove(
|
|
139
|
+
config:
|
|
140
|
+
| {
|
|
141
|
+
seed: "translations";
|
|
142
|
+
locale: string;
|
|
143
|
+
key: string;
|
|
144
|
+
}
|
|
145
|
+
| {
|
|
146
|
+
seed: "locales";
|
|
147
|
+
locale: string;
|
|
148
|
+
},
|
|
149
|
+
) {
|
|
150
|
+
if (config.seed === "translations") {
|
|
151
|
+
await this.client.hdel(
|
|
152
|
+
`${this.prefix}translations:${config.locale}`,
|
|
153
|
+
config.key,
|
|
154
|
+
);
|
|
155
|
+
} else {
|
|
156
|
+
const pipeline = this.client.pipeline();
|
|
157
|
+
pipeline.hdel(`${this.prefix}locales`, config.locale);
|
|
158
|
+
pipeline.del(`${this.prefix}translations:${config.locale}`);
|
|
159
|
+
await pipeline.exec();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async update(
|
|
164
|
+
config:
|
|
165
|
+
| {
|
|
166
|
+
seed: "translations";
|
|
167
|
+
locale: string;
|
|
168
|
+
key: string;
|
|
169
|
+
value: string;
|
|
170
|
+
}
|
|
171
|
+
| {
|
|
172
|
+
seed: "locales";
|
|
173
|
+
code: string;
|
|
174
|
+
locale: {
|
|
175
|
+
native_name: string;
|
|
176
|
+
english_name: string;
|
|
177
|
+
created_at: string;
|
|
178
|
+
updated_at: string;
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
) {
|
|
182
|
+
if (config.seed === "translations") {
|
|
183
|
+
return this.set({
|
|
184
|
+
seed: "translations",
|
|
185
|
+
locale: config.locale,
|
|
186
|
+
key: config.key,
|
|
187
|
+
value: config.value,
|
|
188
|
+
});
|
|
189
|
+
} else {
|
|
190
|
+
const existingStr = await this.client.hget(
|
|
191
|
+
`${this.prefix}locales`,
|
|
192
|
+
config.code,
|
|
193
|
+
);
|
|
194
|
+
const existing = existingStr ? JSON.parse(existingStr) : {};
|
|
195
|
+
const merged = { ...existing, ...config.locale };
|
|
196
|
+
|
|
197
|
+
await this.client.hset(
|
|
198
|
+
`${this.prefix}locales`,
|
|
199
|
+
config.code,
|
|
200
|
+
JSON.stringify(merged),
|
|
201
|
+
);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "Preserve",
|
|
5
|
+
"strict": true,
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["src"],
|
|
12
|
+
"exclude": ["node_modules", "**/*.test.ts"]
|
|
13
|
+
}
|