@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 +14 -0
- package/Dockerfile +46 -0
- package/README.md +7 -0
- package/belocal-js-sdk-0.1.0.tgz +0 -0
- package/build/run-publish +20 -0
- package/docker-compose.yaml +13 -0
- package/package.json +41 -0
- package/src/core/client.ts +14 -0
- package/src/core/engine.ts +88 -0
- package/src/core/types.ts +19 -0
- package/src/index.ts +6 -0
- package/src/transports/browser.ts +41 -0
- package/src/transports/node.ts +57 -0
- package/tsconfig.base.json +19 -0
- package/tsconfig.json +7 -0
- package/tsup.config.js +45 -0
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
|
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,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
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
|
+
]);
|