@xxanderwp/translate-module 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.
@@ -0,0 +1,64 @@
1
+ name: Publish npm Package
2
+
3
+ on:
4
+ push:
5
+ branches: [main, beta]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Checkout repository
12
+ uses: actions/checkout@v3
13
+
14
+ - name: Setup Node.js
15
+ uses: actions/setup-node@v3
16
+ with:
17
+ node-version: 20
18
+ registry-url: https://registry.npmjs.org/
19
+
20
+ - name: Install dependencies
21
+ run: npm ci
22
+
23
+ - name: Build package
24
+ run: npm run build
25
+
26
+ - name: Check if version exists on npm
27
+ id: check_version
28
+ run: |
29
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
30
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
31
+ echo "Package: $PACKAGE_NAME"
32
+ echo "Version: $PACKAGE_VERSION"
33
+
34
+ # Проверяем, есть ли уже такая версия на npm
35
+ if npm view "$PACKAGE_NAME@$PACKAGE_VERSION" >/dev/null 2>&1; then
36
+ echo "Version $PACKAGE_VERSION already exists on npm"
37
+ echo "exists=true" >> $GITHUB_OUTPUT
38
+ else
39
+ echo "Version $PACKAGE_VERSION is new"
40
+ echo "exists=false" >> $GITHUB_OUTPUT
41
+ fi
42
+
43
+ - name: Publish to npm (latest)
44
+ if: steps.check_version.outputs.exists == 'false' && github.ref == 'refs/heads/main'
45
+ env:
46
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
47
+ run: |
48
+ npm publish --access=public
49
+
50
+ - name: Mark existing version as latest
51
+ if: steps.check_version.outputs.exists == 'true' && github.ref == 'refs/heads/main'
52
+ env:
53
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
54
+ run: |
55
+ PACKAGE_NAME=$(node -p "require('./package.json').name")
56
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
57
+ npm dist-tag add "$PACKAGE_NAME@$PACKAGE_VERSION" latest
58
+
59
+ - name: Publish to npm (beta)
60
+ if: steps.check_version.outputs.exists == 'false' && github.ref == 'refs/heads/beta'
61
+ env:
62
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
63
+ run: |
64
+ npm publish --tag beta --access=public
@@ -0,0 +1,46 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [develop]
6
+ pull_request:
7
+ branches: [main, beta, develop]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+
13
+ strategy:
14
+ matrix:
15
+ node-version: [16.x, 18.x, 20.x, 22.x]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+
20
+ - name: Use Node.js ${{ matrix.node-version }}
21
+ uses: actions/setup-node@v3
22
+ with:
23
+ node-version: ${{ matrix.node-version }}
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Run linter
29
+ run: npm run lint
30
+
31
+ - name: Run tests
32
+ run: npm test
33
+
34
+ - name: Run coverage
35
+ run: npm run test:coverage
36
+
37
+ - name: Upload coverage to Codecov
38
+ uses: codecov/codecov-action@v3
39
+ if: matrix.node-version == '22.x'
40
+ with:
41
+ file: ./coverage/lcov.info
42
+ flags: unittests
43
+ name: codecov-umbrella
44
+
45
+ - name: Build
46
+ run: npm run build
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XXanderWP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ <div align="center">
2
+ <img src="./logo.png" alt="translate-module logo" width="320" />
3
+
4
+ [![npm version](https://img.shields.io/npm/v/@xxanderwp/translate-module?style=flat-square&color=4f46e5)](https://www.npmjs.com/package/@xxanderwp/translate-module)
5
+ [![npm downloads](https://img.shields.io/npm/dw/@xxanderwp/translate-module?style=flat-square&color=7c3aed)](https://www.npmjs.com/package/@xxanderwp/translate-module)
6
+ [![Publish npm Package](https://img.shields.io/github/actions/workflow/status/XXanderWP/LangModule/deploy.yml?branch=main&style=flat-square&label=publish&logo=github)](https://github.com/XXanderWP/LangModule/actions/workflows/deploy.yml)
7
+ [![License: MIT](https://img.shields.io/npm/l/@xxanderwp/translate-module?style=flat-square)](LICENSE)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
+ </div>
10
+
11
+ # @xxanderwp/translate-module
12
+
13
+ A lightweight, type-safe TypeScript module for managing multi-language translations with support for interpolation placeholders.
14
+
15
+ ## Features
16
+
17
+ - Full TypeScript generic type safety — language keys and translation keys are statically inferred
18
+ - Runtime language switching via a simple setter
19
+ - String interpolation with indexed placeholders (`{0}`, `{1}`, ...)
20
+ - Zero runtime dependencies
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @xxanderwp/translate-module
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Basic setup
31
+
32
+ ```ts
33
+ import { LanguageCore } from "@xxanderwp/translate-module";
34
+
35
+ const translations = {
36
+ en: {
37
+ greeting: "Hello, {0}!",
38
+ farewell: "Goodbye!",
39
+ },
40
+ es: {
41
+ greeting: "¡Hola, {0}!",
42
+ farewell: "¡Adiós!",
43
+ },
44
+ };
45
+
46
+ const lang = new LanguageCore(translations, "en");
47
+
48
+ lang.translate("greeting", "Alice"); // "Hello, Alice!"
49
+ ```
50
+
51
+ ### Switching language
52
+
53
+ ```ts
54
+ lang.currentLanguage = "es";
55
+ lang.translate("greeting", "Alice"); // "¡Hola, Alice!"
56
+ ```
57
+
58
+ ### Accessing available languages
59
+
60
+ ```ts
61
+ lang.langKeys; // ["en", "es"]
62
+ ```
63
+
64
+ ## API
65
+
66
+ ### `new LanguageCore(data, defaultLanguage?)`
67
+
68
+ | Parameter | Type | Description |
69
+ | ----------------- | ----------- | ------------------------------------------------------------------------ |
70
+ | `data` | `T` | An object mapping language keys to their translation dictionaries. |
71
+ | `defaultLanguage` | `keyof T` | *(Optional)* The language to activate on construction. Defaults to the first key in `data`. |
72
+
73
+ Throws if `data` is empty or `defaultLanguage` is not a key of `data`.
74
+
75
+ ---
76
+
77
+ ### `translate(key, ...args)`
78
+
79
+ Returns the translated string for `key` in the current language, with `{0}`, `{1}`, ... placeholders replaced by the provided `args`.
80
+
81
+ Returns `null` if the key does not exist or no language is set.
82
+
83
+ ---
84
+
85
+ ### `currentLanguage` *(getter / setter)*
86
+
87
+ Gets or sets the active language key. Setting an unsupported key throws an error.
88
+
89
+ ---
90
+
91
+ ### `langKeys`
92
+
93
+ Returns an array of all available language keys.
94
+
95
+ ---
96
+
97
+ ### `languagesData`
98
+
99
+ Returns the full translations object passed to the constructor.
100
+
101
+ ---
102
+
103
+ ### `currentLanguageData`
104
+
105
+ Returns the translation dictionary for the currently active language, or `null` if none is set.
106
+
107
+ ## Development
108
+
109
+ **Build**
110
+
111
+ ```bash
112
+ npm run build
113
+ ```
114
+
115
+ **Run tests**
116
+
117
+ ```bash
118
+ npm test
119
+ ```
120
+
121
+ **Run tests with coverage**
122
+
123
+ ```bash
124
+ npm run test:coverage
125
+ ```
126
+
127
+ ## License
128
+
129
+ [MIT](LICENSE)
@@ -0,0 +1,11 @@
1
+ export declare class LanguageCore<T extends Record<string, Record<string, string>>, LangKey extends keyof T = keyof T> {
2
+ private readonly _languages_data;
3
+ private _currentLanguage;
4
+ constructor(data: T, defaultLanguage?: keyof T);
5
+ get languagesData(): T;
6
+ get currentLanguage(): LangKey | null;
7
+ set currentLanguage(lang: LangKey);
8
+ get langKeys(): LangKey[];
9
+ get currentLanguageData(): T[LangKey] | null;
10
+ translate<K extends keyof T[LangKey]>(key: K, ...args: (string | number)[]): string | null;
11
+ }
package/dist/index.js ADDED
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LanguageCore = void 0;
4
+ class LanguageCore {
5
+ constructor(data, defaultLanguage) {
6
+ this._currentLanguage = null;
7
+ if (Object.keys(data).length === 0) {
8
+ throw new Error("Languages data cannot be empty.");
9
+ }
10
+ this._languages_data = data;
11
+ if (defaultLanguage) {
12
+ if (!this.langKeys.includes(defaultLanguage)) {
13
+ throw new Error(`Default language ${String(defaultLanguage)} is not supported.`);
14
+ }
15
+ this._currentLanguage = defaultLanguage;
16
+ }
17
+ else {
18
+ this._currentLanguage = Object.keys(data)[0];
19
+ }
20
+ }
21
+ get languagesData() {
22
+ return this._languages_data;
23
+ }
24
+ get currentLanguage() {
25
+ return this._currentLanguage;
26
+ }
27
+ set currentLanguage(lang) {
28
+ if (!this.langKeys.includes(lang)) {
29
+ throw new Error(`Language ${String(lang)} is not supported.`);
30
+ }
31
+ this._currentLanguage = lang;
32
+ }
33
+ get langKeys() {
34
+ return Object.keys(this._languages_data);
35
+ }
36
+ get currentLanguageData() {
37
+ if (!this._currentLanguage)
38
+ return null;
39
+ return this._languages_data[this._currentLanguage];
40
+ }
41
+ translate(key, ...args) {
42
+ if (!this._currentLanguage)
43
+ return null;
44
+ const langData = this._languages_data[this._currentLanguage];
45
+ let res = langData[key];
46
+ args.forEach((arg, index) => {
47
+ res = res.replace(new RegExp(`\\{${index}\\}`, "g"), String(arg));
48
+ });
49
+ return res || null;
50
+ }
51
+ }
52
+ exports.LanguageCore = LanguageCore;
package/jest.config.js ADDED
@@ -0,0 +1,30 @@
1
+ module.exports = {
2
+ preset: 'ts-jest',
3
+ testEnvironment: 'node',
4
+ roots: ['<rootDir>/src', '<rootDir>/tests'],
5
+ testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
6
+ testTimeout: 10000,
7
+ collectCoverageFrom: [
8
+ 'src/**/*.ts',
9
+ '!src/**/*.d.ts',
10
+ '!src/**/*.test.ts',
11
+ '!src/**/*.spec.ts',
12
+ ],
13
+ coverageThreshold: {
14
+ global: {
15
+ branches: 50,
16
+ functions: 50,
17
+ lines: 50,
18
+ statements: 50,
19
+ },
20
+ },
21
+ moduleFileExtensions: ['ts', 'js', 'json'],
22
+ transform: {
23
+ '^.+\\.ts$': [
24
+ 'ts-jest',
25
+ {
26
+ tsconfig: 'tsconfig.test.json',
27
+ },
28
+ ],
29
+ },
30
+ };
package/logo.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@xxanderwp/translate-module",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "directories": {
7
+ "test": "tests"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test:coverage": "jest --coverage",
12
+ "test": "jest"
13
+ },
14
+ "author": "XXanderWP",
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/XXanderWP/LangModule.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/XXanderWP/LangModule/issues"
22
+ },
23
+ "homepage": "https://github.com/XXanderWP/LangModule#readme",
24
+ "devDependencies": {
25
+ "@types/jest": "^30.0.0",
26
+ "jest": "^30.4.2",
27
+ "ts-jest": "^29.4.11",
28
+ "ts-loader": "^9.6.0"
29
+ }
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,64 @@
1
+ export class LanguageCore<
2
+ T extends Record<string, Record<string, string>>,
3
+ LangKey extends keyof T = keyof T
4
+ > {
5
+ private readonly _languages_data: T;
6
+ private _currentLanguage: LangKey | null = null;
7
+
8
+ constructor(data: T, defaultLanguage?: keyof T) {
9
+ if (Object.keys(data).length === 0) {
10
+ throw new Error("Languages data cannot be empty.");
11
+ }
12
+
13
+ this._languages_data = data;
14
+ if(defaultLanguage) {
15
+ if (!this.langKeys.includes(defaultLanguage as LangKey)) {
16
+ throw new Error(`Default language ${String(defaultLanguage)} is not supported.`);
17
+ }
18
+ this._currentLanguage = defaultLanguage as LangKey;
19
+ } else {
20
+ this._currentLanguage = Object.keys(data)[0] as LangKey;
21
+ }
22
+ }
23
+
24
+ get languagesData(): T {
25
+ return this._languages_data;
26
+ }
27
+
28
+ get currentLanguage(): LangKey | null {
29
+ return this._currentLanguage;
30
+ }
31
+
32
+ set currentLanguage(lang: LangKey) {
33
+ if (!this.langKeys.includes(lang)) {
34
+ throw new Error(`Language ${String(lang)} is not supported.`);
35
+ }
36
+ this._currentLanguage = lang;
37
+ }
38
+
39
+ get langKeys(): LangKey[] {
40
+ return Object.keys(this._languages_data) as LangKey[];
41
+ }
42
+
43
+ get currentLanguageData(): T[LangKey] | null {
44
+ if (!this._currentLanguage) return null;
45
+ return this._languages_data[this._currentLanguage];
46
+ }
47
+
48
+ translate<K extends keyof T[LangKey]>(
49
+ key: K,
50
+ ...args: (string | number)[]
51
+ ): string | null {
52
+ if (!this._currentLanguage) return null;
53
+
54
+ const langData = this._languages_data[this._currentLanguage];
55
+ let res = langData[key] as string;
56
+
57
+ args.forEach((arg, index) => {
58
+ res = res.replace(new RegExp(`\\{${index}\\}`, "g"), String(arg));
59
+ });
60
+
61
+ return res || null;
62
+ }
63
+ }
64
+
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect } from "@jest/globals";
2
+ import { LanguageCore } from "../src/index";
3
+
4
+ describe("LangModule", () => {
5
+ it("should initialize with valid data and default language", () => {
6
+ const data = {
7
+ en: { greeting: "Hello" },
8
+ es: { greeting: "Hola" },
9
+ };
10
+ const langModule = new LanguageCore(data, "es");
11
+ expect(langModule.languagesData).toEqual(data);
12
+ expect(langModule.currentLanguage).toBe("es");
13
+ });
14
+
15
+ it("should throw an error if initialized with empty data", () => {
16
+ expect(() => new LanguageCore({})).toThrow("Languages data cannot be empty.");
17
+ });
18
+
19
+ it("should throw an error if default language is not supported", () => {
20
+ const data = {
21
+ en: { greeting: "Hello" },
22
+ es: { greeting: "Hola" },
23
+ };
24
+ // @ts-ignore
25
+ expect(() => new LanguageCore(data, "fr")).toThrow("Default language fr is not supported.");
26
+ });
27
+
28
+ it("should return the correct translation for the current language", () => {
29
+ const data = {
30
+ en: { greeting: "Hello, {0}!" },
31
+ es: { greeting: "¡Hola, {0}!" },
32
+ };
33
+ const langModule = new LanguageCore(data, "en");
34
+ expect(langModule.translate("greeting", "John")).toBe("Hello, John!");
35
+ langModule.currentLanguage = "es";
36
+ expect(langModule.translate("greeting", "John")).toBe("¡Hola, John!");
37
+ });
38
+
39
+ it("should throw an error when setting an unsupported language", () => {
40
+ const data = {
41
+ en: { greeting: "Hello" },
42
+ es: { greeting: "Hola" },
43
+ };
44
+ const langModule = new LanguageCore(data);
45
+ // @ts-ignore
46
+ expect(() => (langModule.currentLanguage = "fr")).toThrow("Language fr is not supported.");
47
+ });
48
+
49
+ it("should return null for translation if current language is not set", () => {
50
+ const data = {
51
+ en: { greeting: "Hello" },
52
+ es: { greeting: "Hola" },
53
+ };
54
+ const langModule = new LanguageCore(data);
55
+ // @ts-ignore
56
+ expect(() => (langModule.currentLanguage = null)).toThrow("Language null is not supported.");
57
+ });
58
+
59
+ it("should return null for translation if key does not exist", () => {
60
+ const data = {
61
+ en: { greeting: "Hello" },
62
+ es: { greeting: "Hola" },
63
+ };
64
+ const langModule = new LanguageCore(data);
65
+ // @ts-ignore
66
+ expect(langModule.translate("farewell")).toBeNull();
67
+ });
68
+
69
+ it("should return the correct language keys", () => {
70
+ const data = {
71
+ en: { greeting: "Hello" },
72
+ es: { greeting: "Hola" },
73
+ };
74
+ const langModule = new LanguageCore(data);
75
+ expect(langModule.langKeys).toEqual(["en", "es"]);
76
+ });
77
+
78
+
79
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "declaration": true,
4
+ "target": "ES2020",
5
+ "module": "CommonJS",
6
+ "lib": ["ES2020", "DOM"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "outDir": "./dist",
12
+ "rootDir": "./src",
13
+ "types": ["node"]
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist-tests",
5
+ "rootDir": ".",
6
+ "noEmit": true,
7
+ "types": ["node", "jest"]
8
+ },
9
+ "include": ["src/**/*", "tests/**/*"],
10
+ "exclude": ["node_modules", "dist"]
11
+ }