glotfile 0.1.0 → 0.2.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/README.md +4 -4
- package/dist/server/cli.js +66 -14
- package/dist/server/server.js +57 -13
- package/dist/ui/assets/{index-DK-AGskd.js → index-DB5e5FME.js} +22 -8
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ to make.
|
|
|
8
8
|
|
|
9
9
|
- **One source of truth** — every string and translation lives in `glotfile.json`, committed alongside your code. Versioning, review, and rollback come from git. For large catalogs, run `glotfile split` to store the catalog as a `glotfile/` directory with one file per locale — so a one-locale change is a one-file `git diff` instead of a multi-megabyte one. The in-app experience is identical.
|
|
10
10
|
- **Local web UI** — run one command, edit in the browser, changes save straight back to the file.
|
|
11
|
-
- **AI translation** — fill in missing languages with Anthropic, OpenAI,
|
|
11
|
+
- **AI translation** — fill in missing languages with Anthropic, OpenAI, AWS Bedrock, or OpenRouter, using per-key context, a glossary, and screenshots.
|
|
12
12
|
- **Export anywhere** — generate Flutter ARB, Laravel PHP, i18next JSON, gettext `.po`, and Apple `.stringsdict` from the same source.
|
|
13
13
|
|
|
14
14
|
---
|
|
@@ -181,9 +181,9 @@ in the project directory. For example:
|
|
|
181
181
|
ANTHROPIC_API_KEY=sk-ant-...
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
(Amazon Nova, Claude, and Meta Llama). For the full setup of
|
|
186
|
-
env vars, model ids, regions, and the optional SDKs to install — see
|
|
184
|
+
Four providers are supported — Anthropic (default), OpenAI, AWS Bedrock
|
|
185
|
+
(Amazon Nova, Claude, and Meta Llama), and OpenRouter. For the full setup of
|
|
186
|
+
each — required env vars, model ids, regions, and the optional SDKs to install — see
|
|
187
187
|
**[docs/ai-providers.md](docs/ai-providers.md)**.
|
|
188
188
|
|
|
189
189
|
What the translator does for you:
|
package/dist/server/cli.js
CHANGED
|
@@ -277,7 +277,7 @@ var init_schema = __esm({
|
|
|
277
277
|
PLURAL_CATEGORIES = ["zero", "one", "two", "few", "many", "other"];
|
|
278
278
|
EXACT_SELECTOR_RE = /^=\d+$/;
|
|
279
279
|
LOCALE_CASES = ["lower-hyphen", "lower-underscore", "bcp47-hyphen", "bcp47-underscore"];
|
|
280
|
-
PROVIDERS = ["anthropic", "openai", "bedrock"];
|
|
280
|
+
PROVIDERS = ["anthropic", "openai", "bedrock", "openrouter"];
|
|
281
281
|
GlotfileError = class extends Error {
|
|
282
282
|
};
|
|
283
283
|
}
|
|
@@ -1768,6 +1768,17 @@ var init_anthropic = __esm({
|
|
|
1768
1768
|
|
|
1769
1769
|
// src/server/ai/openai.ts
|
|
1770
1770
|
import { createRequire } from "module";
|
|
1771
|
+
function loadOpenAIClient(opts) {
|
|
1772
|
+
const require2 = createRequire(import.meta.url);
|
|
1773
|
+
let OpenAICtor;
|
|
1774
|
+
try {
|
|
1775
|
+
const mod = require2("openai");
|
|
1776
|
+
OpenAICtor = mod.OpenAI ?? mod.default ?? mod;
|
|
1777
|
+
} catch {
|
|
1778
|
+
throw new Error("The OpenAI SDK is required for this provider. Install it: npm i openai");
|
|
1779
|
+
}
|
|
1780
|
+
return new OpenAICtor(opts);
|
|
1781
|
+
}
|
|
1771
1782
|
var OpenAIProvider;
|
|
1772
1783
|
var init_openai = __esm({
|
|
1773
1784
|
"src/server/ai/openai.ts"() {
|
|
@@ -1784,15 +1795,7 @@ var init_openai = __esm({
|
|
|
1784
1795
|
if (!process.env.OPENAI_API_KEY) {
|
|
1785
1796
|
throw new Error("OPENAI_API_KEY is not set. AI translation requires it; every other feature works offline.");
|
|
1786
1797
|
}
|
|
1787
|
-
|
|
1788
|
-
let OpenAICtor;
|
|
1789
|
-
try {
|
|
1790
|
-
const mod = require2("openai");
|
|
1791
|
-
OpenAICtor = mod.OpenAI ?? mod.default ?? mod;
|
|
1792
|
-
} catch {
|
|
1793
|
-
throw new Error('Provider "openai" requires the OpenAI SDK. Install it: npm i openai');
|
|
1794
|
-
}
|
|
1795
|
-
this.client = new OpenAICtor({ baseURL: config.endpoint ?? void 0 });
|
|
1798
|
+
this.client = loadOpenAIClient({ baseURL: config.endpoint ?? void 0 });
|
|
1796
1799
|
}
|
|
1797
1800
|
config;
|
|
1798
1801
|
client;
|
|
@@ -1985,6 +1988,34 @@ var init_bedrock = __esm({
|
|
|
1985
1988
|
}
|
|
1986
1989
|
});
|
|
1987
1990
|
|
|
1991
|
+
// src/server/ai/openrouter.ts
|
|
1992
|
+
function openRouterClientOptions(config) {
|
|
1993
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
1994
|
+
if (!apiKey) {
|
|
1995
|
+
throw new Error("OPENROUTER_API_KEY is not set. AI translation requires it; every other feature works offline.");
|
|
1996
|
+
}
|
|
1997
|
+
return {
|
|
1998
|
+
apiKey,
|
|
1999
|
+
baseURL: config.endpoint ?? OPENROUTER_BASE_URL,
|
|
2000
|
+
defaultHeaders: { "HTTP-Referer": OPENROUTER_REFERER, "X-Title": OPENROUTER_TITLE }
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
2003
|
+
var OPENROUTER_BASE_URL, OPENROUTER_REFERER, OPENROUTER_TITLE, OpenRouterProvider;
|
|
2004
|
+
var init_openrouter = __esm({
|
|
2005
|
+
"src/server/ai/openrouter.ts"() {
|
|
2006
|
+
"use strict";
|
|
2007
|
+
init_openai();
|
|
2008
|
+
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
2009
|
+
OPENROUTER_REFERER = "https://www.npmjs.com/package/glotfile";
|
|
2010
|
+
OPENROUTER_TITLE = "glotfile";
|
|
2011
|
+
OpenRouterProvider = class extends OpenAIProvider {
|
|
2012
|
+
constructor(config, client) {
|
|
2013
|
+
super(config, client ?? loadOpenAIClient(openRouterClientOptions(config)));
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
|
|
1988
2019
|
// src/server/ai/index.ts
|
|
1989
2020
|
function makeProvider(config) {
|
|
1990
2021
|
const ai = config.ai;
|
|
@@ -1995,8 +2026,10 @@ function makeProvider(config) {
|
|
|
1995
2026
|
return new OpenAIProvider(ai);
|
|
1996
2027
|
case "bedrock":
|
|
1997
2028
|
return new BedrockProvider(ai);
|
|
2029
|
+
case "openrouter":
|
|
2030
|
+
return new OpenRouterProvider(ai);
|
|
1998
2031
|
default:
|
|
1999
|
-
throw new Error(`Unknown AI provider "${String(ai.provider)}". Supported: anthropic, openai, bedrock.`);
|
|
2032
|
+
throw new Error(`Unknown AI provider "${String(ai.provider)}". Supported: anthropic, openai, bedrock, openrouter.`);
|
|
2000
2033
|
}
|
|
2001
2034
|
}
|
|
2002
2035
|
var init_ai = __esm({
|
|
@@ -2005,6 +2038,7 @@ var init_ai = __esm({
|
|
|
2005
2038
|
init_anthropic();
|
|
2006
2039
|
init_openai();
|
|
2007
2040
|
init_bedrock();
|
|
2041
|
+
init_openrouter();
|
|
2008
2042
|
}
|
|
2009
2043
|
});
|
|
2010
2044
|
|
|
@@ -4189,6 +4223,7 @@ import { serve } from "@hono/node-server";
|
|
|
4189
4223
|
import { fileURLToPath } from "url";
|
|
4190
4224
|
import { dirname as dirname6, join as join7, resolve as resolve8, extname as extname3, sep as sep2 } from "path";
|
|
4191
4225
|
import { readFile, stat } from "fs/promises";
|
|
4226
|
+
import { createServer } from "net";
|
|
4192
4227
|
import open from "open";
|
|
4193
4228
|
async function readFileResponse(absPath) {
|
|
4194
4229
|
try {
|
|
@@ -4236,10 +4271,25 @@ function buildApp(opts) {
|
|
|
4236
4271
|
}
|
|
4237
4272
|
return app;
|
|
4238
4273
|
}
|
|
4239
|
-
function
|
|
4274
|
+
function findAvailablePort(start) {
|
|
4275
|
+
return new Promise((resolveP, reject) => {
|
|
4276
|
+
const probe = createServer();
|
|
4277
|
+
probe.listen(start, "127.0.0.1", () => {
|
|
4278
|
+
probe.close(() => resolveP(start));
|
|
4279
|
+
});
|
|
4280
|
+
probe.on("error", (err) => {
|
|
4281
|
+
if (err.code === "EADDRINUSE") {
|
|
4282
|
+
findAvailablePort(start + 1).then(resolveP, reject);
|
|
4283
|
+
} else {
|
|
4284
|
+
reject(err);
|
|
4285
|
+
}
|
|
4286
|
+
});
|
|
4287
|
+
});
|
|
4288
|
+
}
|
|
4289
|
+
async function startServer(opts) {
|
|
4240
4290
|
const app = buildApp(opts);
|
|
4291
|
+
const port = await findAvailablePort(opts.dev ? DEV_PORT : DEFAULT_PORT);
|
|
4241
4292
|
return new Promise((resolveP) => {
|
|
4242
|
-
const port = opts.dev ? 8787 : 0;
|
|
4243
4293
|
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, (info) => {
|
|
4244
4294
|
const url = `http://127.0.0.1:${info.port}`;
|
|
4245
4295
|
if (opts.open !== false && !opts.dev) void open(url);
|
|
@@ -4261,7 +4311,7 @@ function backgroundScan(statePath) {
|
|
|
4261
4311
|
console.warn("[scan] failed:", err instanceof Error ? err.message : String(err));
|
|
4262
4312
|
});
|
|
4263
4313
|
}
|
|
4264
|
-
var here, DEFAULT_UI_DIR, MIME;
|
|
4314
|
+
var here, DEFAULT_UI_DIR, MIME, DEFAULT_PORT, DEV_PORT;
|
|
4265
4315
|
var init_server = __esm({
|
|
4266
4316
|
"src/server/server.ts"() {
|
|
4267
4317
|
"use strict";
|
|
@@ -4285,6 +4335,8 @@ var init_server = __esm({
|
|
|
4285
4335
|
".woff2": "font/woff2",
|
|
4286
4336
|
".woff": "font/woff"
|
|
4287
4337
|
};
|
|
4338
|
+
DEFAULT_PORT = 3e3;
|
|
4339
|
+
DEV_PORT = 8787;
|
|
4288
4340
|
}
|
|
4289
4341
|
});
|
|
4290
4342
|
|
package/dist/server/server.js
CHANGED
|
@@ -31,6 +31,7 @@ import { serve } from "@hono/node-server";
|
|
|
31
31
|
import { fileURLToPath } from "url";
|
|
32
32
|
import { dirname as dirname6, join as join7, resolve as resolve7, extname as extname3, sep as sep2 } from "path";
|
|
33
33
|
import { readFile, stat } from "fs/promises";
|
|
34
|
+
import { createServer } from "net";
|
|
34
35
|
import open from "open";
|
|
35
36
|
|
|
36
37
|
// src/server/api.ts
|
|
@@ -82,7 +83,7 @@ function isPluralForm(key) {
|
|
|
82
83
|
return PLURAL_CATEGORIES.includes(key) || EXACT_SELECTOR_RE.test(key);
|
|
83
84
|
}
|
|
84
85
|
var LOCALE_CASES = ["lower-hyphen", "lower-underscore", "bcp47-hyphen", "bcp47-underscore"];
|
|
85
|
-
var PROVIDERS = ["anthropic", "openai", "bedrock"];
|
|
86
|
+
var PROVIDERS = ["anthropic", "openai", "bedrock", "openrouter"];
|
|
86
87
|
var GlotfileError = class extends Error {
|
|
87
88
|
};
|
|
88
89
|
function isObject(v) {
|
|
@@ -2516,6 +2517,17 @@ var AnthropicProvider = class {
|
|
|
2516
2517
|
|
|
2517
2518
|
// src/server/ai/openai.ts
|
|
2518
2519
|
import { createRequire } from "module";
|
|
2520
|
+
function loadOpenAIClient(opts) {
|
|
2521
|
+
const require2 = createRequire(import.meta.url);
|
|
2522
|
+
let OpenAICtor;
|
|
2523
|
+
try {
|
|
2524
|
+
const mod = require2("openai");
|
|
2525
|
+
OpenAICtor = mod.OpenAI ?? mod.default ?? mod;
|
|
2526
|
+
} catch {
|
|
2527
|
+
throw new Error("The OpenAI SDK is required for this provider. Install it: npm i openai");
|
|
2528
|
+
}
|
|
2529
|
+
return new OpenAICtor(opts);
|
|
2530
|
+
}
|
|
2519
2531
|
var OpenAIProvider = class {
|
|
2520
2532
|
constructor(config, client) {
|
|
2521
2533
|
this.config = config;
|
|
@@ -2526,15 +2538,7 @@ var OpenAIProvider = class {
|
|
|
2526
2538
|
if (!process.env.OPENAI_API_KEY) {
|
|
2527
2539
|
throw new Error("OPENAI_API_KEY is not set. AI translation requires it; every other feature works offline.");
|
|
2528
2540
|
}
|
|
2529
|
-
|
|
2530
|
-
let OpenAICtor;
|
|
2531
|
-
try {
|
|
2532
|
-
const mod = require2("openai");
|
|
2533
|
-
OpenAICtor = mod.OpenAI ?? mod.default ?? mod;
|
|
2534
|
-
} catch {
|
|
2535
|
-
throw new Error('Provider "openai" requires the OpenAI SDK. Install it: npm i openai');
|
|
2536
|
-
}
|
|
2537
|
-
this.client = new OpenAICtor({ baseURL: config.endpoint ?? void 0 });
|
|
2541
|
+
this.client = loadOpenAIClient({ baseURL: config.endpoint ?? void 0 });
|
|
2538
2542
|
}
|
|
2539
2543
|
config;
|
|
2540
2544
|
client;
|
|
@@ -2717,6 +2721,27 @@ var BedrockProvider = class {
|
|
|
2717
2721
|
}
|
|
2718
2722
|
};
|
|
2719
2723
|
|
|
2724
|
+
// src/server/ai/openrouter.ts
|
|
2725
|
+
var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
2726
|
+
var OPENROUTER_REFERER = "https://www.npmjs.com/package/glotfile";
|
|
2727
|
+
var OPENROUTER_TITLE = "glotfile";
|
|
2728
|
+
function openRouterClientOptions(config) {
|
|
2729
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
2730
|
+
if (!apiKey) {
|
|
2731
|
+
throw new Error("OPENROUTER_API_KEY is not set. AI translation requires it; every other feature works offline.");
|
|
2732
|
+
}
|
|
2733
|
+
return {
|
|
2734
|
+
apiKey,
|
|
2735
|
+
baseURL: config.endpoint ?? OPENROUTER_BASE_URL,
|
|
2736
|
+
defaultHeaders: { "HTTP-Referer": OPENROUTER_REFERER, "X-Title": OPENROUTER_TITLE }
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
var OpenRouterProvider = class extends OpenAIProvider {
|
|
2740
|
+
constructor(config, client) {
|
|
2741
|
+
super(config, client ?? loadOpenAIClient(openRouterClientOptions(config)));
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
|
|
2720
2745
|
// src/server/ai/index.ts
|
|
2721
2746
|
function makeProvider(config) {
|
|
2722
2747
|
const ai = config.ai;
|
|
@@ -2727,8 +2752,10 @@ function makeProvider(config) {
|
|
|
2727
2752
|
return new OpenAIProvider(ai);
|
|
2728
2753
|
case "bedrock":
|
|
2729
2754
|
return new BedrockProvider(ai);
|
|
2755
|
+
case "openrouter":
|
|
2756
|
+
return new OpenRouterProvider(ai);
|
|
2730
2757
|
default:
|
|
2731
|
-
throw new Error(`Unknown AI provider "${String(ai.provider)}". Supported: anthropic, openai, bedrock.`);
|
|
2758
|
+
throw new Error(`Unknown AI provider "${String(ai.provider)}". Supported: anthropic, openai, bedrock, openrouter.`);
|
|
2732
2759
|
}
|
|
2733
2760
|
}
|
|
2734
2761
|
|
|
@@ -3905,10 +3932,27 @@ function buildApp(opts) {
|
|
|
3905
3932
|
}
|
|
3906
3933
|
return app;
|
|
3907
3934
|
}
|
|
3908
|
-
|
|
3935
|
+
var DEFAULT_PORT = 3e3;
|
|
3936
|
+
var DEV_PORT = 8787;
|
|
3937
|
+
function findAvailablePort(start) {
|
|
3938
|
+
return new Promise((resolveP, reject) => {
|
|
3939
|
+
const probe = createServer();
|
|
3940
|
+
probe.listen(start, "127.0.0.1", () => {
|
|
3941
|
+
probe.close(() => resolveP(start));
|
|
3942
|
+
});
|
|
3943
|
+
probe.on("error", (err) => {
|
|
3944
|
+
if (err.code === "EADDRINUSE") {
|
|
3945
|
+
findAvailablePort(start + 1).then(resolveP, reject);
|
|
3946
|
+
} else {
|
|
3947
|
+
reject(err);
|
|
3948
|
+
}
|
|
3949
|
+
});
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
async function startServer(opts) {
|
|
3909
3953
|
const app = buildApp(opts);
|
|
3954
|
+
const port = await findAvailablePort(opts.dev ? DEV_PORT : DEFAULT_PORT);
|
|
3910
3955
|
return new Promise((resolveP) => {
|
|
3911
|
-
const port = opts.dev ? 8787 : 0;
|
|
3912
3956
|
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, (info) => {
|
|
3913
3957
|
const url = `http://127.0.0.1:${info.port}`;
|
|
3914
3958
|
if (opts.open !== false && !opts.dev) void open(url);
|