@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 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
@@ -0,0 +1,4 @@
1
+ export { OILang } from "./oilang";
2
+ export { PostgreSQL } from "./adapters/PostgreSQL";
3
+ export { MemoryStore } from "./stores/MemoryStore";
4
+ export { RedisStore } from "./stores/RedisStore";
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
+ }