@belocal/js-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,14 @@
1
+ stages: [release]
2
+
3
+ release:
4
+ stage: release
5
+ image: docker:27.0.3
6
+ services: [docker:27.0.3-dind]
7
+ variables:
8
+ DOCKER_TLS_CERTDIR: ""
9
+ script:
10
+ - ./build/run-publish
11
+ # rules:
12
+ # - if: '$CI_COMMIT_TAG' # публикуем только по тегам
13
+ tags:
14
+ - production
package/Dockerfile ADDED
@@ -0,0 +1,46 @@
1
+ # ---------- deps ----------
2
+ FROM node:20-alpine AS deps
3
+ WORKDIR /app
4
+ COPY package.json package-lock.json ./
5
+ RUN npm ci
6
+
7
+ # ---------- build ----------
8
+ FROM node:20-alpine AS build
9
+ WORKDIR /app
10
+ COPY --from=deps /app/node_modules ./node_modules
11
+ COPY . .
12
+ RUN npm run build
13
+ RUN test -d dist && ls -la dist
14
+ RUN npm pack --silent
15
+
16
+ # ---------- publisher ----------
17
+ FROM node:20-alpine AS publisher
18
+ WORKDIR /app
19
+ COPY --from=build /app /app
20
+
21
+ ENV NPM_REGISTRY="https://registry.npmjs.org/" \
22
+ NPM_TAG="latest" \
23
+ DRY_RUN="0"
24
+
25
+ CMD ["sh","-ceu", "\
26
+ : \"${NPM_TOKEN:?NPM_TOKEN is required}\"; \
27
+ echo \"• Registry: ${NPM_REGISTRY}\"; \
28
+ echo \"• Tag: ${NPM_TAG}\"; \
29
+ printf '%s\\n' \"//registry.npmjs.org/:_authToken=${NPM_TOKEN}\" > ~/.npmrc; \
30
+ npm config set registry \"${NPM_REGISTRY}\"; \
31
+ PKG_NAME=$(node -p \"require('./package.json').name\"); \
32
+ PKG_VER=$(node -p \"require('./package.json').version\"); \
33
+ ACCESS=\"\"; case \"$PKG_NAME\" in @*/*) ACCESS=\"--access public\";; esac; \
34
+ if [ \"${DRY_RUN}\" = \"1\" ]; then \
35
+ echo 'DRY_RUN=1 → публикация пропущена'; \
36
+ ls -1 *.tgz 2>/dev/null || true; \
37
+ exit 0; \
38
+ fi; \
39
+ if npm view \"$PKG_NAME@$PKG_VER\" version >/dev/null 2>&1; then \
40
+ echo \"• Version $PKG_NAME@$PKG_VER уже опубликована — пропускаю\"; \
41
+ exit 0; \
42
+ fi; \
43
+ echo \"• Publishing $PKG_NAME@$PKG_VER with tag '$NPM_TAG'...\"; \
44
+ npm publish --tag \"$NPM_TAG\" $ACCESS; \
45
+ echo \"✓ Published $PKG_NAME@$PKG_VER\" \
46
+ "]
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @belocal/js-sdk
2
+
3
+ Runtime JS SDK for on-demand translation (browser + node).
4
+
5
+ ## Install
6
+ ```bash
7
+ npm i @belocal/js-sdk
Binary file
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ : "${NPM_TOKEN:?NPM_TOKEN is required}"
5
+ export NPM_TAG="${NPM_TAG:-latest}"
6
+ export NPM_REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org/}"
7
+ export NPM_PROVENANCE="${NPM_PROVENANCE:-true}"
8
+ export DRY_RUN="${DRY_RUN:-0}"
9
+
10
+ echo "Building publisher image..."
11
+ docker compose build publisher
12
+
13
+ echo "Running publisher..."
14
+ docker compose run --rm \
15
+ -e NPM_TOKEN \
16
+ -e NPM_TAG \
17
+ -e NPM_REGISTRY \
18
+ -e NPM_PROVENANCE \
19
+ -e DRY_RUN \
20
+ publisher
@@ -0,0 +1,13 @@
1
+ services:
2
+ publisher:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ target: publisher
7
+ environment:
8
+ NPM_TOKEN: ${NPM_TOKEN}
9
+ NPM_TAG: ${NPM_TAG:-latest}
10
+ NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
11
+ NPM_PROVENANCE: ${NPM_PROVENANCE:-true}
12
+ DRY_RUN: ${DRY_RUN:-0}
13
+ tty: false
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@belocal/js-sdk",
3
+ "version": "0.1.0",
4
+ "description": "BeLocal runtime JS SDK for on-demand translation (browser + node)",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1",
8
+ "build": "tsup",
9
+ "clean": "rimraf dist",
10
+ "dev": "tsup --watch",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+ssh://git@gitlab.com/digitalwinddev/belocal/sdk/js.git"
16
+ },
17
+ "keywords": [],
18
+ "author": "BeLocal",
19
+ "license": "MIT",
20
+ "type": "module",
21
+ "bugs": {
22
+ "url": "https://gitlab.com/digitalwinddev/belocal/sdk/js/issues"
23
+ },
24
+ "homepage": "https://gitlab.com/digitalwinddev/belocal/sdk/js#readme",
25
+ "devDependencies": {
26
+ "@types/node": "^24.5.2",
27
+ "tsup": "^8.5.0",
28
+ "typescript": "^5.9.2"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "browser": "./dist/browser.mjs",
33
+ "node": "./dist/node.mjs",
34
+ "import": "./dist/index.mjs",
35
+ "require": "./dist/index.cjs",
36
+ "default": "./dist/index.mjs"
37
+ }
38
+ },
39
+ "types": "./dist/index.d.ts",
40
+ "sideEffects": false
41
+ }
@@ -0,0 +1,14 @@
1
+ import type { ClientOptions, KV, Lang, Transport } from './types';
2
+
3
+ export class BeLocalClient {
4
+ private transport: Transport;
5
+
6
+ constructor(options: ClientOptions) {
7
+ this.transport = options.transport;
8
+ }
9
+
10
+ /** Переводит строку через переданный transport. Возвращает Promise<string>. */
11
+ async t(text: string, lang: Lang, ctx?: KV): Promise<string> {
12
+ return this.transport({ text, lang, ctx });
13
+ }
14
+ }
@@ -0,0 +1,88 @@
1
+ import type { BelocalEngineOptions, KV, Lang, Transport } from './types';
2
+ import { createBrowserTransport } from '../transports/browser';
3
+ import { createNodeTransport } from '../transports/node';
4
+
5
+ export class BelocalEngine {
6
+ private transport: Transport;
7
+
8
+ constructor(options: BelocalEngineOptions) {
9
+ const {
10
+ apiKey,
11
+ baseUrl = 'https://dynamic.belocal.dev',
12
+ transport = 'auto',
13
+ timeoutMs = 10000,
14
+ retries = 3,
15
+ credentials,
16
+ headers = {},
17
+ agent
18
+ } = options;
19
+
20
+ const authHeaders = {
21
+ 'Authorization': `Bearer ${apiKey}`,
22
+ ...headers
23
+ };
24
+
25
+ this.transport = this.createTransport(transport, {
26
+ baseUrl,
27
+ timeoutMs,
28
+ retries,
29
+ credentials,
30
+ headers: authHeaders,
31
+ agent
32
+ });
33
+ }
34
+
35
+ async t(text: string, lang: Lang, ctx?: KV): Promise<string> {
36
+ return this.transport({ text, lang, ctx });
37
+ }
38
+
39
+ private createTransport(
40
+ transportType: 'browser' | 'node' | 'auto',
41
+ config: {
42
+ baseUrl: string;
43
+ timeoutMs: number;
44
+ retries: number;
45
+ credentials?: RequestCredentials;
46
+ headers: Record<string, string>;
47
+ agent?: unknown;
48
+ }
49
+ ): Transport {
50
+ const actualTransport = transportType === 'auto' ? this.detectEnvironment() : transportType;
51
+
52
+ const path = '/v1/translate';
53
+
54
+ if (actualTransport === 'browser') {
55
+ return createBrowserTransport({
56
+ baseUrl: config.baseUrl,
57
+ path: path,
58
+ credentials: config.credentials,
59
+ headers: config.headers,
60
+ timeoutMs: config.timeoutMs,
61
+ });
62
+ } else {
63
+ return createNodeTransport({
64
+ baseUrl: config.baseUrl,
65
+ path: path,
66
+ headers: config.headers,
67
+ timeoutMs: config.timeoutMs,
68
+ agent: config.agent,
69
+ retries: config.retries,
70
+ });
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Автоматически определяет среду выполнения
76
+ */
77
+ private detectEnvironment(): 'browser' | 'node' {
78
+ if (typeof window !== 'undefined' && typeof window.document !== 'undefined') {
79
+ return 'browser';
80
+ }
81
+
82
+ if (typeof process !== 'undefined' && process.versions && process.versions.node) {
83
+ return 'node';
84
+ }
85
+
86
+ return 'node';
87
+ }
88
+ }
@@ -0,0 +1,19 @@
1
+ export type Lang = string;
2
+ export type KV = Record<string, unknown>;
3
+
4
+ export type Transport = (params: { text: string; lang: Lang; ctx?: KV }) => Promise<string>;
5
+
6
+ export interface ClientOptions {
7
+ transport: Transport;
8
+ }
9
+
10
+ export interface BelocalEngineOptions {
11
+ apiKey: string;
12
+ baseUrl?: string;
13
+ transport?: 'browser' | 'node' | 'auto';
14
+ timeoutMs?: number;
15
+ retries?: number;
16
+ credentials?: RequestCredentials;
17
+ headers?: Record<string, string>;
18
+ agent?: unknown;
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ // Основной API
2
+ export { BelocalEngine } from './core/engine';
3
+ export type { BelocalEngineOptions } from './core/types';
4
+
5
+ // Низкоуровневый API
6
+ export type { Lang, KV } from './core/types';
@@ -0,0 +1,41 @@
1
+ import type { Transport } from '../core/types';
2
+
3
+ export interface BrowserTransportConfig {
4
+ baseUrl: string;
5
+ path?: string;
6
+ credentials?: RequestCredentials;
7
+ headers?: Record<string, string>;
8
+ timeoutMs?: number;
9
+ }
10
+
11
+ export function createBrowserTransport(config: BrowserTransportConfig): Transport {
12
+ return async ({ text, lang, ctx }) => {
13
+ const url = `${config.baseUrl}${config.path}`;
14
+ const controller = new AbortController();
15
+ const timeoutId = config.timeoutMs ? setTimeout(() => controller.abort(), config.timeoutMs) : null;
16
+
17
+ try {
18
+ const response = await fetch(url, {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ ...config.headers,
23
+ },
24
+ body: JSON.stringify({ text, lang, ctx }),
25
+ credentials: config.credentials,
26
+ signal: controller.signal,
27
+ });
28
+
29
+ if (!response.ok) {
30
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
31
+ }
32
+
33
+ const result = await response.json();
34
+ return result.text || text;
35
+ } finally {
36
+ if (timeoutId) {
37
+ clearTimeout(timeoutId);
38
+ }
39
+ }
40
+ };
41
+ }
@@ -0,0 +1,57 @@
1
+ import type { Transport } from '../core/types';
2
+
3
+ export interface NodeTransportConfig {
4
+ baseUrl: string;
5
+ path: string;
6
+ headers?: Record<string, string>;
7
+ timeoutMs?: number;
8
+ agent?: unknown;
9
+ retries?: number;
10
+ }
11
+
12
+ export function createNodeTransport(config: NodeTransportConfig): Transport {
13
+ return async ({ text, lang, ctx }) => {
14
+ const url = `${config.baseUrl}${config.path}`;
15
+ const maxRetries = config.retries || 0;
16
+ let attempt = 0;
17
+
18
+ while (attempt <= maxRetries) {
19
+ try {
20
+ const controller = new AbortController();
21
+ const timeoutId = config.timeoutMs ? setTimeout(() => controller.abort(), config.timeoutMs) : null;
22
+
23
+ try {
24
+ const response = await fetch(url, {
25
+ method: 'POST',
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ ...config.headers,
29
+ },
30
+ body: JSON.stringify({ text, lang, ctx }),
31
+ signal: controller.signal,
32
+ });
33
+
34
+ if (!response.ok) {
35
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
36
+ }
37
+
38
+ const result = await response.json();
39
+ return result.text || text;
40
+ } finally {
41
+ if (timeoutId) {
42
+ clearTimeout(timeoutId);
43
+ }
44
+ }
45
+ } catch (error) {
46
+ attempt++;
47
+ if (attempt > maxRetries) {
48
+ throw error;
49
+ }
50
+ // Exponential backoff
51
+ await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
52
+ }
53
+ }
54
+
55
+ return text; // fallback
56
+ };
57
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "lib": ["ES2020", "DOM"],
6
+ "moduleResolution": "Bundler",
7
+ "strict": true,
8
+ "declaration": true,
9
+ "emitDeclarationOnly": false,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "baseUrl": ".",
16
+ "outDir": "dist",
17
+ "noEmit": true
18
+ }
19
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": false
5
+ },
6
+ "include": ["src"]
7
+ }
package/tsup.config.js ADDED
@@ -0,0 +1,45 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig([
4
+ // Browser ESM build
5
+ {
6
+ entry: ['src/index.ts'],
7
+ format: ['esm'],
8
+ platform: 'browser',
9
+ dts: true,
10
+ sourcemap: true,
11
+ minify: true,
12
+ treeshake: true,
13
+ outExtension() {
14
+ return { js: '.mjs' };
15
+ },
16
+ outDir: 'dist',
17
+ target: 'es2020',
18
+ clean: true,
19
+ env: {
20
+ NODE_ENV: 'production'
21
+ },
22
+ define: {
23
+ __BUILD_TARGET__: '"browser"'
24
+ }
25
+ },
26
+ // Node ESM + (optional) CJS build
27
+ {
28
+ entry: ['src/index.ts'],
29
+ format: ['esm', 'cjs'],
30
+ platform: 'node',
31
+ dts: false, // d.ts уже сгенерены предыдущей сборкой
32
+ sourcemap: true,
33
+ minify: false,
34
+ treeshake: true,
35
+ outDir: 'dist',
36
+ target: 'node18',
37
+ clean: false,
38
+ env: {
39
+ NODE_ENV: 'production'
40
+ },
41
+ define: {
42
+ __BUILD_TARGET__: '"node"'
43
+ }
44
+ }
45
+ ]);