glotfile 0.8.7 → 0.8.8

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 CHANGED
@@ -10,7 +10,7 @@ to make.
10
10
 
11
11
  - **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.
12
12
  - **Local web UI** — run one command, edit in the browser, changes save straight back to the file.
13
- - **AI translation** — fill in missing languages with Anthropic, OpenAI, AWS Bedrock, OpenRouter, or a local Ollama model, using per-key context, a glossary, and screenshots.
13
+ - **AI translation** — fill in missing languages with Anthropic, OpenAI, AWS Bedrock, OpenRouter, Claude Code, or a local Ollama model, using per-key context, a glossary, and screenshots.
14
14
  - **Export anywhere** — generate Flutter ARB, Laravel PHP, i18next JSON, gettext `.po`, and Apple `.stringsdict` from the same source.
15
15
 
16
16
  ---
@@ -181,11 +181,11 @@ in the project directory. For example:
181
181
  ANTHROPIC_API_KEY=sk-ant-...
182
182
  ```
183
183
 
184
- Five providers are supported — Anthropic (default), OpenAI, AWS Bedrock
185
- (Amazon Nova, Claude, and Meta Llama), OpenRouter, and Ollama (local,
186
- no API key needed). For the full setup of
184
+ Six providers are supported — Anthropic (default), OpenAI, AWS Bedrock
185
+ (Amazon Nova, Claude, and Meta Llama), OpenRouter, Claude Code, and Ollama
186
+ (local, no API key needed). For the full setup of
187
187
  each — required env vars, model ids, regions, and the optional SDKs to install — see
188
- **[docs/ai-providers.md](docs/ai-providers.md)**.
188
+ **[AI Providers](https://glotfile.dev/docs/ai-translation/ai-providers/)**.
189
189
 
190
190
  What the translator does for you:
191
191
 
@@ -999,6 +999,9 @@ function extractLiterals(value) {
999
999
  });
1000
1000
  return out;
1001
1001
  }
1002
+ function quotedLiterals(value) {
1003
+ return extractLiterals(value).map((content) => `'${content}'`);
1004
+ }
1002
1005
  function toLaravel(value) {
1003
1006
  if (isIcuPluralOrSelect(value)) return value;
1004
1007
  return withLiterals(value, (gap) => gap.replace(/\{(\w+)\}/g, ":$1"), (lit) => lit);
@@ -2070,10 +2073,11 @@ function buildSystemPrompt(hasPluralItems) {
2070
2073
  "You are a professional software localization engine for a UI string catalog.",
2071
2074
  "Your goal: translate each source UI string into its target locale accurately and idiomatically, as a native speaker would phrase it in a real app interface.",
2072
2075
  "",
2073
- "You are given, per item: the key path, the source text, optional human context, the target locale, an optional max length, the list of interpolation placeholders, and any relevant glossary entries. Some items also include a screenshot image showing where the string appears in the UI \u2014 use it to disambiguate meaning, tone, and length.",
2076
+ "You are given, per item: the key path, the source text, optional human context, the target locale, an optional max length, the list of interpolation placeholders, an optional `literals` list, and any relevant glossary entries. Some items also include a screenshot image showing where the string appears in the UI \u2014 use it to disambiguate meaning, tone, and length.",
2074
2077
  "",
2075
2078
  "Hard rules:",
2076
2079
  "- Preserve every interpolation placeholder EXACTLY as written: {name}, {{count}}, %s, %d, :name. Never translate, rename, reorder, or remove them.",
2080
+ "- Reproduce every entry of the item's `literals` array EXACTLY, including its surrounding apostrophes (e.g. '{{visitor}}', '{name}'). These are app-managed literal tokens, not prose: translate the words around them, but never translate, rename, unquote, or drop them. The apostrophes are required \u2014 a result with bare {{visitor}} instead of '{{visitor}}' is wrong.",
2077
2081
  "- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
2078
2082
  "- Glossary: a term marked do-not-translate MUST appear unchanged in the translation. A term with a forced translation for the target locale MUST use that exact translation.",
2079
2083
  "- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
@@ -2102,6 +2106,7 @@ function buildBatchPrompt(reqs) {
2102
2106
  // Wrap in braces so the model sees "{site}" not "site" — makes the visual
2103
2107
  // connection to the source string obvious and reduces rename errors.
2104
2108
  placeholders: r.placeholders.map((p) => `{${p}}`),
2109
+ ...r.literals?.length ? { literals: r.literals } : {},
2105
2110
  ...r.glossary?.length ? { glossary: r.glossary } : {},
2106
2111
  hasScreenshot: r.image !== void 0
2107
2112
  };
@@ -3162,6 +3167,7 @@ function selectRequests(state, opts) {
3162
3167
  const complete = categoriesFor(locale).every((c) => (have[c] ?? "") !== "");
3163
3168
  if (opts.onlyMissing && complete) continue;
3164
3169
  const glossary = relevantGlossary(other, locale, state.glossary);
3170
+ const literals = quotedLiterals(other);
3165
3171
  reqs.push({
3166
3172
  id: String(id++),
3167
3173
  key,
@@ -3171,6 +3177,7 @@ function selectRequests(state, opts) {
3171
3177
  targetLocale: locale,
3172
3178
  maxLength: entry.maxLength,
3173
3179
  placeholders: extractPlaceholders(other),
3180
+ ...literals.length ? { literals } : {},
3174
3181
  ...glossary.length ? { glossary } : {},
3175
3182
  plural: { arg: entry.plural.arg, categories: categoriesFor(locale), sourceForms }
3176
3183
  });
@@ -3183,6 +3190,7 @@ function selectRequests(state, opts) {
3183
3190
  const existing = entry.values[locale]?.value;
3184
3191
  if (opts.onlyMissing && existing) continue;
3185
3192
  const glossary = relevantGlossary(source, locale, state.glossary);
3193
+ const literals = quotedLiterals(source);
3186
3194
  reqs.push({
3187
3195
  id: String(id++),
3188
3196
  key,
@@ -3192,6 +3200,7 @@ function selectRequests(state, opts) {
3192
3200
  targetLocale: locale,
3193
3201
  maxLength: entry.maxLength,
3194
3202
  placeholders: extractPlaceholders(source),
3203
+ ...literals.length ? { literals } : {},
3195
3204
  ...glossary.length ? { glossary } : {}
3196
3205
  });
3197
3206
  }
@@ -3683,7 +3692,8 @@ function buildContextSystemPrompt() {
3683
3692
  "- Do NOT restate the source string itself.",
3684
3693
  "- Do NOT say 'This string is...' \u2014 write the context as a direct description.",
3685
3694
  "- Keep it under 500 characters.",
3686
- "- If no code snippets are available, infer from the key path and source value."
3695
+ "- If no code snippets are available, infer from the key path and source value.",
3696
+ "- Tokens: a source may contain interpolation placeholders ({name}, {{name}}, :name, %s) and ICU-apostrophe-quoted LITERAL tokens (e.g. '{{visitor}}', '{name}') that the app fills at runtime. Any provided `literals` are literal tokens, NOT plain placeholders. If you reference a token, write it EXACTLY as it appears in the source \u2014 keep apostrophe-quoted literals quoted, and never relabel a quoted literal as a placeholder or strip its quotes. The translation engine needs these to survive verbatim, so a note may simply remind translators to reproduce them exactly."
3687
3697
  ].join("\n");
3688
3698
  }
3689
3699
  function buildContextBatchPrompt(reqs) {
@@ -3695,7 +3705,14 @@ function buildContextBatchPrompt(reqs) {
3695
3705
  ${s.lines}
3696
3706
  \`\`\``;
3697
3707
  }).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
3698
- return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText };
3708
+ const literals = quotedLiterals(r.source);
3709
+ return {
3710
+ id: r.id,
3711
+ key: r.key,
3712
+ source: r.source,
3713
+ ...literals.length ? { literals } : {},
3714
+ codeSnippets: snippetText
3715
+ };
3699
3716
  });
3700
3717
  return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
3701
3718
  }
@@ -3733,6 +3750,7 @@ var init_context = __esm({
3733
3750
  "src/server/ai/context.ts"() {
3734
3751
  "use strict";
3735
3752
  init_state();
3753
+ init_placeholders();
3736
3754
  MAX_CONTEXT_LENGTH = 500;
3737
3755
  SNIPPET_WINDOW = 15;
3738
3756
  MAX_SNIPPETS = 3;
@@ -1215,6 +1215,116 @@ function runScan(projectRoot, opts, existing) {
1215
1215
  // src/server/ai/context.ts
1216
1216
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1217
1217
  import { resolve as resolve3 } from "path";
1218
+
1219
+ // src/server/placeholders.ts
1220
+ var ICU_PLURAL_SELECT = /\{\s*\w+\s*,\s*(?:plural|select|selectordinal)\s*,/;
1221
+ function withoutQuotedSpans(value) {
1222
+ if (!value.includes("'")) return value;
1223
+ let out = "";
1224
+ for (let i = 0; i < value.length; ) {
1225
+ if (value[i] === "'") {
1226
+ const next = value[i + 1];
1227
+ if (next === "'") {
1228
+ out += " ";
1229
+ i += 2;
1230
+ continue;
1231
+ }
1232
+ if (next === "{" || next === "}" || next === "#" || next === "|") {
1233
+ i += 2;
1234
+ while (i < value.length && value[i] !== "'") i++;
1235
+ i++;
1236
+ continue;
1237
+ }
1238
+ }
1239
+ out += value[i];
1240
+ i++;
1241
+ }
1242
+ return out;
1243
+ }
1244
+ function extractPlaceholders(value) {
1245
+ const scan = withoutQuotedSpans(value);
1246
+ const names = /* @__PURE__ */ new Set();
1247
+ for (const m of scan.matchAll(/\{\s*(\w+)\s*,\s*(?:plural|select|selectordinal)\s*,/g)) {
1248
+ names.add(m[1]);
1249
+ }
1250
+ if (!isIcuPluralOrSelect(scan)) {
1251
+ for (const m of scan.matchAll(/\{\s*(\w+)\s*\}/g)) names.add(m[1]);
1252
+ }
1253
+ return [...names];
1254
+ }
1255
+ function isIcuPluralOrSelect(value) {
1256
+ return ICU_PLURAL_SELECT.test(value);
1257
+ }
1258
+ function withLiterals(value, convertGap, emitLiteral) {
1259
+ let out = "";
1260
+ let gap = "";
1261
+ const flushGap = () => {
1262
+ if (gap) {
1263
+ out += convertGap(gap);
1264
+ gap = "";
1265
+ }
1266
+ };
1267
+ for (let i = 0; i < value.length; ) {
1268
+ if (value[i] === "'") {
1269
+ const next = value[i + 1];
1270
+ if (next === "'") {
1271
+ gap += "'";
1272
+ i += 2;
1273
+ continue;
1274
+ }
1275
+ if (next === "{" || next === "}" || next === "#" || next === "|") {
1276
+ flushGap();
1277
+ let j = i + 1;
1278
+ while (j < value.length && value[j] !== "'") j++;
1279
+ out += emitLiteral(value.slice(i + 1, j));
1280
+ i = j < value.length ? j + 1 : j;
1281
+ continue;
1282
+ }
1283
+ }
1284
+ gap += value[i];
1285
+ i++;
1286
+ }
1287
+ flushGap();
1288
+ return out;
1289
+ }
1290
+ function extractLiterals2(value) {
1291
+ const out = [];
1292
+ withLiterals(value, () => "", (lit) => {
1293
+ out.push(lit);
1294
+ return "";
1295
+ });
1296
+ return out;
1297
+ }
1298
+ function quotedLiterals(value) {
1299
+ return extractLiterals2(value).map((content) => `'${content}'`);
1300
+ }
1301
+ function toLaravel(value) {
1302
+ if (isIcuPluralOrSelect(value)) return value;
1303
+ return withLiterals(value, (gap) => gap.replace(/\{(\w+)\}/g, ":$1"), (lit) => lit);
1304
+ }
1305
+ function toI18next(value) {
1306
+ if (isIcuPluralOrSelect(value)) return value;
1307
+ return withLiterals(value, (gap) => gap.replace(/(?<!\{)\{(\w+)\}(?!\})/g, "{{$1}}"), (lit) => lit);
1308
+ }
1309
+ function toRuby(value) {
1310
+ if (isIcuPluralOrSelect(value)) return value;
1311
+ return withLiterals(value, (gap) => gap.replace(/(?<!%)\{(\w+)\}/g, "%{$1}"), (lit) => lit);
1312
+ }
1313
+ function placeholdersMatch(source, translation) {
1314
+ const a = extractPlaceholders(source).sort();
1315
+ const b = extractPlaceholders(translation).sort();
1316
+ return a.length === b.length && a.every((x, i) => x === b[i]);
1317
+ }
1318
+ function placeholdersSubset(source, translation) {
1319
+ const allowed = new Set(extractPlaceholders(source));
1320
+ return extractPlaceholders(translation).every((p) => allowed.has(p));
1321
+ }
1322
+ var COUNT_OPTIONAL = /* @__PURE__ */ new Set(["zero", "one", "two"]);
1323
+ function pluralFormPlaceholdersMatch(category, source, form) {
1324
+ return COUNT_OPTIONAL.has(category) ? placeholdersSubset(source, form) : placeholdersMatch(source, form);
1325
+ }
1326
+
1327
+ // src/server/ai/context.ts
1218
1328
  var MAX_CONTEXT_LENGTH = 500;
1219
1329
  var SNIPPET_WINDOW = 15;
1220
1330
  var MAX_SNIPPETS = 3;
@@ -1300,7 +1410,8 @@ function buildContextSystemPrompt() {
1300
1410
  "- Do NOT restate the source string itself.",
1301
1411
  "- Do NOT say 'This string is...' \u2014 write the context as a direct description.",
1302
1412
  "- Keep it under 500 characters.",
1303
- "- If no code snippets are available, infer from the key path and source value."
1413
+ "- If no code snippets are available, infer from the key path and source value.",
1414
+ "- Tokens: a source may contain interpolation placeholders ({name}, {{name}}, :name, %s) and ICU-apostrophe-quoted LITERAL tokens (e.g. '{{visitor}}', '{name}') that the app fills at runtime. Any provided `literals` are literal tokens, NOT plain placeholders. If you reference a token, write it EXACTLY as it appears in the source \u2014 keep apostrophe-quoted literals quoted, and never relabel a quoted literal as a placeholder or strip its quotes. The translation engine needs these to survive verbatim, so a note may simply remind translators to reproduce them exactly."
1304
1415
  ].join("\n");
1305
1416
  }
1306
1417
  function buildContextBatchPrompt(reqs) {
@@ -1312,7 +1423,14 @@ function buildContextBatchPrompt(reqs) {
1312
1423
  ${s.lines}
1313
1424
  \`\`\``;
1314
1425
  }).join("\n\n") : "(no code references found \u2014 infer from key path and source value)";
1315
- return { id: r.id, key: r.key, source: r.source, codeSnippets: snippetText };
1426
+ const literals = quotedLiterals(r.source);
1427
+ return {
1428
+ id: r.id,
1429
+ key: r.key,
1430
+ source: r.source,
1431
+ ...literals.length ? { literals } : {},
1432
+ codeSnippets: snippetText
1433
+ };
1316
1434
  });
1317
1435
  return 'Write a context note for each key. Return JSON {"items":[{"id","context"}]}.\n' + JSON.stringify(items, null, 2);
1318
1436
  }
@@ -1476,111 +1594,6 @@ function computeStats(state) {
1476
1594
  };
1477
1595
  }
1478
1596
 
1479
- // src/server/placeholders.ts
1480
- var ICU_PLURAL_SELECT = /\{\s*\w+\s*,\s*(?:plural|select|selectordinal)\s*,/;
1481
- function withoutQuotedSpans(value) {
1482
- if (!value.includes("'")) return value;
1483
- let out = "";
1484
- for (let i = 0; i < value.length; ) {
1485
- if (value[i] === "'") {
1486
- const next = value[i + 1];
1487
- if (next === "'") {
1488
- out += " ";
1489
- i += 2;
1490
- continue;
1491
- }
1492
- if (next === "{" || next === "}" || next === "#" || next === "|") {
1493
- i += 2;
1494
- while (i < value.length && value[i] !== "'") i++;
1495
- i++;
1496
- continue;
1497
- }
1498
- }
1499
- out += value[i];
1500
- i++;
1501
- }
1502
- return out;
1503
- }
1504
- function extractPlaceholders(value) {
1505
- const scan = withoutQuotedSpans(value);
1506
- const names = /* @__PURE__ */ new Set();
1507
- for (const m of scan.matchAll(/\{\s*(\w+)\s*,\s*(?:plural|select|selectordinal)\s*,/g)) {
1508
- names.add(m[1]);
1509
- }
1510
- if (!isIcuPluralOrSelect(scan)) {
1511
- for (const m of scan.matchAll(/\{\s*(\w+)\s*\}/g)) names.add(m[1]);
1512
- }
1513
- return [...names];
1514
- }
1515
- function isIcuPluralOrSelect(value) {
1516
- return ICU_PLURAL_SELECT.test(value);
1517
- }
1518
- function withLiterals(value, convertGap, emitLiteral) {
1519
- let out = "";
1520
- let gap = "";
1521
- const flushGap = () => {
1522
- if (gap) {
1523
- out += convertGap(gap);
1524
- gap = "";
1525
- }
1526
- };
1527
- for (let i = 0; i < value.length; ) {
1528
- if (value[i] === "'") {
1529
- const next = value[i + 1];
1530
- if (next === "'") {
1531
- gap += "'";
1532
- i += 2;
1533
- continue;
1534
- }
1535
- if (next === "{" || next === "}" || next === "#" || next === "|") {
1536
- flushGap();
1537
- let j = i + 1;
1538
- while (j < value.length && value[j] !== "'") j++;
1539
- out += emitLiteral(value.slice(i + 1, j));
1540
- i = j < value.length ? j + 1 : j;
1541
- continue;
1542
- }
1543
- }
1544
- gap += value[i];
1545
- i++;
1546
- }
1547
- flushGap();
1548
- return out;
1549
- }
1550
- function extractLiterals2(value) {
1551
- const out = [];
1552
- withLiterals(value, () => "", (lit) => {
1553
- out.push(lit);
1554
- return "";
1555
- });
1556
- return out;
1557
- }
1558
- function toLaravel(value) {
1559
- if (isIcuPluralOrSelect(value)) return value;
1560
- return withLiterals(value, (gap) => gap.replace(/\{(\w+)\}/g, ":$1"), (lit) => lit);
1561
- }
1562
- function toI18next(value) {
1563
- if (isIcuPluralOrSelect(value)) return value;
1564
- return withLiterals(value, (gap) => gap.replace(/(?<!\{)\{(\w+)\}(?!\})/g, "{{$1}}"), (lit) => lit);
1565
- }
1566
- function toRuby(value) {
1567
- if (isIcuPluralOrSelect(value)) return value;
1568
- return withLiterals(value, (gap) => gap.replace(/(?<!%)\{(\w+)\}/g, "%{$1}"), (lit) => lit);
1569
- }
1570
- function placeholdersMatch(source, translation) {
1571
- const a = extractPlaceholders(source).sort();
1572
- const b = extractPlaceholders(translation).sort();
1573
- return a.length === b.length && a.every((x, i) => x === b[i]);
1574
- }
1575
- function placeholdersSubset(source, translation) {
1576
- const allowed = new Set(extractPlaceholders(source));
1577
- return extractPlaceholders(translation).every((p) => allowed.has(p));
1578
- }
1579
- var COUNT_OPTIONAL = /* @__PURE__ */ new Set(["zero", "one", "two"]);
1580
- function pluralFormPlaceholdersMatch(category, source, form) {
1581
- return COUNT_OPTIONAL.has(category) ? placeholdersSubset(source, form) : placeholdersMatch(source, form);
1582
- }
1583
-
1584
1597
  // src/server/glossary.ts
1585
1598
  function contains(haystack, needle, caseSensitive) {
1586
1599
  return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
@@ -2960,10 +2973,11 @@ function buildSystemPrompt(hasPluralItems) {
2960
2973
  "You are a professional software localization engine for a UI string catalog.",
2961
2974
  "Your goal: translate each source UI string into its target locale accurately and idiomatically, as a native speaker would phrase it in a real app interface.",
2962
2975
  "",
2963
- "You are given, per item: the key path, the source text, optional human context, the target locale, an optional max length, the list of interpolation placeholders, and any relevant glossary entries. Some items also include a screenshot image showing where the string appears in the UI \u2014 use it to disambiguate meaning, tone, and length.",
2976
+ "You are given, per item: the key path, the source text, optional human context, the target locale, an optional max length, the list of interpolation placeholders, an optional `literals` list, and any relevant glossary entries. Some items also include a screenshot image showing where the string appears in the UI \u2014 use it to disambiguate meaning, tone, and length.",
2964
2977
  "",
2965
2978
  "Hard rules:",
2966
2979
  "- Preserve every interpolation placeholder EXACTLY as written: {name}, {{count}}, %s, %d, :name. Never translate, rename, reorder, or remove them.",
2980
+ "- Reproduce every entry of the item's `literals` array EXACTLY, including its surrounding apostrophes (e.g. '{{visitor}}', '{name}'). These are app-managed literal tokens, not prose: translate the words around them, but never translate, rename, unquote, or drop them. The apostrophes are required \u2014 a result with bare {{visitor}} instead of '{{visitor}}' is wrong.",
2967
2981
  "- Preserve ICU plural/select structure verbatim (e.g. {count, plural, one {\u2026} other {\u2026}}); translate only the human-readable text inside each branch.",
2968
2982
  "- Glossary: a term marked do-not-translate MUST appear unchanged in the translation. A term with a forced translation for the target locale MUST use that exact translation.",
2969
2983
  "- Respect the max length (characters) when given; prefer a shorter natural phrasing over exceeding it.",
@@ -2992,6 +3006,7 @@ function buildBatchPrompt(reqs) {
2992
3006
  // Wrap in braces so the model sees "{site}" not "site" — makes the visual
2993
3007
  // connection to the source string obvious and reduces rename errors.
2994
3008
  placeholders: r.placeholders.map((p) => `{${p}}`),
3009
+ ...r.literals?.length ? { literals: r.literals } : {},
2995
3010
  ...r.glossary?.length ? { glossary: r.glossary } : {},
2996
3011
  hasScreenshot: r.image !== void 0
2997
3012
  };
@@ -3810,6 +3825,7 @@ function selectRequests(state, opts) {
3810
3825
  const complete = categoriesFor(locale).every((c) => (have[c] ?? "") !== "");
3811
3826
  if (opts.onlyMissing && complete) continue;
3812
3827
  const glossary = relevantGlossary(other, locale, state.glossary);
3828
+ const literals = quotedLiterals(other);
3813
3829
  reqs.push({
3814
3830
  id: String(id++),
3815
3831
  key,
@@ -3819,6 +3835,7 @@ function selectRequests(state, opts) {
3819
3835
  targetLocale: locale,
3820
3836
  maxLength: entry.maxLength,
3821
3837
  placeholders: extractPlaceholders(other),
3838
+ ...literals.length ? { literals } : {},
3822
3839
  ...glossary.length ? { glossary } : {},
3823
3840
  plural: { arg: entry.plural.arg, categories: categoriesFor(locale), sourceForms }
3824
3841
  });
@@ -3831,6 +3848,7 @@ function selectRequests(state, opts) {
3831
3848
  const existing = entry.values[locale]?.value;
3832
3849
  if (opts.onlyMissing && existing) continue;
3833
3850
  const glossary = relevantGlossary(source, locale, state.glossary);
3851
+ const literals = quotedLiterals(source);
3834
3852
  reqs.push({
3835
3853
  id: String(id++),
3836
3854
  key,
@@ -3840,6 +3858,7 @@ function selectRequests(state, opts) {
3840
3858
  targetLocale: locale,
3841
3859
  maxLength: entry.maxLength,
3842
3860
  placeholders: extractPlaceholders(source),
3861
+ ...literals.length ? { literals } : {},
3843
3862
  ...glossary.length ? { glossary } : {}
3844
3863
  });
3845
3864
  }
@@ -71,14 +71,13 @@ var e=Object.defineProperty,t=(t,n)=>{let r={};for(var i in t)e(r,i,{get:t[i],en
71
71
  <p>Glotfile is designed to run with <strong>no install</strong> via <code>npx</code>:</p>
72
72
  <pre><code class="language-bash">npx glotfile
73
73
  </code></pre>
74
- <blockquote>
75
- <p><strong>⚠ Pre-1.0: run from a checkout for now</strong> Glotfile is pre-1.0 and not yet published to npm. Until it is, run it from a clone of the repo:</p>
76
- <pre><code class="language-bash">npm install
77
- npm run build # builds the UI + server into dist/
78
- node bin/glotfile.js # equivalent to the \`glotfile\` command
74
+ <p>Or install it globally:</p>
75
+ <pre><code class="language-bash">npm i -g glotfile
79
76
  </code></pre>
77
+ <blockquote>
78
+ <p><strong>⚠ Pre-1.0</strong> — Glotfile is published to npm but still pre-1.0, so expect occasional breaking changes; pin a version (e.g. <code>npx glotfile@0.8.7</code>) if you need stability.</p>
80
79
  </blockquote>
81
- <p>Once published, <code>npx glotfile &lt;command&gt;</code> and a global install will both work. Throughout these docs, <code>glotfile &lt;command&gt;</code> and <code>node bin/glotfile.js &lt;command&gt;</code> are interchangeable.</p>
80
+ <p>Throughout these docs, <code>glotfile &lt;command&gt;</code> and <code>node bin/glotfile.js &lt;command&gt;</code> are interchangeable.</p>
82
81
  <h2>First run</h2>
83
82
  <p>Run Glotfile from the root of the project whose copy you want to manage:</p>
84
83
  <pre><code class="language-bash">glotfile # same as: glotfile serve
@@ -104,7 +103,7 @@ node bin/glotfile.js # equivalent to the \`glotfile\` command
104
103
  <li>Quick Start — translate your first strings</li>
105
104
  <li>The State File — understand what <code>glotfile.json</code> holds</li>
106
105
  </ul>
107
- `,text:`Installation Requirements - Node.js That's the only hard requirement. Everything except AI translation runs fully offline, with no account and no network access. Running Glotfile Glotfile is designed to run with no install via : ⚠ Pre-1.0: run from a checkout for now — Glotfile is pre-1.0 and not yet published to npm. Until it is, run it from a clone of the repo: Once published, and a global install will both work. Throughout these docs, and are interchangeable. First run Run Glotfile from the root of the project whose copy you want to manage: This starts a local server bound to , opens your browser, and: - If a already exists in the current directory, it loads it. - If not, it starts from sensible defaults and writes the file as soon as you make your first edit . Info: Already have locale files? — Import an existing project by reading your current / / files into the catalog with — see import. Targeting a different file Every command accepts ( ) to use a state file other than : Developing Glotfile itself If you're hacking on Glotfile (not just using it), run the Vite UI with hot-reload alongside the server: Next steps - Quick Start — translate your first strings - The State File — understand what holds`},{id:`getting-started/quick-start`,title:`Quick Start`,section:`Getting Started`,html:`<h1>Quick Start</h1>
106
+ `,text:`Installation Requirements - Node.js That's the only hard requirement. Everything except AI translation runs fully offline, with no account and no network access. Running Glotfile Glotfile is designed to run with no install via : Or install it globally: ⚠ Pre-1.0 — Glotfile is published to npm but still pre-1.0, so expect occasional breaking changes; pin a version (e.g. ) if you need stability. Throughout these docs, and are interchangeable. First run Run Glotfile from the root of the project whose copy you want to manage: This starts a local server bound to , opens your browser, and: - If a already exists in the current directory, it loads it. - If not, it starts from sensible defaults and writes the file as soon as you make your first edit . Info: Already have locale files? — Import an existing project by reading your current / / files into the catalog with — see import. Targeting a different file Every command accepts ( ) to use a state file other than : Developing Glotfile itself If you're hacking on Glotfile (not just using it), run the Vite UI with hot-reload alongside the server: Next steps - Quick Start — translate your first strings - The State File — understand what holds`},{id:`getting-started/quick-start`,title:`Quick Start`,section:`Getting Started`,html:`<h1>Quick Start</h1>
108
107
  <p>This walks you from an empty project to translated, exported strings. It assumes you&#39;ve done Installation.</p>
109
108
  <h2>1. Start the UI</h2>
110
109
  <p>From your project root:</p>
@@ -2334,7 +2333,7 @@ git commit -m &quot;Checkout copy + translations&quot;
2334
2333
  `,text:`Translation Workflow The day-to-day loop, end to end. This is the same flow as Quick Start, but with the why behind each step. The loop 1. Author source copy as you build Run and add keys with their source strings in the Editor while you build a feature. Add a one-line context for anything ambiguous — it's the highest-leverage thing you can do for translation quality. Attach a screenshot for UI-heavy strings. 2. Fill the other languages This fills only the empty values; pass to re-translate everything. Or use the Editor's translate action. New translations land as . See How Translation Works. 3. Review Filter the Editor to machine and check the new translations. Promote good ones to (which protects them from being overwritten); flag the rest as . Watch the Analytics panel for reviewed-vs-translated coverage. 4. Export This regenerates your apps' locale files from the catalog. See Output Formats. 5. Commit Commit together with your code and the regenerated locale files. The diff is the review: Why git-native pays off Because the catalog is just a file in your repo: - Branching — translations live on the feature branch with the code that needs them. - Pull-request review — reviewers see copy changes as a normal diff. - Rollback — reverting a commit reverts the copy too. - No drift — CI can fail the build if exports are stale or the catalog has problems. Related - Continuous Integration · Keeping Translations Fresh · How Translation Works · Review States`},{id:`troubleshooting-and-faq`,title:`Troubleshooting and FAQ`,section:`Help`,html:`<h1>Troubleshooting and FAQ</h1>
2335
2334
  <h2>Setup</h2>
2336
2335
  <p><strong><code>glotfile: command not found</code></strong>
2337
- It&#39;s pre-1.0 and not yet on npm. Run from a checkout: <code>npm install &amp;&amp; npm run build &amp;&amp; node bin/glotfile.js</code>. See Installation.</p>
2336
+ Run it with <code>npx glotfile</code> (no global install needed), or install globally with <code>npm i -g glotfile</code>. From a checkout, build first: <code>npm install &amp;&amp; npm run build &amp;&amp; node bin/glotfile.js</code>. See Installation.</p>
2338
2337
  <p><strong>Wrong Node version</strong>
2339
2338
  Glotfile needs Node <code>^20.19.0 || &gt;=22.12.0</code>. Check with <code>node -v</code>.</p>
2340
2339
  <h2>Translation</h2>
@@ -2369,4 +2368,4 @@ Only the AI calls you trigger. The UI is local-only; no credentials or screensho
2369
2368
  <ul>
2370
2369
  <li>Home · Installation · AI Providers · Continuous Integration</li>
2371
2370
  </ul>
2372
- `,text:`Troubleshooting and FAQ Setup It's pre-1.0 and not yet on npm. Run from a checkout: . See Installation. Wrong Node version Glotfile needs Node . Check with . Translation "Install the SDK for this provider" OpenAI, OpenRouter, and Bedrock need an optional SDK. Install the one for your provider — (OpenAI or OpenRouter) or (Bedrock). See AI Providers. No API key / auth errors Set the provider's credentials in your environment or a local : , , , or the AWS chain. See AI Providers. A translation was skipped The result failed validation — a dropped placeholder, broken ICU, or an over-length value. Glotfile prints the reason and leaves the value untranslated. See Placeholders and ICU and How Translation Works. My reviewed translation didn't change after That's intentional — values are protected from being overwritten. Unmark it first to re-translate. Screenshots seem ignored The selected model may not support vision (e.g. Bedrock Meta Llama text models). The run proceeds text-only and warns how many screenshots were skipped. See Screenshots. Validation and CI fails with Your exported files don't match the catalog. Run and commit the result. See Continuous Integration. Spelling flags real words Add them to the custom dictionary in Settings ( ). The same list silences both the editor's live spell check and the lint rule. Glossary terms are accepted automatically. See Checks and Validation. A rule is too noisy Override its severity (or turn it off) in , or certain key globs. See Configuration Reference. Files and git Can I edit by hand? You can, but you rarely need to — the UI writes it for you, deterministically. If you do edit it, keep it valid: it's validated on load. See The State File. Big diffs on every save Check ( , , ). Deterministic formatting keeps diffs minimal. See The State File. General Do I already have translations I can import? Import existing locale files with — see import. You can also add keys via the Editor. Does anything leave my machine? Only the AI calls you trigger. The UI is local-only; no credentials or screenshot bytes are ever logged. See AI Log. Related - Home · Installation · AI Providers · Continuous Integration`}],$F={class:`flex min-h-0 flex-1 overflow-hidden`},eI={class:`flex w-56 shrink-0 flex-col overflow-hidden border-r bg-muted/30`},tI={class:`relative shrink-0 px-2 py-2`},nI={key:0,class:`flex flex-col gap-1 overflow-y-auto px-2 pb-3`},rI={class:`px-2 py-1 text-xs text-muted-foreground`},iI=[`onClick`],aI={class:`text-sm font-medium`},oI={key:0,class:`text-[10px] uppercase tracking-wider text-muted-foreground`},sI={key:1,class:`mt-0.5 line-clamp-2 text-xs text-muted-foreground`},cI={class:`bg-primary/20 text-foreground`},lI={key:1,class:`flex flex-col gap-1 overflow-y-auto px-2 pb-3`},uI=[`onClick`],dI={class:`min-w-0 flex-1 overflow-y-auto`},fI=[`innerHTML`],pI=F({__name:`DocsView`,setup(e){let t=[``,`Getting Started`,`Frameworks`,`Web UI`,`CLI`,`Concepts`,`AI Translation`,`Reference`,`Guides`,`Help`];function n(){let e=Np().get(`doc`);return e&&QF.some(t=>t.id===e)?e:QF[0]?.id??``}let r=M(n()),i=M(``),a=q(()=>{let e=new Map;for(let t of QF)e.has(t.section)||e.set(t.section,[]),e.get(t.section).push(t);return[...t,...[...e.keys()].filter(e=>!t.includes(e))].map(t=>({section:t,pages:e.get(t)??[]})).filter(e=>e.pages.length>0)}),o=q(()=>{let e=i.value.trim().toLowerCase();if(!e)return[];let t=[];for(let n of QF){let r=n.text.toLowerCase(),i=n.title.toLowerCase().includes(e),a=r.indexOf(e);if(a===-1&&!i)continue;if(a===-1){t.push({id:n.id,title:n.title,section:n.section,before:``,match:``,after:``});continue}let o=Math.max(0,a-40),s=Math.min(n.text.length,a+e.length+80);t.push({id:n.id,title:n.title,section:n.section,before:(o>0?`…`:``)+n.text.slice(o,a),match:n.text.slice(a,a+e.length),after:n.text.slice(a+e.length,s)+(s<n.text.length?`…`:``)})}return t}),s=q(()=>QF.find(e=>e.id===r.value)??QF[0]);function c(e){e!==r.value&&(r.value=e,history.pushState(null,``,`#docs?${new URLSearchParams({doc:e})}`))}function l(e){c(e),i.value=``}function u(){r.value=n()}return Gi(()=>window.addEventListener(`hashchange`,u)),Yi(()=>window.removeEventListener(`hashchange`,u)),(e,t)=>(z(),B(`div`,$F,[H(`nav`,eI,[H(`div`,tI,[U(N(pd),{class:`pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground`}),gr(H(`input`,{"onUpdate:modelValue":t[0]||=e=>i.value=e,type:`search`,placeholder:`Search docs…`,class:`w-full rounded-md border bg-background py-1 pl-7 pr-7 text-sm outline-none focus:ring-2 focus:ring-ring`},null,512),[[Pl,i.value]]),i.value?(z(),B(`button`,{key:0,type:`button`,class:`absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground`,onClick:t[1]||=e=>i.value=``},[U(N(kd),{class:`size-3.5`})])):G(``,!0)]),i.value.trim()?(z(),B(`div`,nI,[H(`div`,rI,j(o.value.length)+` result`+j(o.value.length===1?``:`s`),1),(z(!0),B(R,null,I(o.value,e=>(z(),B(`button`,{key:e.id,type:`button`,class:`w-full rounded-md px-2 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground`,onClick:t=>l(e.id)},[H(`div`,aI,j(e.title),1),e.section?(z(),B(`div`,oI,j(e.section),1)):G(``,!0),e.match?(z(),B(`div`,sI,[W(j(e.before),1),H(`mark`,cI,j(e.match),1),W(j(e.after),1)])):G(``,!0)],8,iI))),128))])):(z(),B(`div`,lI,[(z(!0),B(R,null,I(a.value,(e,t)=>(z(),B(R,{key:e.section},[e.section?(z(),B(`div`,{key:0,class:A([`px-2 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground`,t>0?`mt-3`:``])},j(e.section),3)):G(``,!0),(z(!0),B(R,null,I(e.pages,e=>(z(),B(`button`,{key:e.id,type:`button`,class:A([`w-full rounded-md px-2 py-1 text-left text-sm transition-colors`,r.value===e.id?`bg-primary text-primary-foreground`:`text-foreground hover:bg-accent hover:text-accent-foreground`]),onClick:t=>c(e.id)},j(e.title),11,uI))),128))],64))),128))]))]),H(`div`,dI,[H(`div`,{class:`prose prose-sm dark:prose-invert mx-auto max-w-3xl px-8 py-6`,innerHTML:s.value?.html},null,8,fI)])]))}}),mI={en:`English`,fr:`French`,de:`German`,es:`Spanish`,pt:`Portuguese`,it:`Italian`,nl:`Dutch`,ca:`Catalan`,pl:`Polish`,ru:`Russian`,uk:`Ukrainian`,cs:`Czech`,sk:`Slovak`,sv:`Swedish`,da:`Danish`,no:`Norwegian`,nb:`Norwegian Bokmål`,fi:`Finnish`,tr:`Turkish`,el:`Greek`,ro:`Romanian`,hu:`Hungarian`,bg:`Bulgarian`,hr:`Croatian`,sr:`Serbian`,sl:`Slovenian`,ja:`Japanese`,ko:`Korean`,zh:`Chinese`,ar:`Arabic`,he:`Hebrew`,hi:`Hindi`,th:`Thai`,vi:`Vietnamese`,id:`Indonesian`,ms:`Malay`,fa:`Persian`};function hI(e){let[t=``,n]=e.split(/[-_]/),r=t.toLowerCase(),i=n&&/^[A-Za-z]{2}$/.test(n)?n.toUpperCase():void 0,a=mI[r]??e;return{code:e,name:i?`${a} (${i})`:a}}var gI={class:`p-6`},_I={class:`flex items-start gap-3 pr-6`},vI={class:`grid h-10 w-10 shrink-0 place-items-center rounded-lg border border-primary/25 bg-primary/10 font-mono text-[13px] font-semibold leading-none tracking-tight text-primary`},yI={class:`min-w-0`},bI={class:`font-medium text-foreground`},xI={class:`font-mono text-[12px]`},SI={class:`mt-4 flex items-center gap-4 rounded-lg border border-border bg-muted/40 px-3.5 py-2.5`},CI={class:`flex flex-col`},wI={class:`text-[17px] font-semibold leading-none tabular-nums`},TI={class:`flex flex-col`},EI={class:`text-[17px] font-semibold leading-none tabular-nums`},DI={class:`ml-auto flex items-center gap-1.5 whitespace-nowrap text-[11px] text-muted-foreground`},OI={class:`mt-5`},kI={class:`!flex min-w-0 items-center gap-2`},AI={class:`font-mono text-[13px]`},jI={class:`truncate text-[13px] text-muted-foreground`},MI={class:`flex items-center gap-2`},NI={class:`font-mono text-[13px]`},PI={class:`text-[13px] text-muted-foreground`},FI={class:`mt-4`},II={class:`flex items-center justify-between`},LI={class:`whitespace-nowrap text-[12px] tabular-nums text-muted-foreground`},RI={class:`mt-1.5 flex flex-wrap gap-1.5 rounded-lg border border-border bg-muted/30 p-2.5`},zI=[`disabled`,`onClick`],BI={class:`font-mono`},VI={key:0,class:`ml-0.5 rounded-sm bg-primary/15 px-1 text-[9px] font-semibold uppercase tracking-wide text-primary`},HI={key:0,class:`mt-4`},UI={class:`overflow-hidden rounded-lg border border-border`},WI={class:`ml-auto font-mono text-[11px] text-muted-foreground/70`},GI={key:0,class:`divide-y divide-border border-t border-border gf-content-fade`},KI={class:`shrink-0 font-mono text-foreground`},qI={class:`ml-auto truncate text-right text-muted-foreground`},JI={key:0,class:`px-3 py-1.5 text-[11px] text-muted-foreground/70`},YI={class:`flex items-center gap-2 border-t border-border px-6 py-4`},XI={class:`ml-1 flex cursor-pointer select-none items-center gap-1.5 text-[12px] text-muted-foreground`},ZI={class:`ml-auto`},QI={class:`flex flex-col items-center justify-center gap-4 p-6 py-8 text-center`},$I={key:0,class:`mt-1 text-[13px] text-muted-foreground`},eL={class:`p-6`},tL={class:`flex flex-col items-center gap-3 pt-1 text-center`},nL={class:`grid h-12 w-12 place-items-center rounded-full bg-success-bg text-success ring-1 ring-success-border`},rL={class:`font-semibold text-foreground tabular-nums`},iL={class:`font-semibold text-foreground`},aL={key:0,class:`mt-5 overflow-hidden rounded-lg border border-warning-border bg-warning-bg/60`},oL={class:`whitespace-nowrap text-[13px] font-medium text-warning`},sL={key:0,class:`max-h-44 divide-y divide-warning-border/40 overflow-auto border-t border-warning-border/70 gf-content-fade`},cL={class:`flex items-center border-t border-border px-6 py-4`},lL={class:`ml-auto`},uL={class:`p-6`},dL={class:`flex items-start gap-3 pr-6`},fL={class:`grid h-10 w-10 shrink-0 place-items-center rounded-full bg-destructive-soft text-destructive`},pL={class:`min-w-0`},mL={class:`flex items-center border-t border-border px-6 py-4`},hL={class:`ml-auto`},gL=F({__name:`ImportWizard`,emits:[`dismiss`,`imported`],setup(e,{expose:t,emit:n}){let r=n,i=M(!0),a=M(`confirm`),o=M(null),s=M(``),c=M({}),l=M(!1),u=M(!1),d=M(null),f=M(``),p=M(!0);t({init:m});async function m(){try{let e=await Lf();if(!e.found){r(`dismiss`);return}o.value=e,s.value=e.sourceLocale}catch{r(`dismiss`)}}let h={"laravel-php":{label:`Laravel PHP`,glyph:`</>`,file:`lang/{locale}/*.php`},"vue-i18n-json":{label:`Vue i18n JSON`,glyph:`{ }`,file:`locales/{locale}.json`},"flutter-arb":{label:`Flutter ARB`,glyph:`arb`,file:`lib/l10n/app_{locale}.arb`},"apple-strings":{label:`Apple .strings`,glyph:``,file:`{locale}.lproj/Localizable.strings`}},g=q(()=>h[o.value?.format??``]??{label:o.value?.format??``,glyph:`{}`,file:``});function _(e){return e===s.value||!c.value[e]}function v(e){e!==s.value&&(c.value={...c.value,[e]:!c.value[e]})}let y=q(()=>o.value?o.value.locales.filter(_):[]),b=q(()=>y.value.length),x=q(()=>o.value?Math.max(0,o.value.keyCount-o.value.sampleKeys.length):0);async function S(){if(o.value){a.value=`importing`;try{d.value=await Rf({format:o.value.format,sourceLocale:s.value,locales:y.value,cldr:p.value}),a.value=`done`}catch(e){f.value=e.message,a.value=`error`}}}function C(e){e||a.value!==`importing`&&(a.value===`done`?r(`imported`):r(`dismiss`))}return(e,t)=>(z(),V(N(C_),{open:i.value,"onUpdate:open":C},{default:P(()=>[U(N(uv),null,{default:P(()=>[U(N(cv),{class:`fixed inset-0 z-50 bg-foreground/40 gf-content-fade`}),U(N(av),{class:`fixed left-1/2 top-1/2 z-50 w-[28rem] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-card text-card-foreground shadow-2xl focus:outline-none gf-content-fade`,onInteractOutside:t[9]||=Jl(()=>{},[`prevent`])},{default:P(()=>[a.value===`confirm`&&o.value?(z(),B(R,{key:0},[H(`button`,{type:`button`,class:`absolute right-3.5 top-3.5 z-10 grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground`,onClick:t[0]||=e=>r(`dismiss`)},[U(N(kd),{class:`size-4`})]),H(`div`,gI,[H(`div`,_I,[H(`div`,vI,j(g.value.glyph),1),H(`div`,yI,[U(N(dv),{class:`text-base font-semibold leading-tight`},{default:P(()=>[...t[10]||=[W(`Import your translations`,-1)]]),_:1}),U(N(ov),{class:`mt-0.5 text-[13px] text-muted-foreground`},{default:P(()=>[t[11]||=W(` Found a `,-1),H(`span`,bI,j(g.value.label),1),t[12]||=W(` setup in `,-1),H(`span`,xI,j(g.value.file),1),t[13]||=W(`. `,-1)]),_:1})])]),H(`div`,SI,[H(`div`,CI,[H(`span`,wI,j(o.value.keyCount.toLocaleString()),1),t[14]||=H(`span`,{class:`mt-1 whitespace-nowrap text-[11px] text-muted-foreground`},`keys detected`,-1)]),t[17]||=H(`div`,{class:`h-7 w-px bg-border`},null,-1),H(`div`,TI,[H(`span`,EI,j(o.value.locales.length),1),t[15]||=H(`span`,{class:`mt-1 whitespace-nowrap text-[11px] text-muted-foreground`},`locales`,-1)]),H(`div`,DI,[U(N(Gu),{class:`size-3.5 opacity-70`}),t[16]||=W(` Detected automatically `,-1)])]),H(`div`,OI,[U(N(MC),{class:`flex items-center gap-1.5 whitespace-nowrap`},{default:P(()=>[...t[18]||=[W(` Source language `,-1),H(`span`,{class:`text-[12px] font-normal text-muted-foreground`},`· the original text`,-1)]]),_:1}),U(N(mS),{modelValue:s.value,"onUpdate:modelValue":t[1]||=e=>s.value=e},{default:P(()=>[U(N(cC),{class:`mt-1.5`},{default:P(()=>[H(`span`,kI,[U(jw,{code:s.value,size:14},null,8,[`code`]),H(`span`,AI,j(s.value),1),H(`span`,jI,j(N(hI)(s.value).name),1)])]),_:1}),U(N(lC),null,{default:P(()=>[(z(!0),B(R,null,I(o.value.locales,e=>(z(),V(N(dC),{key:e,value:e},{default:P(()=>[H(`span`,MI,[U(jw,{code:e,size:14},null,8,[`code`]),H(`span`,NI,j(e),1),H(`span`,PI,j(N(hI)(e).name),1)])]),_:2},1032,[`value`]))),128))]),_:1})]),_:1},8,[`modelValue`])]),H(`div`,FI,[H(`div`,II,[U(N(MC),{class:`whitespace-nowrap`},{default:P(()=>[...t[19]||=[W(`Locales to import`,-1)]]),_:1}),H(`span`,LI,j(b.value)+` of `+j(o.value.locales.length),1)]),H(`div`,RI,[(z(!0),B(R,null,I(o.value.locales,e=>(z(),V(N(eC),{key:e},{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`button`,{type:`button`,disabled:e===s.value,class:A([`group inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border pl-1.5 pr-2 text-[12px] transition-colors`,e===s.value?`border-primary/40 bg-primary/10 cursor-default`:_(e)?`border-input bg-card text-foreground hover:bg-muted`:`border-dashed border-border bg-transparent text-muted-foreground hover:bg-muted/50`]),onClick:t=>v(e)},[U(jw,{code:e,size:14},null,8,[`code`]),H(`span`,BI,j(e),1),e===s.value?(z(),B(`span`,VI,`src`)):(z(),B(`span`,{key:1,class:A([`grid size-3.5 place-items-center rounded-[3px] border`,_(e)?`border-primary bg-primary text-primary-foreground`:`border-border text-transparent`])},[U(N(Du),{class:`size-2.5`,"stroke-width":3})],2))],10,zI)]),_:2},1024),U(N(qC),null,{default:P(()=>[W(j(e===s.value?`Source language — always imported`:_(e)?`Click to exclude`:`Click to include`),1)]),_:2},1024)]),_:2},1024))),128))])]),o.value.sampleKeys.length?(z(),B(`div`,HI,[H(`div`,UI,[H(`button`,{type:`button`,class:`flex w-full items-center gap-2 px-3 py-2 text-[13px] text-muted-foreground transition-colors hover:bg-muted/50`,onClick:t[2]||=e=>l.value=!l.value},[U(N(ku),{class:A([`size-3.5 shrink-0 transition-transform`,l.value?`rotate-90`:``])},null,8,[`class`]),t[20]||=H(`span`,{class:`whitespace-nowrap`},`Preview sample keys`,-1),H(`span`,WI,j(g.value.glyph),1)]),l.value?(z(),B(`div`,GI,[(z(!0),B(R,null,I(o.value.sampleKeys,e=>(z(),B(`div`,{key:e.key,class:`flex items-baseline gap-3 px-3 py-1.5 text-[12px]`},[H(`span`,KI,j(e.key),1),H(`span`,qI,j(e.value),1)]))),128)),x.value>0?(z(),B(`div`,JI,` + `+j(x.value.toLocaleString())+` more keys `,1)):G(``,!0)])):G(``,!0)])])):G(``,!0)]),H(`div`,YI,[U(N($),{variant:`ghost`,size:`sm`,onClick:t[3]||=e=>r(`dismiss`)},{default:P(()=>[...t[21]||=[W(`Skip`,-1)]]),_:1}),U(N(eC),null,{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`label`,XI,[gr(H(`input`,{type:`checkbox`,"data-testid":`cldr-toggle`,"onUpdate:modelValue":t[4]||=e=>p.value=e,class:`size-3.5 accent-primary`},null,512),[[Fl,p.value]]),t[22]||=W(` Convert plurals to CLDR categories `,-1)])]),_:1}),U(N(qC),{class:`max-w-xs`},{default:P(()=>[...t[23]||=[W(`Rewrite exact =N plural selectors (e.g. =1) into each language's CLDR plural categories, the way Crowdin does.`,-1)]]),_:1})]),_:1}),H(`div`,ZI,[U(N($),{"data-testid":`import-btn`,disabled:b.value===0,onClick:S},{default:P(()=>[U(N(Ad),{class:`size-4`}),W(` Import `+j(b.value)+` `+j(b.value===1?`locale`:`locales`),1)]),_:1},8,[`disabled`])])])],64)):a.value===`importing`?(z(),B(R,{key:1},[U(N(dv),{class:`sr-only`},{default:P(()=>[...t[24]||=[W(`Importing translations`,-1)]]),_:1}),H(`div`,QI,[U(N($u),{class:`size-10 text-primary gf-spin`}),H(`div`,null,[t[25]||=H(`div`,{class:`text-sm font-semibold`},`Importing translations…`,-1),o.value?(z(),B(`div`,$I,` Reading `+j(o.value.keyCount.toLocaleString())+` keys across `+j(b.value)+` locales `,1)):G(``,!0)])])],64)):a.value===`done`&&d.value?(z(),B(R,{key:2},[H(`div`,eL,[H(`div`,tL,[H(`div`,nL,[U(N(Du),{class:`size-6`,"stroke-width":2.5})]),H(`div`,null,[U(N(dv),{class:`text-base font-semibold`},{default:P(()=>[...t[26]||=[W(`Import complete`,-1)]]),_:1}),U(N(ov),{class:`mt-1 text-[13px] text-muted-foreground`},{default:P(()=>[H(`span`,rL,j(d.value.keyCount.toLocaleString())+` keys`,1),t[27]||=W(` across `,-1),H(`span`,iL,j(d.value.localeCount)+` locales`,1),t[28]||=W(` imported. `,-1)]),_:1})])]),d.value.warnings.length?(z(),B(`div`,aL,[H(`button`,{type:`button`,class:`flex w-full items-center gap-2 px-3 py-2.5 text-left`,onClick:t[5]||=e=>u.value=!u.value},[U(N(Td),{class:`size-3.5 shrink-0 text-warning`}),H(`span`,oL,j(d.value.warnings.length)+` `+j(d.value.warnings.length===1?`warning`:`warnings`),1),t[29]||=H(`span`,{class:`whitespace-nowrap text-[12px] text-warning/70`},`· import still succeeded`,-1),U(N(ku),{class:A([`ml-auto size-3.5 shrink-0 text-warning transition-transform`,u.value?`rotate-90`:``])},null,8,[`class`])]),u.value?(z(),B(`ul`,sL,[(z(!0),B(R,null,I(d.value.warnings,(e,t)=>(z(),B(`li`,{key:t,class:`px-3 py-2 text-[12px] text-muted-foreground`},j(e),1))),128))])):G(``,!0)])):G(``,!0)]),H(`div`,cL,[H(`div`,lL,[U(N($),{onClick:t[6]||=e=>r(`imported`)},{default:P(()=>[t[30]||=W(`Open editor `,-1),U(N(bu),{class:`size-4`})]),_:1})])])],64)):a.value===`error`?(z(),B(R,{key:3},[H(`button`,{type:`button`,class:`absolute right-3.5 top-3.5 z-10 grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground`,onClick:t[7]||=e=>r(`dismiss`)},[U(N(kd),{class:`size-4`})]),H(`div`,uL,[H(`div`,dL,[H(`div`,fL,[U(N(Td),{class:`size-5`})]),H(`div`,pL,[U(N(dv),{class:`text-base font-semibold`},{default:P(()=>[...t[31]||=[W(`Couldn't import translations`,-1)]]),_:1}),U(N(ov),{class:`mt-1 text-[13px] text-muted-foreground`},{default:P(()=>[W(j(f.value),1)]),_:1})])])]),H(`div`,mL,[H(`div`,hL,[U(N($),{variant:`outline`,onClick:t[8]||=e=>r(`dismiss`)},{default:P(()=>[...t[32]||=[W(`Dismiss`,-1)]]),_:1})])])],64)):G(``,!0)]),_:1})]),_:1})]),_:1},8,[`open`]))}}),_L=`glotfile-theme`,vL=[`system`,`light`,`dark`],yL=e=>vL.includes(e);function bL(){let e=localStorage.getItem(_L);return yL(e)?e:`system`}var xL=window.matchMedia(`(prefers-color-scheme: dark)`),SL=M(xL.matches);xL.addEventListener(`change`,e=>{SL.value=e.matches});var CL=M(bL()),wL=q(()=>CL.value===`system`?SL.value:CL.value===`dark`);function TL(){document.documentElement.classList.toggle(`dark`,wL.value)}function EL(){Er(wL,TL,{immediate:!0,flush:`sync`})}function DL(e){CL.value=e,localStorage.setItem(_L,e),uf({theme:e}).catch(()=>{})}async function OL(){try{let{theme:e}=await lf();yL(e)&&e!==CL.value&&(CL.value=e,localStorage.setItem(_L,e))}catch{}}var kL={class:`flex flex-col items-center gap-0.5 rounded-lg bg-black/15 p-0.5`},AL=[`data-mode`,`aria-label`,`aria-pressed`,`onClick`],jL=F({__name:`ThemeToggle`,setup(e){let t=[{value:`light`,label:`Light`,icon:xd},{value:`system`,label:`System`,icon:nd},{value:`dark`,label:`Dark`,icon:rd}];return(e,n)=>(z(),B(`div`,kL,[(z(),B(R,null,I(t,e=>U(N(eC),{key:e.value},{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`button`,{type:`button`,"data-mode":e.value,"aria-label":e.label,"aria-pressed":N(CL)===e.value,class:A(N(lh)(`flex size-8 items-center justify-center rounded-md transition-colors`,N(CL)===e.value?`bg-primary text-primary-foreground`:`text-rail-foreground/70 hover:bg-white/10 hover:text-rail-foreground`)),onClick:t=>N(DL)(e.value)},[(z(),V(ia(e.icon),{class:`size-4`}))],10,AL)]),_:2},1024),U(N(qC),{side:`right`},{default:P(()=>[W(j(e.label),1)]),_:2},1024)]),_:2},1024)),64))]))}}),ML=F({__name:`Kbd`,props:{class:{}},setup(e){return(e,t)=>(z(),B(`kbd`,{class:A(N(lh)(`inline-flex h-5 min-w-5 items-center justify-center rounded border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground`,e.$props.class))},[L(e.$slots,`default`)],2))}}),NL=[{route:`editor`,keys:[`g`,`e`],label:`Editor`},{route:`analytics`,keys:[`g`,`a`],label:`Analytics`},{route:`glossary`,keys:[`g`,`g`],label:`Glossary`},{route:`screenshots`,keys:[`g`,`i`],label:`Screenshots`},{route:`settings`,keys:[`g`,`s`],label:`Settings`},{route:`activity`,keys:[`g`,`l`],label:`Activity`},{route:`docs`,keys:[`g`,`d`],label:`Docs`}];function PL(e,t){if(e.pending===`g`){let e=NL.find(e=>e.keys[1]===t);return{state:{pending:null},action:e?{type:`navigate`,route:e.route}:null}}return t===`g`?{state:{pending:`g`},action:null}:t===`?`?{state:{pending:null},action:{type:`toggleHelp`}}:{state:{pending:null},action:null}}var FL={class:`flex w-14 shrink-0 flex-col items-center gap-1 bg-rail py-3 text-rail-foreground`},IL=[`aria-label`,`onClick`],LL={key:0,class:`flex items-center gap-0.5`},RL={class:`mt-auto flex flex-col items-center gap-1.5`},zL={class:`font-mono text-[10px] text-rail-foreground/50`},BL=F({__name:`NavRail`,setup(e){let t=Mp(),n=[{id:`editor`,label:`Editor`,icon:Zu},{id:`analytics`,label:`Analytics`,icon:Eu},{id:`glossary`,label:`Glossary`,icon:Tu},{id:`screenshots`,label:`Screenshots`,icon:Yu},{id:`settings`,label:`Settings`,icon:md},{id:`activity`,label:`Activity`,icon:fd},{id:`docs`,label:`Docs`,icon:Cu}].map(e=>({...e,keys:NL.find(t=>t.route===e.id)?.keys}));return(e,r)=>(z(),V(N(XS),{"delay-duration":300},{default:P(()=>[H(`nav`,FL,[r[0]||=H(`div`,{class:`mb-3 flex size-9 items-center justify-center rounded-md bg-primary font-mono text-base font-semibold text-primary-foreground`},` G `,-1),(z(!0),B(R,null,I(N(n),e=>(z(),V(N(eC),{key:e.id},{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`button`,{type:`button`,"aria-label":e.label,class:A(N(lh)(`relative flex size-10 items-center justify-center rounded-md transition-colors`,N(t)===e.id?`bg-primary text-primary-foreground`:`text-rail-foreground/70 hover:bg-white/10 hover:text-rail-foreground`)),onClick:t=>N(jp)(e.id)},[(z(),V(ia(e.icon),{class:`size-5`}))],10,IL)]),_:2},1024),U(N(qC),{side:`right`,class:`flex items-center gap-2`},{default:P(()=>[H(`span`,null,j(e.label),1),e.keys?(z(),B(`span`,LL,[(z(!0),B(R,null,I(e.keys,(e,t)=>(z(),V(ML,{key:t},{default:P(()=>[W(j(e),1)]),_:2},1024))),128))])):G(``,!0)]),_:2},1024)]),_:2},1024))),128)),H(`div`,RL,[U(jL),H(`span`,zL,`v`+j(N(`0.8.7`)),1)])])]),_:1}))}}),VL={class:`flex flex-col gap-1`},HL={class:`flex items-center gap-1`},UL={class:`flex items-center justify-between py-1 text-sm`},WL={class:`flex items-center gap-1`},GL=F({__name:`ShortcutsDialog`,props:{open:{type:Boolean,required:!0},openModifiers:{}},emits:[`update:open`],setup(e){let t=Xa(e,`open`);return(e,n)=>(z(),V(N(C_),{open:t.value,"onUpdate:open":n[0]||=e=>t.value=e},{default:P(()=>[U(N(NC),{class:`max-w-sm`},{default:P(()=>[U(N(PC),null,{default:P(()=>[U(N(IC),null,{default:P(()=>[...n[1]||=[W(`Keyboard shortcuts`,-1)]]),_:1})]),_:1}),H(`ul`,VL,[(z(!0),B(R,null,I(N(NL),e=>(z(),B(`li`,{key:e.route,class:`flex items-center justify-between py-1 text-sm`},[H(`span`,null,j(e.label),1),H(`span`,HL,[(z(!0),B(R,null,I(e.keys,(e,t)=>(z(),V(ML,{key:t},{default:P(()=>[W(j(e),1)]),_:2},1024))),128))])]))),128)),H(`li`,UL,[n[3]||=H(`span`,null,`Search keys`,-1),H(`span`,WL,[U(ML,null,{default:P(()=>[...n[2]||=[W(`/`,-1)]]),_:1})])])])]),_:1})]),_:1},8,[`open`]))}}),KL=M(!1),qL=1e3;function JL(){let e={pending:null},t=null;function n(){t!==null&&(clearTimeout(t),t=null)}function r(r){if(r.metaKey||r.ctrlKey||r.altKey||r.repeat)return;let i=r.target;if(i&&(i.tagName===`INPUT`||i.tagName===`TEXTAREA`||i.isContentEditable))return;let a=r.key.toLowerCase(),o=!!document.querySelector(`[role="dialog"],[role="menu"],[role="listbox"]`),s=a===`?`&&KL.value;if(o&&!s)return;let c=PL(e,a);e=c.state,n(),e.pending===`g`&&(t=window.setTimeout(()=>{e={pending:null},t=null},qL));let{action:l}=c;l&&(l.type===`navigate`?jp(l.route):KL.value=!KL.value,r.preventDefault())}Gi(()=>window.addEventListener(`keydown`,r)),Yi(()=>{n(),window.removeEventListener(`keydown`,r)})}var YL={class:`flex h-screen bg-background text-foreground`},XL={class:`flex min-w-0 flex-1 flex-col`},ZL={class:`flex h-12 shrink-0 items-center justify-between border-b px-4`},QL={class:`flex min-w-0 items-center gap-3`},$L={key:0,class:`flex min-w-0 items-center gap-1.5`},eR={class:`max-w-[12rem] shrink-0 truncate font-mono text-sm font-medium`},tR={class:`truncate`},nR={key:0,class:`text-muted-foreground`},rR={class:`text-sm font-semibold`},iR={class:`font-mono`},aR={class:`flex min-h-0 flex-1 flex-col overflow-hidden`},oR=F({__name:`App`,setup(e){let t=Mp();JL();let n=un(null),r=M(null),i=M([]),a=q(()=>[...i.value].sort((e,t)=>{let n=e.relDir?`${e.relDir}/${e.name}`:e.name,r=t.relDir?`${t.relDir}/${t.name}`:t.name;return n.localeCompare(r)})),o=M(!1),s=M(null);async function c(){n.value=await jd()}Th(c),Gi(async()=>{await c(),Ch();try{[r.value,i.value]=await Promise.all([kf(),Af()]),document.title=r.value.project?`${r.value.project} — Glotfile`:`Glotfile`}catch(e){Q.error(e.message)}n.value&&Object.keys(n.value.keys).length===0&&(o.value=!0)});function l(){location.reload()}async function u(e){if(e!==r.value?.path)try{await jf(e),location.reload()}catch(e){Q.error(e.message)}}let d=q(()=>({editor:`Editor`,analytics:`Analytics`,glossary:`Glossary`,screenshots:`Screenshots`,settings:`Settings`,activity:`Activity`,docs:`Docs`})[t.value]),f=q(()=>{if(!n.value)return null;let{sourceLocale:e,locales:t}=n.value.config;return{source:e,targets:t.filter(t=>t!==e)}});return(e,n)=>(z(),V(N(XS),{"delay-duration":300},{default:P(()=>[H(`div`,YL,[U(BL),H(`div`,XL,[H(`header`,ZL,[H(`div`,QL,[r.value?(z(),B(`div`,$L,[U(N(eC),null,{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`span`,eR,j(r.value.project),1)]),_:1}),U(N(qC),{side:`bottom`,class:`font-mono`},{default:P(()=>[W(j(r.value.dir),1)]),_:1})]),_:1}),U(N(ku),{class:`size-3.5 shrink-0 text-muted-foreground`}),U(N(Qx),null,{default:P(()=>[U(N(iS),{"as-child":``},{default:P(()=>[U(N($),{variant:`outline`,size:`sm`,class:`max-w-[16rem] gap-1.5 font-mono`},{default:P(()=>[H(`span`,tR,j(r.value.name),1),U(N(Au),{class:`size-3.5 shrink-0 text-muted-foreground`})]),_:1})]),_:1}),U(N(RC),{align:`start`,class:`w-max`},{default:P(()=>[(z(!0),B(R,null,I(a.value,e=>(z(),V(N(zC),{key:e.path,class:`font-mono`,onSelect:t=>u(e.path)},{default:P(()=>[U(N(Du),{class:A([`size-4 shrink-0`,e.path===r.value.path?`opacity-100`:`opacity-0`])},null,8,[`class`]),H(`span`,null,[e.relDir?(z(),B(`span`,nR,j(e.relDir)+`/`,1)):G(``,!0),W(j(e.name),1)])]),_:2},1032,[`onSelect`]))),128))]),_:1})]),_:1})])):G(``,!0),H(`h1`,rR,j(d.value),1)]),f.value?(z(),V(N(eC),{key:0},{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`button`,{type:`button`,class:`shrink-0 font-mono text-xs text-muted-foreground transition-colors hover:text-foreground`,onClick:n[0]||=e=>N(jp)(`settings`)},j(f.value.source)+` → `+j(f.value.targets.length)+` `+j(f.value.targets.length===1?`locale`:`locales`),1)]),_:1}),U(N(qC),{side:`bottom`,class:`max-w-[28rem] leading-relaxed`},{default:P(()=>[H(`div`,iR,j(f.value.targets.join(`, `)||`—`),1),n[4]||=H(`div`,{class:`mt-1 text-background/60`},`Click to manage in Settings`,-1)]),_:1})]),_:1})):G(``,!0)]),H(`main`,aR,[N(t)===`editor`?(z(),V(NO,{key:0})):N(t)===`analytics`?(z(),V(DA,{key:1})):N(t)===`glossary`?(z(),V($A,{key:2})):N(t)===`screenshots`?(z(),V(hF,{key:3})):N(t)===`settings`?(z(),V($P,{key:4})):N(t)===`activity`?(z(),V(ZF,{key:5})):N(t)===`docs`?(z(),V(pI,{key:6})):G(``,!0)])]),U(N(hh)),U(GL,{open:N(KL),"onUpdate:open":n[1]||=e=>ln(KL)?KL.value=e:null},null,8,[`open`]),o.value?(z(),V(gL,{key:0,ref_key:`wizardRef`,ref:s,onVnodeMounted:n[2]||=e=>s.value?.init(),onDismiss:n[3]||=e=>o.value=!1,onImported:l},null,512)):G(``,!0)])]),_:1}))}});EL(),iu(oR).mount(`#app`),OL(),sO(),TT();
2371
+ `,text:`Troubleshooting and FAQ Setup Run it with (no global install needed), or install globally with . From a checkout, build first: . See Installation. Wrong Node version Glotfile needs Node . Check with . Translation "Install the SDK for this provider" OpenAI, OpenRouter, and Bedrock need an optional SDK. Install the one for your provider — (OpenAI or OpenRouter) or (Bedrock). See AI Providers. No API key / auth errors Set the provider's credentials in your environment or a local : , , , or the AWS chain. See AI Providers. A translation was skipped The result failed validation — a dropped placeholder, broken ICU, or an over-length value. Glotfile prints the reason and leaves the value untranslated. See Placeholders and ICU and How Translation Works. My reviewed translation didn't change after That's intentional — values are protected from being overwritten. Unmark it first to re-translate. Screenshots seem ignored The selected model may not support vision (e.g. Bedrock Meta Llama text models). The run proceeds text-only and warns how many screenshots were skipped. See Screenshots. Validation and CI fails with Your exported files don't match the catalog. Run and commit the result. See Continuous Integration. Spelling flags real words Add them to the custom dictionary in Settings ( ). The same list silences both the editor's live spell check and the lint rule. Glossary terms are accepted automatically. See Checks and Validation. A rule is too noisy Override its severity (or turn it off) in , or certain key globs. See Configuration Reference. Files and git Can I edit by hand? You can, but you rarely need to — the UI writes it for you, deterministically. If you do edit it, keep it valid: it's validated on load. See The State File. Big diffs on every save Check ( , , ). Deterministic formatting keeps diffs minimal. See The State File. General Do I already have translations I can import? Import existing locale files with — see import. You can also add keys via the Editor. Does anything leave my machine? Only the AI calls you trigger. The UI is local-only; no credentials or screenshot bytes are ever logged. See AI Log. Related - Home · Installation · AI Providers · Continuous Integration`}],$F={class:`flex min-h-0 flex-1 overflow-hidden`},eI={class:`flex w-56 shrink-0 flex-col overflow-hidden border-r bg-muted/30`},tI={class:`relative shrink-0 px-2 py-2`},nI={key:0,class:`flex flex-col gap-1 overflow-y-auto px-2 pb-3`},rI={class:`px-2 py-1 text-xs text-muted-foreground`},iI=[`onClick`],aI={class:`text-sm font-medium`},oI={key:0,class:`text-[10px] uppercase tracking-wider text-muted-foreground`},sI={key:1,class:`mt-0.5 line-clamp-2 text-xs text-muted-foreground`},cI={class:`bg-primary/20 text-foreground`},lI={key:1,class:`flex flex-col gap-1 overflow-y-auto px-2 pb-3`},uI=[`onClick`],dI={class:`min-w-0 flex-1 overflow-y-auto`},fI=[`innerHTML`],pI=F({__name:`DocsView`,setup(e){let t=[``,`Getting Started`,`Frameworks`,`Web UI`,`CLI`,`Concepts`,`AI Translation`,`Reference`,`Guides`,`Help`];function n(){let e=Np().get(`doc`);return e&&QF.some(t=>t.id===e)?e:QF[0]?.id??``}let r=M(n()),i=M(``),a=q(()=>{let e=new Map;for(let t of QF)e.has(t.section)||e.set(t.section,[]),e.get(t.section).push(t);return[...t,...[...e.keys()].filter(e=>!t.includes(e))].map(t=>({section:t,pages:e.get(t)??[]})).filter(e=>e.pages.length>0)}),o=q(()=>{let e=i.value.trim().toLowerCase();if(!e)return[];let t=[];for(let n of QF){let r=n.text.toLowerCase(),i=n.title.toLowerCase().includes(e),a=r.indexOf(e);if(a===-1&&!i)continue;if(a===-1){t.push({id:n.id,title:n.title,section:n.section,before:``,match:``,after:``});continue}let o=Math.max(0,a-40),s=Math.min(n.text.length,a+e.length+80);t.push({id:n.id,title:n.title,section:n.section,before:(o>0?`…`:``)+n.text.slice(o,a),match:n.text.slice(a,a+e.length),after:n.text.slice(a+e.length,s)+(s<n.text.length?`…`:``)})}return t}),s=q(()=>QF.find(e=>e.id===r.value)??QF[0]);function c(e){e!==r.value&&(r.value=e,history.pushState(null,``,`#docs?${new URLSearchParams({doc:e})}`))}function l(e){c(e),i.value=``}function u(){r.value=n()}return Gi(()=>window.addEventListener(`hashchange`,u)),Yi(()=>window.removeEventListener(`hashchange`,u)),(e,t)=>(z(),B(`div`,$F,[H(`nav`,eI,[H(`div`,tI,[U(N(pd),{class:`pointer-events-none absolute left-4 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground`}),gr(H(`input`,{"onUpdate:modelValue":t[0]||=e=>i.value=e,type:`search`,placeholder:`Search docs…`,class:`w-full rounded-md border bg-background py-1 pl-7 pr-7 text-sm outline-none focus:ring-2 focus:ring-ring`},null,512),[[Pl,i.value]]),i.value?(z(),B(`button`,{key:0,type:`button`,class:`absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground`,onClick:t[1]||=e=>i.value=``},[U(N(kd),{class:`size-3.5`})])):G(``,!0)]),i.value.trim()?(z(),B(`div`,nI,[H(`div`,rI,j(o.value.length)+` result`+j(o.value.length===1?``:`s`),1),(z(!0),B(R,null,I(o.value,e=>(z(),B(`button`,{key:e.id,type:`button`,class:`w-full rounded-md px-2 py-1.5 text-left transition-colors hover:bg-accent hover:text-accent-foreground`,onClick:t=>l(e.id)},[H(`div`,aI,j(e.title),1),e.section?(z(),B(`div`,oI,j(e.section),1)):G(``,!0),e.match?(z(),B(`div`,sI,[W(j(e.before),1),H(`mark`,cI,j(e.match),1),W(j(e.after),1)])):G(``,!0)],8,iI))),128))])):(z(),B(`div`,lI,[(z(!0),B(R,null,I(a.value,(e,t)=>(z(),B(R,{key:e.section},[e.section?(z(),B(`div`,{key:0,class:A([`px-2 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground`,t>0?`mt-3`:``])},j(e.section),3)):G(``,!0),(z(!0),B(R,null,I(e.pages,e=>(z(),B(`button`,{key:e.id,type:`button`,class:A([`w-full rounded-md px-2 py-1 text-left text-sm transition-colors`,r.value===e.id?`bg-primary text-primary-foreground`:`text-foreground hover:bg-accent hover:text-accent-foreground`]),onClick:t=>c(e.id)},j(e.title),11,uI))),128))],64))),128))]))]),H(`div`,dI,[H(`div`,{class:`prose prose-sm dark:prose-invert mx-auto max-w-3xl px-8 py-6`,innerHTML:s.value?.html},null,8,fI)])]))}}),mI={en:`English`,fr:`French`,de:`German`,es:`Spanish`,pt:`Portuguese`,it:`Italian`,nl:`Dutch`,ca:`Catalan`,pl:`Polish`,ru:`Russian`,uk:`Ukrainian`,cs:`Czech`,sk:`Slovak`,sv:`Swedish`,da:`Danish`,no:`Norwegian`,nb:`Norwegian Bokmål`,fi:`Finnish`,tr:`Turkish`,el:`Greek`,ro:`Romanian`,hu:`Hungarian`,bg:`Bulgarian`,hr:`Croatian`,sr:`Serbian`,sl:`Slovenian`,ja:`Japanese`,ko:`Korean`,zh:`Chinese`,ar:`Arabic`,he:`Hebrew`,hi:`Hindi`,th:`Thai`,vi:`Vietnamese`,id:`Indonesian`,ms:`Malay`,fa:`Persian`};function hI(e){let[t=``,n]=e.split(/[-_]/),r=t.toLowerCase(),i=n&&/^[A-Za-z]{2}$/.test(n)?n.toUpperCase():void 0,a=mI[r]??e;return{code:e,name:i?`${a} (${i})`:a}}var gI={class:`p-6`},_I={class:`flex items-start gap-3 pr-6`},vI={class:`grid h-10 w-10 shrink-0 place-items-center rounded-lg border border-primary/25 bg-primary/10 font-mono text-[13px] font-semibold leading-none tracking-tight text-primary`},yI={class:`min-w-0`},bI={class:`font-medium text-foreground`},xI={class:`font-mono text-[12px]`},SI={class:`mt-4 flex items-center gap-4 rounded-lg border border-border bg-muted/40 px-3.5 py-2.5`},CI={class:`flex flex-col`},wI={class:`text-[17px] font-semibold leading-none tabular-nums`},TI={class:`flex flex-col`},EI={class:`text-[17px] font-semibold leading-none tabular-nums`},DI={class:`ml-auto flex items-center gap-1.5 whitespace-nowrap text-[11px] text-muted-foreground`},OI={class:`mt-5`},kI={class:`!flex min-w-0 items-center gap-2`},AI={class:`font-mono text-[13px]`},jI={class:`truncate text-[13px] text-muted-foreground`},MI={class:`flex items-center gap-2`},NI={class:`font-mono text-[13px]`},PI={class:`text-[13px] text-muted-foreground`},FI={class:`mt-4`},II={class:`flex items-center justify-between`},LI={class:`whitespace-nowrap text-[12px] tabular-nums text-muted-foreground`},RI={class:`mt-1.5 flex flex-wrap gap-1.5 rounded-lg border border-border bg-muted/30 p-2.5`},zI=[`disabled`,`onClick`],BI={class:`font-mono`},VI={key:0,class:`ml-0.5 rounded-sm bg-primary/15 px-1 text-[9px] font-semibold uppercase tracking-wide text-primary`},HI={key:0,class:`mt-4`},UI={class:`overflow-hidden rounded-lg border border-border`},WI={class:`ml-auto font-mono text-[11px] text-muted-foreground/70`},GI={key:0,class:`divide-y divide-border border-t border-border gf-content-fade`},KI={class:`shrink-0 font-mono text-foreground`},qI={class:`ml-auto truncate text-right text-muted-foreground`},JI={key:0,class:`px-3 py-1.5 text-[11px] text-muted-foreground/70`},YI={class:`flex items-center gap-2 border-t border-border px-6 py-4`},XI={class:`ml-1 flex cursor-pointer select-none items-center gap-1.5 text-[12px] text-muted-foreground`},ZI={class:`ml-auto`},QI={class:`flex flex-col items-center justify-center gap-4 p-6 py-8 text-center`},$I={key:0,class:`mt-1 text-[13px] text-muted-foreground`},eL={class:`p-6`},tL={class:`flex flex-col items-center gap-3 pt-1 text-center`},nL={class:`grid h-12 w-12 place-items-center rounded-full bg-success-bg text-success ring-1 ring-success-border`},rL={class:`font-semibold text-foreground tabular-nums`},iL={class:`font-semibold text-foreground`},aL={key:0,class:`mt-5 overflow-hidden rounded-lg border border-warning-border bg-warning-bg/60`},oL={class:`whitespace-nowrap text-[13px] font-medium text-warning`},sL={key:0,class:`max-h-44 divide-y divide-warning-border/40 overflow-auto border-t border-warning-border/70 gf-content-fade`},cL={class:`flex items-center border-t border-border px-6 py-4`},lL={class:`ml-auto`},uL={class:`p-6`},dL={class:`flex items-start gap-3 pr-6`},fL={class:`grid h-10 w-10 shrink-0 place-items-center rounded-full bg-destructive-soft text-destructive`},pL={class:`min-w-0`},mL={class:`flex items-center border-t border-border px-6 py-4`},hL={class:`ml-auto`},gL=F({__name:`ImportWizard`,emits:[`dismiss`,`imported`],setup(e,{expose:t,emit:n}){let r=n,i=M(!0),a=M(`confirm`),o=M(null),s=M(``),c=M({}),l=M(!1),u=M(!1),d=M(null),f=M(``),p=M(!0);t({init:m});async function m(){try{let e=await Lf();if(!e.found){r(`dismiss`);return}o.value=e,s.value=e.sourceLocale}catch{r(`dismiss`)}}let h={"laravel-php":{label:`Laravel PHP`,glyph:`</>`,file:`lang/{locale}/*.php`},"vue-i18n-json":{label:`Vue i18n JSON`,glyph:`{ }`,file:`locales/{locale}.json`},"flutter-arb":{label:`Flutter ARB`,glyph:`arb`,file:`lib/l10n/app_{locale}.arb`},"apple-strings":{label:`Apple .strings`,glyph:``,file:`{locale}.lproj/Localizable.strings`}},g=q(()=>h[o.value?.format??``]??{label:o.value?.format??``,glyph:`{}`,file:``});function _(e){return e===s.value||!c.value[e]}function v(e){e!==s.value&&(c.value={...c.value,[e]:!c.value[e]})}let y=q(()=>o.value?o.value.locales.filter(_):[]),b=q(()=>y.value.length),x=q(()=>o.value?Math.max(0,o.value.keyCount-o.value.sampleKeys.length):0);async function S(){if(o.value){a.value=`importing`;try{d.value=await Rf({format:o.value.format,sourceLocale:s.value,locales:y.value,cldr:p.value}),a.value=`done`}catch(e){f.value=e.message,a.value=`error`}}}function C(e){e||a.value!==`importing`&&(a.value===`done`?r(`imported`):r(`dismiss`))}return(e,t)=>(z(),V(N(C_),{open:i.value,"onUpdate:open":C},{default:P(()=>[U(N(uv),null,{default:P(()=>[U(N(cv),{class:`fixed inset-0 z-50 bg-foreground/40 gf-content-fade`}),U(N(av),{class:`fixed left-1/2 top-1/2 z-50 w-[28rem] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-card text-card-foreground shadow-2xl focus:outline-none gf-content-fade`,onInteractOutside:t[9]||=Jl(()=>{},[`prevent`])},{default:P(()=>[a.value===`confirm`&&o.value?(z(),B(R,{key:0},[H(`button`,{type:`button`,class:`absolute right-3.5 top-3.5 z-10 grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground`,onClick:t[0]||=e=>r(`dismiss`)},[U(N(kd),{class:`size-4`})]),H(`div`,gI,[H(`div`,_I,[H(`div`,vI,j(g.value.glyph),1),H(`div`,yI,[U(N(dv),{class:`text-base font-semibold leading-tight`},{default:P(()=>[...t[10]||=[W(`Import your translations`,-1)]]),_:1}),U(N(ov),{class:`mt-0.5 text-[13px] text-muted-foreground`},{default:P(()=>[t[11]||=W(` Found a `,-1),H(`span`,bI,j(g.value.label),1),t[12]||=W(` setup in `,-1),H(`span`,xI,j(g.value.file),1),t[13]||=W(`. `,-1)]),_:1})])]),H(`div`,SI,[H(`div`,CI,[H(`span`,wI,j(o.value.keyCount.toLocaleString()),1),t[14]||=H(`span`,{class:`mt-1 whitespace-nowrap text-[11px] text-muted-foreground`},`keys detected`,-1)]),t[17]||=H(`div`,{class:`h-7 w-px bg-border`},null,-1),H(`div`,TI,[H(`span`,EI,j(o.value.locales.length),1),t[15]||=H(`span`,{class:`mt-1 whitespace-nowrap text-[11px] text-muted-foreground`},`locales`,-1)]),H(`div`,DI,[U(N(Gu),{class:`size-3.5 opacity-70`}),t[16]||=W(` Detected automatically `,-1)])]),H(`div`,OI,[U(N(MC),{class:`flex items-center gap-1.5 whitespace-nowrap`},{default:P(()=>[...t[18]||=[W(` Source language `,-1),H(`span`,{class:`text-[12px] font-normal text-muted-foreground`},`· the original text`,-1)]]),_:1}),U(N(mS),{modelValue:s.value,"onUpdate:modelValue":t[1]||=e=>s.value=e},{default:P(()=>[U(N(cC),{class:`mt-1.5`},{default:P(()=>[H(`span`,kI,[U(jw,{code:s.value,size:14},null,8,[`code`]),H(`span`,AI,j(s.value),1),H(`span`,jI,j(N(hI)(s.value).name),1)])]),_:1}),U(N(lC),null,{default:P(()=>[(z(!0),B(R,null,I(o.value.locales,e=>(z(),V(N(dC),{key:e,value:e},{default:P(()=>[H(`span`,MI,[U(jw,{code:e,size:14},null,8,[`code`]),H(`span`,NI,j(e),1),H(`span`,PI,j(N(hI)(e).name),1)])]),_:2},1032,[`value`]))),128))]),_:1})]),_:1},8,[`modelValue`])]),H(`div`,FI,[H(`div`,II,[U(N(MC),{class:`whitespace-nowrap`},{default:P(()=>[...t[19]||=[W(`Locales to import`,-1)]]),_:1}),H(`span`,LI,j(b.value)+` of `+j(o.value.locales.length),1)]),H(`div`,RI,[(z(!0),B(R,null,I(o.value.locales,e=>(z(),V(N(eC),{key:e},{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`button`,{type:`button`,disabled:e===s.value,class:A([`group inline-flex h-7 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border pl-1.5 pr-2 text-[12px] transition-colors`,e===s.value?`border-primary/40 bg-primary/10 cursor-default`:_(e)?`border-input bg-card text-foreground hover:bg-muted`:`border-dashed border-border bg-transparent text-muted-foreground hover:bg-muted/50`]),onClick:t=>v(e)},[U(jw,{code:e,size:14},null,8,[`code`]),H(`span`,BI,j(e),1),e===s.value?(z(),B(`span`,VI,`src`)):(z(),B(`span`,{key:1,class:A([`grid size-3.5 place-items-center rounded-[3px] border`,_(e)?`border-primary bg-primary text-primary-foreground`:`border-border text-transparent`])},[U(N(Du),{class:`size-2.5`,"stroke-width":3})],2))],10,zI)]),_:2},1024),U(N(qC),null,{default:P(()=>[W(j(e===s.value?`Source language — always imported`:_(e)?`Click to exclude`:`Click to include`),1)]),_:2},1024)]),_:2},1024))),128))])]),o.value.sampleKeys.length?(z(),B(`div`,HI,[H(`div`,UI,[H(`button`,{type:`button`,class:`flex w-full items-center gap-2 px-3 py-2 text-[13px] text-muted-foreground transition-colors hover:bg-muted/50`,onClick:t[2]||=e=>l.value=!l.value},[U(N(ku),{class:A([`size-3.5 shrink-0 transition-transform`,l.value?`rotate-90`:``])},null,8,[`class`]),t[20]||=H(`span`,{class:`whitespace-nowrap`},`Preview sample keys`,-1),H(`span`,WI,j(g.value.glyph),1)]),l.value?(z(),B(`div`,GI,[(z(!0),B(R,null,I(o.value.sampleKeys,e=>(z(),B(`div`,{key:e.key,class:`flex items-baseline gap-3 px-3 py-1.5 text-[12px]`},[H(`span`,KI,j(e.key),1),H(`span`,qI,j(e.value),1)]))),128)),x.value>0?(z(),B(`div`,JI,` + `+j(x.value.toLocaleString())+` more keys `,1)):G(``,!0)])):G(``,!0)])])):G(``,!0)]),H(`div`,YI,[U(N($),{variant:`ghost`,size:`sm`,onClick:t[3]||=e=>r(`dismiss`)},{default:P(()=>[...t[21]||=[W(`Skip`,-1)]]),_:1}),U(N(eC),null,{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`label`,XI,[gr(H(`input`,{type:`checkbox`,"data-testid":`cldr-toggle`,"onUpdate:modelValue":t[4]||=e=>p.value=e,class:`size-3.5 accent-primary`},null,512),[[Fl,p.value]]),t[22]||=W(` Convert plurals to CLDR categories `,-1)])]),_:1}),U(N(qC),{class:`max-w-xs`},{default:P(()=>[...t[23]||=[W(`Rewrite exact =N plural selectors (e.g. =1) into each language's CLDR plural categories, the way Crowdin does.`,-1)]]),_:1})]),_:1}),H(`div`,ZI,[U(N($),{"data-testid":`import-btn`,disabled:b.value===0,onClick:S},{default:P(()=>[U(N(Ad),{class:`size-4`}),W(` Import `+j(b.value)+` `+j(b.value===1?`locale`:`locales`),1)]),_:1},8,[`disabled`])])])],64)):a.value===`importing`?(z(),B(R,{key:1},[U(N(dv),{class:`sr-only`},{default:P(()=>[...t[24]||=[W(`Importing translations`,-1)]]),_:1}),H(`div`,QI,[U(N($u),{class:`size-10 text-primary gf-spin`}),H(`div`,null,[t[25]||=H(`div`,{class:`text-sm font-semibold`},`Importing translations…`,-1),o.value?(z(),B(`div`,$I,` Reading `+j(o.value.keyCount.toLocaleString())+` keys across `+j(b.value)+` locales `,1)):G(``,!0)])])],64)):a.value===`done`&&d.value?(z(),B(R,{key:2},[H(`div`,eL,[H(`div`,tL,[H(`div`,nL,[U(N(Du),{class:`size-6`,"stroke-width":2.5})]),H(`div`,null,[U(N(dv),{class:`text-base font-semibold`},{default:P(()=>[...t[26]||=[W(`Import complete`,-1)]]),_:1}),U(N(ov),{class:`mt-1 text-[13px] text-muted-foreground`},{default:P(()=>[H(`span`,rL,j(d.value.keyCount.toLocaleString())+` keys`,1),t[27]||=W(` across `,-1),H(`span`,iL,j(d.value.localeCount)+` locales`,1),t[28]||=W(` imported. `,-1)]),_:1})])]),d.value.warnings.length?(z(),B(`div`,aL,[H(`button`,{type:`button`,class:`flex w-full items-center gap-2 px-3 py-2.5 text-left`,onClick:t[5]||=e=>u.value=!u.value},[U(N(Td),{class:`size-3.5 shrink-0 text-warning`}),H(`span`,oL,j(d.value.warnings.length)+` `+j(d.value.warnings.length===1?`warning`:`warnings`),1),t[29]||=H(`span`,{class:`whitespace-nowrap text-[12px] text-warning/70`},`· import still succeeded`,-1),U(N(ku),{class:A([`ml-auto size-3.5 shrink-0 text-warning transition-transform`,u.value?`rotate-90`:``])},null,8,[`class`])]),u.value?(z(),B(`ul`,sL,[(z(!0),B(R,null,I(d.value.warnings,(e,t)=>(z(),B(`li`,{key:t,class:`px-3 py-2 text-[12px] text-muted-foreground`},j(e),1))),128))])):G(``,!0)])):G(``,!0)]),H(`div`,cL,[H(`div`,lL,[U(N($),{onClick:t[6]||=e=>r(`imported`)},{default:P(()=>[t[30]||=W(`Open editor `,-1),U(N(bu),{class:`size-4`})]),_:1})])])],64)):a.value===`error`?(z(),B(R,{key:3},[H(`button`,{type:`button`,class:`absolute right-3.5 top-3.5 z-10 grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground`,onClick:t[7]||=e=>r(`dismiss`)},[U(N(kd),{class:`size-4`})]),H(`div`,uL,[H(`div`,dL,[H(`div`,fL,[U(N(Td),{class:`size-5`})]),H(`div`,pL,[U(N(dv),{class:`text-base font-semibold`},{default:P(()=>[...t[31]||=[W(`Couldn't import translations`,-1)]]),_:1}),U(N(ov),{class:`mt-1 text-[13px] text-muted-foreground`},{default:P(()=>[W(j(f.value),1)]),_:1})])])]),H(`div`,mL,[H(`div`,hL,[U(N($),{variant:`outline`,onClick:t[8]||=e=>r(`dismiss`)},{default:P(()=>[...t[32]||=[W(`Dismiss`,-1)]]),_:1})])])],64)):G(``,!0)]),_:1})]),_:1})]),_:1},8,[`open`]))}}),_L=`glotfile-theme`,vL=[`system`,`light`,`dark`],yL=e=>vL.includes(e);function bL(){let e=localStorage.getItem(_L);return yL(e)?e:`system`}var xL=window.matchMedia(`(prefers-color-scheme: dark)`),SL=M(xL.matches);xL.addEventListener(`change`,e=>{SL.value=e.matches});var CL=M(bL()),wL=q(()=>CL.value===`system`?SL.value:CL.value===`dark`);function TL(){document.documentElement.classList.toggle(`dark`,wL.value)}function EL(){Er(wL,TL,{immediate:!0,flush:`sync`})}function DL(e){CL.value=e,localStorage.setItem(_L,e),uf({theme:e}).catch(()=>{})}async function OL(){try{let{theme:e}=await lf();yL(e)&&e!==CL.value&&(CL.value=e,localStorage.setItem(_L,e))}catch{}}var kL={class:`flex flex-col items-center gap-0.5 rounded-lg bg-black/15 p-0.5`},AL=[`data-mode`,`aria-label`,`aria-pressed`,`onClick`],jL=F({__name:`ThemeToggle`,setup(e){let t=[{value:`light`,label:`Light`,icon:xd},{value:`system`,label:`System`,icon:nd},{value:`dark`,label:`Dark`,icon:rd}];return(e,n)=>(z(),B(`div`,kL,[(z(),B(R,null,I(t,e=>U(N(eC),{key:e.value},{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`button`,{type:`button`,"data-mode":e.value,"aria-label":e.label,"aria-pressed":N(CL)===e.value,class:A(N(lh)(`flex size-8 items-center justify-center rounded-md transition-colors`,N(CL)===e.value?`bg-primary text-primary-foreground`:`text-rail-foreground/70 hover:bg-white/10 hover:text-rail-foreground`)),onClick:t=>N(DL)(e.value)},[(z(),V(ia(e.icon),{class:`size-4`}))],10,AL)]),_:2},1024),U(N(qC),{side:`right`},{default:P(()=>[W(j(e.label),1)]),_:2},1024)]),_:2},1024)),64))]))}}),ML=F({__name:`Kbd`,props:{class:{}},setup(e){return(e,t)=>(z(),B(`kbd`,{class:A(N(lh)(`inline-flex h-5 min-w-5 items-center justify-center rounded border bg-muted px-1.5 font-mono text-xs font-medium text-muted-foreground`,e.$props.class))},[L(e.$slots,`default`)],2))}}),NL=[{route:`editor`,keys:[`g`,`e`],label:`Editor`},{route:`analytics`,keys:[`g`,`a`],label:`Analytics`},{route:`glossary`,keys:[`g`,`g`],label:`Glossary`},{route:`screenshots`,keys:[`g`,`i`],label:`Screenshots`},{route:`settings`,keys:[`g`,`s`],label:`Settings`},{route:`activity`,keys:[`g`,`l`],label:`Activity`},{route:`docs`,keys:[`g`,`d`],label:`Docs`}];function PL(e,t){if(e.pending===`g`){let e=NL.find(e=>e.keys[1]===t);return{state:{pending:null},action:e?{type:`navigate`,route:e.route}:null}}return t===`g`?{state:{pending:`g`},action:null}:t===`?`?{state:{pending:null},action:{type:`toggleHelp`}}:{state:{pending:null},action:null}}var FL={class:`flex w-14 shrink-0 flex-col items-center gap-1 bg-rail py-3 text-rail-foreground`},IL=[`aria-label`,`onClick`],LL={key:0,class:`flex items-center gap-0.5`},RL={class:`mt-auto flex flex-col items-center gap-1.5`},zL={class:`font-mono text-[10px] text-rail-foreground/50`},BL=F({__name:`NavRail`,setup(e){let t=Mp(),n=[{id:`editor`,label:`Editor`,icon:Zu},{id:`analytics`,label:`Analytics`,icon:Eu},{id:`glossary`,label:`Glossary`,icon:Tu},{id:`screenshots`,label:`Screenshots`,icon:Yu},{id:`settings`,label:`Settings`,icon:md},{id:`activity`,label:`Activity`,icon:fd},{id:`docs`,label:`Docs`,icon:Cu}].map(e=>({...e,keys:NL.find(t=>t.route===e.id)?.keys}));return(e,r)=>(z(),V(N(XS),{"delay-duration":300},{default:P(()=>[H(`nav`,FL,[r[0]||=H(`div`,{class:`mb-3 flex size-9 items-center justify-center rounded-md bg-primary font-mono text-base font-semibold text-primary-foreground`},` G `,-1),(z(!0),B(R,null,I(N(n),e=>(z(),V(N(eC),{key:e.id},{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`button`,{type:`button`,"aria-label":e.label,class:A(N(lh)(`relative flex size-10 items-center justify-center rounded-md transition-colors`,N(t)===e.id?`bg-primary text-primary-foreground`:`text-rail-foreground/70 hover:bg-white/10 hover:text-rail-foreground`)),onClick:t=>N(jp)(e.id)},[(z(),V(ia(e.icon),{class:`size-5`}))],10,IL)]),_:2},1024),U(N(qC),{side:`right`,class:`flex items-center gap-2`},{default:P(()=>[H(`span`,null,j(e.label),1),e.keys?(z(),B(`span`,LL,[(z(!0),B(R,null,I(e.keys,(e,t)=>(z(),V(ML,{key:t},{default:P(()=>[W(j(e),1)]),_:2},1024))),128))])):G(``,!0)]),_:2},1024)]),_:2},1024))),128)),H(`div`,RL,[U(jL),H(`span`,zL,`v`+j(N(`0.8.8`)),1)])])]),_:1}))}}),VL={class:`flex flex-col gap-1`},HL={class:`flex items-center gap-1`},UL={class:`flex items-center justify-between py-1 text-sm`},WL={class:`flex items-center gap-1`},GL=F({__name:`ShortcutsDialog`,props:{open:{type:Boolean,required:!0},openModifiers:{}},emits:[`update:open`],setup(e){let t=Xa(e,`open`);return(e,n)=>(z(),V(N(C_),{open:t.value,"onUpdate:open":n[0]||=e=>t.value=e},{default:P(()=>[U(N(NC),{class:`max-w-sm`},{default:P(()=>[U(N(PC),null,{default:P(()=>[U(N(IC),null,{default:P(()=>[...n[1]||=[W(`Keyboard shortcuts`,-1)]]),_:1})]),_:1}),H(`ul`,VL,[(z(!0),B(R,null,I(N(NL),e=>(z(),B(`li`,{key:e.route,class:`flex items-center justify-between py-1 text-sm`},[H(`span`,null,j(e.label),1),H(`span`,HL,[(z(!0),B(R,null,I(e.keys,(e,t)=>(z(),V(ML,{key:t},{default:P(()=>[W(j(e),1)]),_:2},1024))),128))])]))),128)),H(`li`,UL,[n[3]||=H(`span`,null,`Search keys`,-1),H(`span`,WL,[U(ML,null,{default:P(()=>[...n[2]||=[W(`/`,-1)]]),_:1})])])])]),_:1})]),_:1},8,[`open`]))}}),KL=M(!1),qL=1e3;function JL(){let e={pending:null},t=null;function n(){t!==null&&(clearTimeout(t),t=null)}function r(r){if(r.metaKey||r.ctrlKey||r.altKey||r.repeat)return;let i=r.target;if(i&&(i.tagName===`INPUT`||i.tagName===`TEXTAREA`||i.isContentEditable))return;let a=r.key.toLowerCase(),o=!!document.querySelector(`[role="dialog"],[role="menu"],[role="listbox"]`),s=a===`?`&&KL.value;if(o&&!s)return;let c=PL(e,a);e=c.state,n(),e.pending===`g`&&(t=window.setTimeout(()=>{e={pending:null},t=null},qL));let{action:l}=c;l&&(l.type===`navigate`?jp(l.route):KL.value=!KL.value,r.preventDefault())}Gi(()=>window.addEventListener(`keydown`,r)),Yi(()=>{n(),window.removeEventListener(`keydown`,r)})}var YL={class:`flex h-screen bg-background text-foreground`},XL={class:`flex min-w-0 flex-1 flex-col`},ZL={class:`flex h-12 shrink-0 items-center justify-between border-b px-4`},QL={class:`flex min-w-0 items-center gap-3`},$L={key:0,class:`flex min-w-0 items-center gap-1.5`},eR={class:`max-w-[12rem] shrink-0 truncate font-mono text-sm font-medium`},tR={class:`truncate`},nR={key:0,class:`text-muted-foreground`},rR={class:`text-sm font-semibold`},iR={class:`font-mono`},aR={class:`flex min-h-0 flex-1 flex-col overflow-hidden`},oR=F({__name:`App`,setup(e){let t=Mp();JL();let n=un(null),r=M(null),i=M([]),a=q(()=>[...i.value].sort((e,t)=>{let n=e.relDir?`${e.relDir}/${e.name}`:e.name,r=t.relDir?`${t.relDir}/${t.name}`:t.name;return n.localeCompare(r)})),o=M(!1),s=M(null);async function c(){n.value=await jd()}Th(c),Gi(async()=>{await c(),Ch();try{[r.value,i.value]=await Promise.all([kf(),Af()]),document.title=r.value.project?`${r.value.project} — Glotfile`:`Glotfile`}catch(e){Q.error(e.message)}n.value&&Object.keys(n.value.keys).length===0&&(o.value=!0)});function l(){location.reload()}async function u(e){if(e!==r.value?.path)try{await jf(e),location.reload()}catch(e){Q.error(e.message)}}let d=q(()=>({editor:`Editor`,analytics:`Analytics`,glossary:`Glossary`,screenshots:`Screenshots`,settings:`Settings`,activity:`Activity`,docs:`Docs`})[t.value]),f=q(()=>{if(!n.value)return null;let{sourceLocale:e,locales:t}=n.value.config;return{source:e,targets:t.filter(t=>t!==e)}});return(e,n)=>(z(),V(N(XS),{"delay-duration":300},{default:P(()=>[H(`div`,YL,[U(BL),H(`div`,XL,[H(`header`,ZL,[H(`div`,QL,[r.value?(z(),B(`div`,$L,[U(N(eC),null,{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`span`,eR,j(r.value.project),1)]),_:1}),U(N(qC),{side:`bottom`,class:`font-mono`},{default:P(()=>[W(j(r.value.dir),1)]),_:1})]),_:1}),U(N(ku),{class:`size-3.5 shrink-0 text-muted-foreground`}),U(N(Qx),null,{default:P(()=>[U(N(iS),{"as-child":``},{default:P(()=>[U(N($),{variant:`outline`,size:`sm`,class:`max-w-[16rem] gap-1.5 font-mono`},{default:P(()=>[H(`span`,tR,j(r.value.name),1),U(N(Au),{class:`size-3.5 shrink-0 text-muted-foreground`})]),_:1})]),_:1}),U(N(RC),{align:`start`,class:`w-max`},{default:P(()=>[(z(!0),B(R,null,I(a.value,e=>(z(),V(N(zC),{key:e.path,class:`font-mono`,onSelect:t=>u(e.path)},{default:P(()=>[U(N(Du),{class:A([`size-4 shrink-0`,e.path===r.value.path?`opacity-100`:`opacity-0`])},null,8,[`class`]),H(`span`,null,[e.relDir?(z(),B(`span`,nR,j(e.relDir)+`/`,1)):G(``,!0),W(j(e.name),1)])]),_:2},1032,[`onSelect`]))),128))]),_:1})]),_:1})])):G(``,!0),H(`h1`,rR,j(d.value),1)]),f.value?(z(),V(N(eC),{key:0},{default:P(()=>[U(N(aC),{"as-child":``},{default:P(()=>[H(`button`,{type:`button`,class:`shrink-0 font-mono text-xs text-muted-foreground transition-colors hover:text-foreground`,onClick:n[0]||=e=>N(jp)(`settings`)},j(f.value.source)+` → `+j(f.value.targets.length)+` `+j(f.value.targets.length===1?`locale`:`locales`),1)]),_:1}),U(N(qC),{side:`bottom`,class:`max-w-[28rem] leading-relaxed`},{default:P(()=>[H(`div`,iR,j(f.value.targets.join(`, `)||`—`),1),n[4]||=H(`div`,{class:`mt-1 text-background/60`},`Click to manage in Settings`,-1)]),_:1})]),_:1})):G(``,!0)]),H(`main`,aR,[N(t)===`editor`?(z(),V(NO,{key:0})):N(t)===`analytics`?(z(),V(DA,{key:1})):N(t)===`glossary`?(z(),V($A,{key:2})):N(t)===`screenshots`?(z(),V(hF,{key:3})):N(t)===`settings`?(z(),V($P,{key:4})):N(t)===`activity`?(z(),V(ZF,{key:5})):N(t)===`docs`?(z(),V(pI,{key:6})):G(``,!0)])]),U(N(hh)),U(GL,{open:N(KL),"onUpdate:open":n[1]||=e=>ln(KL)?KL.value=e:null},null,8,[`open`]),o.value?(z(),V(gL,{key:0,ref_key:`wizardRef`,ref:s,onVnodeMounted:n[2]||=e=>s.value?.init(),onDismiss:n[3]||=e=>o.value=!1,onImported:l},null,512)):G(``,!0)])]),_:1}))}});EL(),iu(oR).mount(`#app`),OL(),sO(),TT();
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Glotfile</title>
7
- <script type="module" crossorigin src="/assets/index-CVA535xu.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-BgJsS1zp.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BaHu118N.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "glotfile",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "Local-first, git-native translation management.",
5
5
  "license": "MIT",
6
6
  "author": "James Dempster",
7
- "homepage": "https://github.com/jdempster/glotfile#readme",
7
+ "homepage": "https://glotfile.dev",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/jdempster/glotfile.git"