@shipi18n/core 2.4.0 → 2.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @shipi18n/core
2
2
 
3
+ ## 2.5.0
4
+
5
+ - New: the `openai` adapter accepts `baseURL`, pointing it at any OpenAI-compatible endpoint —
6
+ Ollama (no key needed, fully offline), Gemini's compatibility endpoint, Groq, Mistral, LM Studio,
7
+ vLLM, corporate gateways. `translateJSON`, `reviewTranslations` and `runSemantic` all take and
8
+ thread it through. When a `baseURL` is set and no key is given, a placeholder key is sent instead
9
+ of failing, since servers like Ollama accept anything.
10
+
3
11
  ## 2.4.0
4
12
 
5
13
  - Fix: `runSemantic` now returns `excluded` — the number of pairs it skipped because the key already
package/README.md CHANGED
@@ -36,6 +36,19 @@ console.log(stats) // { translated, reused, placeholderWarnings }
36
36
  The API key is resolved from `apiKey` or, if omitted, the provider's env var
37
37
  (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY`). Your key is used to call **your** LLM directly.
38
38
 
39
+ The `openai` provider also takes a `baseURL`, which points it at **any OpenAI-compatible
40
+ endpoint** — Ollama (`http://localhost:11434/v1`, no key needed, fully offline), Gemini's
41
+ compatibility endpoint, Groq, Mistral, LM Studio, vLLM, or a corporate gateway:
42
+
43
+ ```js
44
+ await translateJSON({
45
+ content, from: 'en', to: 'es',
46
+ provider: 'openai',
47
+ baseURL: 'http://localhost:11434/v1', // Ollama — no apiKey required
48
+ model: 'llama3.2',
49
+ })
50
+ ```
51
+
39
52
  ## What it does
40
53
 
41
54
  - **Structure-preserving** — flattens/unflattens nested JSON; non-string leaves pass through untouched.
@@ -95,7 +108,7 @@ Format adapters for mobile catalogs are exported too: `parseArbBundle` (Flutter
95
108
 
96
109
  ## API
97
110
 
98
- - `translateJSON({ content, from, to, provider, apiKey?, model?, existing? })` → `{ result, stats }`
111
+ - `translateJSON({ content, from, to, provider, apiKey?, model?, baseURL?, existing? })` → `{ result, stats }`
99
112
  - `translateStrings(texts, { adapter, from, to, batchSize? })` → `string[]`
100
113
  - `flatten(obj)` / `unflatten(flat)`
101
114
  - `extractPlaceholders(str)` / `validatePlaceholders(source, translation)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipi18n/core",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Translation QA for i18n locale files: placeholder and plural validation, key parity, coverage, and an LLM-as-judge semantic review. Also a structure-preserving translation engine — bring your own OpenAI or Anthropic key.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -70,7 +70,13 @@ export function anthropicAdapter(config = {}) {
70
70
  /**
71
71
  * OpenAI adapter. Requires the optional peer dep `openai`.
72
72
  * Key resolved from opts.apiKey or the OPENAI_API_KEY env var (SDK default).
73
- * @param {{ apiKey?: string, model?: string }} [config]
73
+ *
74
+ * `baseURL` points the same adapter at any OpenAI-compatible endpoint —
75
+ * Ollama (http://localhost:11434/v1), Gemini's compatibility endpoint, Groq,
76
+ * Mistral, LM Studio, vLLM, a corporate gateway. Servers like Ollama accept
77
+ * any key, but the SDK refuses to construct without one, so when a baseURL is
78
+ * given and no key is, we pass a placeholder instead of failing the run.
79
+ * @param {{ apiKey?: string, model?: string, baseURL?: string }} [config]
74
80
  * @returns {LLMAdapter}
75
81
  */
76
82
  export function openaiAdapter(config = {}) {
@@ -79,7 +85,17 @@ export function openaiAdapter(config = {}) {
79
85
  const getClient = async () => {
80
86
  if (!clientPromise) {
81
87
  clientPromise = import('openai')
82
- .then(({ default: OpenAI }) => new OpenAI(config.apiKey ? { apiKey: config.apiKey } : {}))
88
+ .then(
89
+ ({ default: OpenAI }) =>
90
+ new OpenAI({
91
+ ...(config.apiKey
92
+ ? { apiKey: config.apiKey }
93
+ : config.baseURL
94
+ ? { apiKey: 'not-needed' } // local/keyless endpoints; real ones will 401
95
+ : {}), // no baseURL: keep SDK default (OPENAI_API_KEY env)
96
+ ...(config.baseURL ? { baseURL: config.baseURL } : {}),
97
+ })
98
+ )
83
99
  .catch(() => {
84
100
  throw missingSdkError('openai', 'openai')
85
101
  })
@@ -106,7 +122,7 @@ export function openaiAdapter(config = {}) {
106
122
  /**
107
123
  * Resolve a provider name (+ config) to an adapter instance.
108
124
  * @param {'anthropic'|'openai'|LLMAdapter} provider
109
- * @param {{ apiKey?: string, model?: string }} [config]
125
+ * @param {{ apiKey?: string, model?: string, baseURL?: string }} [config]
110
126
  * @returns {LLMAdapter}
111
127
  */
112
128
  export function resolveAdapter(provider, config = {}) {
package/src/review.js CHANGED
@@ -118,10 +118,11 @@ export async function reviewTranslations({
118
118
  passes = 3,
119
119
  glossary,
120
120
  cache,
121
+ baseURL,
121
122
  }) {
122
123
  const judgeModel =
123
124
  model ?? (typeof provider === 'string' ? DEFAULT_JUDGE_MODELS[provider] : undefined)
124
- const adapter = resolveAdapter(provider, { apiKey, model: judgeModel })
125
+ const adapter = resolveAdapter(provider, { apiKey, model: judgeModel, baseURL })
125
126
 
126
127
  const src = flatten(source)
127
128
  const tgt = flatten(target)
package/src/translate.js CHANGED
@@ -116,11 +116,12 @@ export async function translateStrings(texts, { adapter, from, to, batchSize = 4
116
116
  * @param {'anthropic'|'openai'|object} params.provider provider name or a custom adapter
117
117
  * @param {string} [params.apiKey] LLM API key (else provider env var)
118
118
  * @param {string} [params.model] override the provider's default model
119
+ * @param {string} [params.baseURL] OpenAI-compatible endpoint override (Ollama, Gemini compat, ...)
119
120
  * @param {Record<string,any>} [params.existing] prior translation → only re-translate changed/new keys (incremental)
120
121
  * @returns {Promise<{ result: object, stats: { translated: number, reused: number, placeholderWarnings: Array }}>}
121
122
  */
122
- export async function translateJSON({ content, from, to, provider, apiKey, model, existing }) {
123
- const adapter = resolveAdapter(provider, { apiKey, model })
123
+ export async function translateJSON({ content, from, to, provider, apiKey, model, baseURL, existing }) {
124
+ const adapter = resolveAdapter(provider, { apiKey, model, baseURL })
124
125
  const sourceFlat = flatten(content)
125
126
  const existingFlat = existing ? flatten(existing) : {}
126
127
 
package/src/tree.js CHANGED
@@ -320,7 +320,7 @@ export function runCheck({ input, source = 'en', ignoreKeys, glossary, locks } =
320
320
  * @returns aggregated judge stats { judged, cached, flagged, calls, parseFailures, excluded }
321
321
  */
322
322
 
323
- export async function runSemantic(result, { provider, apiKey, model, passes, glossary, cache, fail = false }) {
323
+ export async function runSemantic(result, { provider, apiKey, model, baseURL, passes, glossary, cache, fail = false }) {
324
324
  const totals = { judged: 0, cached: 0, flagged: 0, calls: 0, parseFailures: 0, excluded: 0 }
325
325
 
326
326
  for (const l of result.languages) {
@@ -346,7 +346,7 @@ export async function runSemantic(result, { provider, apiKey, model, passes, glo
346
346
 
347
347
  const { findings, stats } = await reviewTranslations({
348
348
  source: src, target: tgt, from: result.source, to: l.lang,
349
- provider, apiKey, model, passes, glossary, cache,
349
+ provider, apiKey, model, baseURL, passes, glossary, cache,
350
350
  })
351
351
  for (const k of Object.keys(stats)) totals[k] = (totals[k] ?? 0) + (stats[k] ?? 0)
352
352