glotfile 0.8.6 → 0.8.7

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.
@@ -183,6 +183,7 @@ ng build --localize # 5. Angular builds one bundle per locale
183
183
  <h2>Format notes</h2>
184
184
  <ul>
185
185
  <li><strong>Placeholders</strong> — <code>{name}</code> ↔ <code>&lt;x id=&quot;INTERPOLATION&quot; equiv-text=&quot;{{name}}&quot;/&gt;</code>.</li>
186
+ <li><strong>Literals</strong> — mark literal text with ICU apostrophe quoting (<code>&#39;{site}&#39;</code>) and it renders as plain text instead of an <code>&lt;x/&gt;</code> placeholder. Apostrophe quoting is ICU&#39;s native escape, so it round-trips fully. See Placeholders and ICU.</li>
186
187
  <li><strong>Plurals</strong> — native ICU using Angular&#39;s <code>VAR_PLURAL</code> argument.</li>
187
188
  <li><strong>Locale codes</strong> — default to BCP-47 hyphen (<code>messages.pt-BR.xlf</code>), written into the <code>target-language</code> attribute. See Output Formats.</li>
188
189
  </ul>
@@ -190,7 +191,7 @@ ng build --localize # 5. Angular builds one bundle per locale
190
191
  <ul>
191
192
  <li>Output Formats · Placeholders and ICU · Plurals · Scan · Translation Workflow</li>
192
193
  </ul>
193
- `,text:`Angular Glotfile works with Angular's XLIFF files through the adapter (XLIFF 1.2). Angular owns the source — glotfile syncs from it This is the one framework where your code is the source of truth, not . You mark strings with in templates and Angular's generates . Glotfile syncs from that file rather than owning the source copy — so the day-to-day command is , not . The loop: merges the re-extracted source into your catalog preserving glossary, key context, and reviewed translations, and detects keys you removed from the source. Preview with ; add to drop keys that no longer appear in . Re-run (never ) every time you re-extract. Configure the output The untranslated stays owned by Angular; glotfile only writes the per-locale files. Format notes - Placeholders — ↔ . - Plurals — native ICU using Angular's argument. - Locale codes — default to BCP-47 hyphen ( ), written into the attribute. See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Scan · Translation Workflow`},{id:`frameworks/apple`,title:`Apple (iOS & macOS)`,section:`Frameworks`,html:`<h1>Apple (iOS &amp; macOS)</h1>
194
+ `,text:`Angular Glotfile works with Angular's XLIFF files through the adapter (XLIFF 1.2). Angular owns the source — glotfile syncs from it This is the one framework where your code is the source of truth, not . You mark strings with in templates and Angular's generates . Glotfile syncs from that file rather than owning the source copy — so the day-to-day command is , not . The loop: merges the re-extracted source into your catalog preserving glossary, key context, and reviewed translations, and detects keys you removed from the source. Preview with ; add to drop keys that no longer appear in . Re-run (never ) every time you re-extract. Configure the output The untranslated stays owned by Angular; glotfile only writes the per-locale files. Format notes - Placeholders — ↔ . - Literals — mark literal text with ICU apostrophe quoting ( ) and it renders as plain text instead of an placeholder. Apostrophe quoting is ICU's native escape, so it round-trips fully. See Placeholders and ICU. - Plurals — native ICU using Angular's argument. - Locale codes — default to BCP-47 hyphen ( ), written into the attribute. See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Scan · Translation Workflow`},{id:`frameworks/apple`,title:`Apple (iOS & macOS)`,section:`Frameworks`,html:`<h1>Apple (iOS &amp; macOS)</h1>
194
195
  <p>Apple localization splits across two files per locale, so glotfile uses two adapters that both write into <code>{locale}.lproj/</code>:</p>
195
196
  <ul>
196
197
  <li><strong><code>apple-strings</code></strong> → <code>Localizable.strings</code> — the scalar strings.</li>
@@ -211,6 +212,7 @@ ng build --localize # 5. Angular builds one bundle per locale
211
212
  <h2>Format notes</h2>
212
213
  <ul>
213
214
  <li><strong>Placeholders</strong> — printf style (<code>%@</code>, <code>%d</code>).</li>
215
+ <li><strong>Literals</strong> — mark literal text with ICU apostrophe quoting (<code>&#39;{site}&#39;</code>); it exports as plain <code>{site}</code>, and a literal <code>%</code> is escaped to <code>%%</code> so printf won&#39;t misread it. Fully escapable. See Placeholders and ICU.</li>
214
216
  <li><strong>Plurals</strong> — live only in <code>.stringsdict</code>, keyed by CLDR categories (<code>one</code>, <code>other</code>, …) with an <code>NSStringPluralRuleType</code> spec.</li>
215
217
  <li><strong>Locale codes</strong> — default to BCP-47 hyphen (<code>en.lproj</code>, <code>pt-BR.lproj</code>). See Output Formats.</li>
216
218
  </ul>
@@ -218,7 +220,7 @@ ng build --localize # 5. Angular builds one bundle per locale
218
220
  <ul>
219
221
  <li>Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow</li>
220
222
  </ul>
221
- `,text:`Apple (iOS & macOS) Apple localization splits across two files per locale, so glotfile uses two adapters that both write into : - → — the scalar strings. - → — the plurals. Configure the outputs Add both outputs in : The Settings → Output Formats dropdown lists ; add the entry by editing . skips plural keys (they would be lossy in a flat table) and emits them as plist entries — so without the second output your plurals never ship . Import existing strings Detection finds directories that contain a (that table only — and other tables are ignored). Wire Xcode to the exports Xcode picks up resources automatically once they're in the bundle. Use , , or SwiftUI . Format notes - Placeholders — printf style ( , ). - Plurals — live only in , keyed by CLDR categories ( , , …) with an spec. - Locale codes — default to BCP-47 hyphen ( , ). See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`frameworks/flutter`,title:`Flutter (ARB)`,section:`Frameworks`,html:`<h1>Flutter (ARB)</h1>
223
+ `,text:`Apple (iOS & macOS) Apple localization splits across two files per locale, so glotfile uses two adapters that both write into : - → — the scalar strings. - → — the plurals. Configure the outputs Add both outputs in : The Settings → Output Formats dropdown lists ; add the entry by editing . skips plural keys (they would be lossy in a flat table) and emits them as plist entries — so without the second output your plurals never ship . Import existing strings Detection finds directories that contain a (that table only — and other tables are ignored). Wire Xcode to the exports Xcode picks up resources automatically once they're in the bundle. Use , , or SwiftUI . Format notes - Placeholders — printf style ( , ). - Literals — mark literal text with ICU apostrophe quoting ( ); it exports as plain , and a literal is escaped to so printf won't misread it. Fully escapable. See Placeholders and ICU. - Plurals — live only in , keyed by CLDR categories ( , , …) with an spec. - Locale codes — default to BCP-47 hyphen ( , ). See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`frameworks/flutter`,title:`Flutter (ARB)`,section:`Frameworks`,html:`<h1>Flutter (ARB)</h1>
222
224
  <p>Glotfile exports your catalog to Flutter&#39;s <code>.arb</code> files with the <strong><code>flutter-arb</code></strong> adapter — one <code>app_{locale}.arb</code> per locale, ready for Flutter&#39;s <code>gen_l10n</code> code generator.</p>
223
225
  <h2>Configure the output</h2>
224
226
  <p>In Settings → Output Formats (or <code>glotfile.json</code>):</p>
@@ -240,6 +242,7 @@ output-localization-file: app_localizations.dart
240
242
  <h2>Format notes</h2>
241
243
  <ul>
242
244
  <li><strong>Placeholders</strong> — ICU <code>{name}</code> is preserved as-is. Glotfile writes the <code>@key</code> placeholder metadata that <code>gen_l10n</code> needs to compile typed arguments.</li>
245
+ <li><strong>Literals</strong> — mark literal text with ICU apostrophe quoting (<code>&#39;{site}&#39;</code>); it is preserved verbatim (apostrophe quoting is ICU&#39;s native escape) and excluded from the <code>@key</code> placeholder metadata. Fully escapable. See Placeholders and ICU.</li>
243
246
  <li><strong>Plurals</strong> — emitted as native inline ICU: <code>{count, plural, one {1 item} other {# items}}</code>.</li>
244
247
  <li><strong>Locale codes</strong> — default to BCP-47 underscore (<code>app_en.arb</code>, <code>app_zh_Hant.arb</code>), written into <code>@@locale</code>. Flutter rejects some script subtags; remap with <code>localeMap</code>, e.g. <code>&quot;localeMap&quot;: { &quot;zh-hant&quot;: &quot;zh_HK&quot; }</code>. See Output Formats.</li>
245
248
  </ul>
@@ -247,7 +250,7 @@ output-localization-file: app_localizations.dart
247
250
  <ul>
248
251
  <li>Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow</li>
249
252
  </ul>
250
- `,text:`Flutter (ARB) Glotfile exports your catalog to Flutter's files with the adapter — one per locale, ready for Flutter's code generator. Configure the output In Settings → Output Formats (or ): then writes , , … each with an header. Import existing strings If you already have files, pull them into the catalog once: Detection finds under , , or , strips the prefix, and records the so a later export reproduces your filenames byte-for-byte. Wire Flutter to the exports Flutter turns ARB into Dart at build time. Add an at the project root: Set under in , then (a normal regenerates too). Use it as . Format notes - Placeholders — ICU is preserved as-is. Glotfile writes the placeholder metadata that needs to compile typed arguments. - Plurals — emitted as native inline ICU: . - Locale codes — default to BCP-47 underscore ( , ), written into . Flutter rejects some script subtags; remap with , e.g. . See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`frameworks/laravel`,title:`Laravel`,section:`Frameworks`,html:`<h1>Laravel</h1>
253
+ `,text:`Flutter (ARB) Glotfile exports your catalog to Flutter's files with the adapter — one per locale, ready for Flutter's code generator. Configure the output In Settings → Output Formats (or ): then writes , , … each with an header. Import existing strings If you already have files, pull them into the catalog once: Detection finds under , , or , strips the prefix, and records the so a later export reproduces your filenames byte-for-byte. Wire Flutter to the exports Flutter turns ARB into Dart at build time. Add an at the project root: Set under in , then (a normal regenerates too). Use it as . Format notes - Placeholders — ICU is preserved as-is. Glotfile writes the placeholder metadata that needs to compile typed arguments. - Literals — mark literal text with ICU apostrophe quoting ( ); it is preserved verbatim (apostrophe quoting is ICU's native escape) and excluded from the placeholder metadata. Fully escapable. See Placeholders and ICU. - Plurals — emitted as native inline ICU: . - Locale codes — default to BCP-47 underscore ( , ), written into . Flutter rejects some script subtags; remap with , e.g. . See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`frameworks/laravel`,title:`Laravel`,section:`Frameworks`,html:`<h1>Laravel</h1>
251
254
  <p>Glotfile exports to Laravel&#39;s PHP array files with the <strong><code>laravel-php</code></strong> adapter — one file per locale <strong>per namespace</strong>, consumed directly by <code>__()</code> / <code>trans()</code>.</p>
252
255
  <h2>Configure the output</h2>
253
256
  <p>In Settings → Output Formats (or <code>glotfile.json</code>):</p>
@@ -265,6 +268,7 @@ output-localization-file: app_localizations.dart
265
268
  <h2>Format notes</h2>
266
269
  <ul>
267
270
  <li><strong>Placeholders</strong> — <code>{name}</code> is rendered as Laravel&#39;s <code>:name</code>.</li>
271
+ <li><strong>Literals</strong> — mark literal text with ICU apostrophe quoting (<code>&#39;{site}&#39;</code>); it exports as plain <code>{site}</code> (Laravel interpolates <code>:name</code>, so braces are literal) and round-trips. <strong>Partial:</strong> Laravel has no escape for <code>:name</code> itself, so a literal <code>:name</code> matching a real placeholder in the same string can&#39;t be protected — glotfile emits a <code>lossy-literal</code> warning. See Placeholders and ICU.</li>
268
272
  <li><strong>Plurals</strong> — pipe-separated forms (<code>one|other</code>) consumed by <code>trans_choice()</code>.</li>
269
273
  <li><strong>Locale codes</strong> — default to BCP-47 with underscores (<code>lang/pt_BR/…</code>, <code>lang/zh_HK/…</code>); bare codes like <code>fr</code> stay bare. This matches Laravel&#39;s loader, which keys off underscore + uppercase-region directory names. Override per output with <code>localeCase</code>. See Output Formats.</li>
270
274
  </ul>
@@ -272,7 +276,7 @@ output-localization-file: app_localizations.dart
272
276
  <ul>
273
277
  <li>Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow</li>
274
278
  </ul>
275
- `,text:`Laravel Glotfile exports to Laravel's PHP array files with the adapter — one file per locale per namespace , consumed directly by / . Configure the output In Settings → Output Formats (or ): Laravel 9 and earlier keep these under — use that path instead. How works The first dot-segment of a key becomes its file. is written as inside ; keys with no dot fall into . This mirrors how resolves. Import existing strings Detection finds or . PHP must be on your — the importer evaluates the array files to read them. Wire Laravel to the exports Nothing to generate: Laravel reads the PHP arrays directly. Use , , the Blade directive, or for plurals. Format notes - Placeholders — is rendered as Laravel's . - Plurals — pipe-separated forms ( ) consumed by . - Locale codes — default to BCP-47 with underscores ( , ); bare codes like stay bare. This matches Laravel's loader, which keys off underscore + uppercase-region directory names. Override per output with . See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`frameworks/rails`,title:`Rails`,section:`Frameworks`,html:`<h1>Rails</h1>
279
+ `,text:`Laravel Glotfile exports to Laravel's PHP array files with the adapter — one file per locale per namespace , consumed directly by / . Configure the output In Settings → Output Formats (or ): Laravel 9 and earlier keep these under — use that path instead. How works The first dot-segment of a key becomes its file. is written as inside ; keys with no dot fall into . This mirrors how resolves. Import existing strings Detection finds or . PHP must be on your — the importer evaluates the array files to read them. Wire Laravel to the exports Nothing to generate: Laravel reads the PHP arrays directly. Use , , the Blade directive, or for plurals. Format notes - Placeholders — is rendered as Laravel's . - Literals — mark literal text with ICU apostrophe quoting ( ); it exports as plain (Laravel interpolates , so braces are literal) and round-trips. Partial: Laravel has no escape for itself, so a literal matching a real placeholder in the same string can't be protected — glotfile emits a warning. See Placeholders and ICU. - Plurals — pipe-separated forms ( ) consumed by . - Locale codes — default to BCP-47 with underscores ( , ); bare codes like stay bare. This matches Laravel's loader, which keys off underscore + uppercase-region directory names. Override per output with . See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`frameworks/rails`,title:`Rails`,section:`Frameworks`,html:`<h1>Rails</h1>
276
280
  <p>Glotfile exports to Rails i18n YAML files with the <strong><code>rails-yaml</code></strong> adapter — one <code>config/locales/{locale}.yml</code> per locale.</p>
277
281
  <h2>Configure the output</h2>
278
282
  <p>In Settings → Output Formats (or <code>glotfile.json</code>):</p>
@@ -297,6 +301,7 @@ output-localization-file: app_localizations.dart
297
301
  <h2>Format notes</h2>
298
302
  <ul>
299
303
  <li><strong>Placeholders</strong> — <code>{name}</code> is rendered as Rails&#39; <code>%{name}</code>.</li>
304
+ <li><strong>Literals</strong> — mark literal text with ICU apostrophe quoting (<code>&#39;{site}&#39;</code>); it exports as plain <code>{site}</code> (Rails interpolates <code>%{name}</code>, so bare braces are literal) and round-trips. A literal <code>%</code> is best-effort. See Placeholders and ICU.</li>
300
305
  <li><strong>Plurals</strong> — a nested CLDR subtree (<code>one:</code> / <code>other:</code> / …) that Rails pluralises via <code>count:</code>.</li>
301
306
  <li><strong>Locale codes</strong> — default to BCP-47 hyphen (<code>pt-BR.yml</code>) and are written as the top-level key. See Output Formats.</li>
302
307
  </ul>
@@ -304,7 +309,7 @@ output-localization-file: app_localizations.dart
304
309
  <ul>
305
310
  <li>Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow</li>
306
311
  </ul>
307
- `,text:`Rails Glotfile exports to Rails i18n YAML files with the adapter — one per locale. Configure the output In Settings → Output Formats (or ): Each file is a nested map under a top-level locale key: The top-level key ( ) is authoritative for the locale — not the filename. Import existing strings Detection parses by their top-level locale keys. Glotfile reads a safe YAML subset (plain maps and scalar strings); anchors, flow collections, and sequences aren't supported. Wire Rails to the exports Rails auto-loads . Use , or the relative form in views; pass for plurals. Format notes - Placeholders — is rendered as Rails' . - Plurals — a nested CLDR subtree ( / / …) that Rails pluralises via . - Locale codes — default to BCP-47 hyphen ( ) and are written as the top-level key. See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`frameworks/vue-i18n`,title:`Vue I18n`,section:`Frameworks`,html:`<h1>Vue I18n</h1>
312
+ `,text:`Rails Glotfile exports to Rails i18n YAML files with the adapter — one per locale. Configure the output In Settings → Output Formats (or ): Each file is a nested map under a top-level locale key: The top-level key ( ) is authoritative for the locale — not the filename. Import existing strings Detection parses by their top-level locale keys. Glotfile reads a safe YAML subset (plain maps and scalar strings); anchors, flow collections, and sequences aren't supported. Wire Rails to the exports Rails auto-loads . Use , or the relative form in views; pass for plurals. Format notes - Placeholders — is rendered as Rails' . - Literals — mark literal text with ICU apostrophe quoting ( ); it exports as plain (Rails interpolates , so bare braces are literal) and round-trips. A literal is best-effort. See Placeholders and ICU. - Plurals — a nested CLDR subtree ( / / …) that Rails pluralises via . - Locale codes — default to BCP-47 hyphen ( ) and are written as the top-level key. See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`frameworks/vue-i18n`,title:`Vue I18n`,section:`Frameworks`,html:`<h1>Vue I18n</h1>
308
313
  <p>Glotfile exports to Vue I18n&#39;s JSON message files with the <strong><code>vue-i18n-json</code></strong> adapter — one JSON file per locale.</p>
309
314
  <h2>Configure the output</h2>
310
315
  <p>In Settings → Output Formats (or <code>glotfile.json</code>):</p>
@@ -327,6 +332,7 @@ const i18n = createI18n({ legacy: false, locale: &#39;en&#39;, messages: { en, f
327
332
  <h2>Format notes</h2>
328
333
  <ul>
329
334
  <li><strong>Placeholders</strong> — <code>{name}</code> is used verbatim (Vue I18n&#39;s named-interpolation syntax).</li>
335
+ <li><strong>Literals</strong> — mark literal text with ICU apostrophe quoting (<code>&#39;{site}&#39;</code>); it exports as Vue&#39;s literal interpolation <code>{&#39;{site}&#39;}</code> so Vue renders it verbatim instead of substituting. Fully escapable. See Placeholders and ICU.</li>
330
336
  <li><strong>Plurals</strong> — pipe-separated forms (<code>one | other</code>), Vue I18n&#39;s choice format.</li>
331
337
  <li><strong>Locale codes</strong> — default to lower-hyphen (<code>fr.json</code>, <code>pt-br.json</code>). See Output Formats.</li>
332
338
  </ul>
@@ -334,7 +340,7 @@ const i18n = createI18n({ legacy: false, locale: &#39;en&#39;, messages: { en, f
334
340
  <ul>
335
341
  <li>Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow</li>
336
342
  </ul>
337
- `,text:`Vue I18n Glotfile exports to Vue I18n's JSON message files with the adapter — one JSON file per locale. Configure the output In Settings → Output Formats (or ): The option controls layout: expands dot-segments into nested objects ( ); keeps the dotted keys as-is. New outputs default to nested. Import existing strings Detection looks in , , , and . It needs at least two locale files, or one named (or ), to recognise the layout with confidence. Wire Vue to the exports Load the JSON into : Use in templates; pass a count for plurals: . Format notes - Placeholders — is used verbatim (Vue I18n's named-interpolation syntax). - Plurals — pipe-separated forms ( ), Vue I18n's choice format. - Locale codes — default to lower-hyphen ( , ). See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`web-ui/ai-log`,title:`AI Log`,section:`Web UI`,html:`<h1>AI Log</h1>
343
+ `,text:`Vue I18n Glotfile exports to Vue I18n's JSON message files with the adapter — one JSON file per locale. Configure the output In Settings → Output Formats (or ): The option controls layout: expands dot-segments into nested objects ( ); keeps the dotted keys as-is. New outputs default to nested. Import existing strings Detection looks in , , , and . It needs at least two locale files, or one named (or ), to recognise the layout with confidence. Wire Vue to the exports Load the JSON into : Use in templates; pass a count for plurals: . Format notes - Placeholders — is used verbatim (Vue I18n's named-interpolation syntax). - Literals — mark literal text with ICU apostrophe quoting ( ); it exports as Vue's literal interpolation so Vue renders it verbatim instead of substituting. Fully escapable. See Placeholders and ICU. - Plurals — pipe-separated forms ( ), Vue I18n's choice format. - Locale codes — default to lower-hyphen ( , ). See Output Formats. Related - Output Formats · Placeholders and ICU · Plurals · Quick Start · Translation Workflow`},{id:`web-ui/ai-log`,title:`AI Log`,section:`Web UI`,html:`<h1>AI Log</h1>
338
344
  <p>The AI Log is a record of recent translation runs — what was sent to the provider and what came back. It&#39;s useful for understanding why a translation turned out the way it did, and for auditing what left your machine.</p>
339
345
  <h2>What each entry records</h2>
340
346
  <p>For every run:</p>
@@ -2071,14 +2077,14 @@ time like <code>(h:m)</code>, a ratio <code>a:b</code>) for a placeholder.</p>
2071
2077
  <strong>export-only and lossy</strong> — they sit outside the canonical guarantee. See
2072
2078
  <em>Lossy conversions</em> below.</p>
2073
2079
  </blockquote>
2074
- <h2>Escaping a literal <code>{name}</code></h2>
2075
- <p>To write text that <em>looks</em> like a placeholder but should be left alone, use
2076
- <strong>ICU apostrophe quoting</strong>:</p>
2080
+ <h2>Literals — text that looks like a placeholder</h2>
2081
+ <p>To write text that <em>looks</em> like a placeholder but should be left alone, mark it
2082
+ with <strong>ICU apostrophe quoting</strong>:</p>
2077
2083
  <table>
2078
2084
  <thead>
2079
2085
  <tr>
2080
2086
  <th>You write</th>
2081
- <th>You get</th>
2087
+ <th>Means</th>
2082
2088
  </tr>
2083
2089
  </thead>
2084
2090
  <tbody><tr>
@@ -2094,9 +2100,76 @@ time like <code>(h:m)</code>, a ratio <code>a:b</code>) for a placeholder.</p>
2094
2100
  <td>a literal apostrophe <code>&#39;</code></td>
2095
2101
  </tr>
2096
2102
  </tbody></table>
2097
- <p>Quoted spans are treated as plain text, so <code>&#39;{name}&#39;</code> is never flagged by the
2098
- checks or altered by AI translation. ARB output (ICU) preserves the quoting on
2099
- round-trip.</p>
2103
+ <p>Quoted spans are treated as plain text everywhere: the editor doesn&#39;t highlight
2104
+ them, the <code>placeholder-mismatch</code> check ignores them, and AI translation leaves
2105
+ them untouched.</p>
2106
+ <p>On <strong>export</strong>, each format renders a literal in its own native way so the
2107
+ runtime won&#39;t interpolate it:</p>
2108
+ <table>
2109
+ <thead>
2110
+ <tr>
2111
+ <th>Format</th>
2112
+ <th>A literal <code>&#39;{site}&#39;</code> exports as</th>
2113
+ <th>A literal <code>%</code></th>
2114
+ <th>Fully escapable?</th>
2115
+ </tr>
2116
+ </thead>
2117
+ <tbody><tr>
2118
+ <td>Flutter ARB / Angular</td>
2119
+ <td><code>&#39;{site}&#39;</code> (ICU apostrophe — native)</td>
2120
+ <td>—</td>
2121
+ <td>✅</td>
2122
+ </tr>
2123
+ <tr>
2124
+ <td>Vue I18n</td>
2125
+ <td><code>{&#39;{site}&#39;}</code> (vue literal interpolation)</td>
2126
+ <td>—</td>
2127
+ <td>✅</td>
2128
+ </tr>
2129
+ <tr>
2130
+ <td>gettext / Apple</td>
2131
+ <td><code>{site}</code> (braces are plain text)</td>
2132
+ <td><code>%%</code></td>
2133
+ <td>✅</td>
2134
+ </tr>
2135
+ <tr>
2136
+ <td>Rails YAML</td>
2137
+ <td><code>{site}</code> (braces are plain text)</td>
2138
+ <td>best-effort</td>
2139
+ <td>✅ (braces)</td>
2140
+ </tr>
2141
+ <tr>
2142
+ <td><strong>i18next</strong></td>
2143
+ <td><code>{site}</code> — but a literal <code>{{site}}</code> is emitted as-is</td>
2144
+ <td>—</td>
2145
+ <td>⚠️ partial</td>
2146
+ </tr>
2147
+ <tr>
2148
+ <td><strong>Laravel</strong></td>
2149
+ <td><code>{site}</code> — but a literal <code>:name</code> can&#39;t be protected</td>
2150
+ <td>—</td>
2151
+ <td>⚠️ partial</td>
2152
+ </tr>
2153
+ </tbody></table>
2154
+ <h3>Two formats can&#39;t fully escape</h3>
2155
+ <p>Neither <strong>i18next</strong> (<code>{{ }}</code>) nor <strong>Laravel</strong> (<code>:name</code>) has a way to mark its
2156
+ own interpolation syntax as literal, so glotfile emits the best it can and
2157
+ <strong>warns</strong> with <code>lossy-literal</code>:</p>
2158
+ <ul>
2159
+ <li><strong>i18next</strong> — a literal whose content is a <code>{{name}}</code> token (you wrote
2160
+ <code>&#39;{{site}}&#39;</code>) is written verbatim and i18next <em>will</em> substitute it at runtime.
2161
+ A single-brace literal <code>&#39;{site}&#39;</code> → <code>{site}</code> is safe, since i18next only
2162
+ interpolates <code>{{ }}</code>.</li>
2163
+ <li><strong>Laravel</strong> — a literal <code>:name</code> that matches a real placeholder in the <em>same</em>
2164
+ string collapses to the same <code>:name</code>, and Laravel interpolates both.</li>
2165
+ </ul>
2166
+ <pre><code>warning [lossy-literal] tpl @ en: i18next will interpolate a literal containing {{…}}; i18next has no escape for it
2167
+ </code></pre>
2168
+ <p>On <strong>import</strong>, the inverse runs wherever it&#39;s unambiguous: vue <code>{&#39;…&#39;}</code>, ICU
2169
+ apostrophes, and a bare <code>{name}</code> in a Laravel/Rails file (whose interpolation is
2170
+ <code>:name</code>/<code>%{name}</code>, so braces are literal) all become canonical literals.
2171
+ i18next import stays <strong>lenient</strong> — a single-brace <code>{name}</code> is kept as a
2172
+ placeholder — so an i18next literal round-trip is best-effort.</p>
2100
2173
  <h2>ICU plural and select</h2>
2101
2174
  <p>ICU constructs like:</p>
2102
2175
  <pre><code>{count, plural, one {# item} other {# items}}
@@ -2112,7 +2185,7 @@ round-trip.</p>
2112
2185
  <ul>
2113
2186
  <li>Plurals · Output Formats · Checks and Validation · How Translation Works</li>
2114
2187
  </ul>
2115
- `,text:`Placeholders and ICU Translations must keep the machinery the source string carries: interpolation placeholders and ICU plural/select structure. Glotfile protects both during translation, validation, and export. One canonical form, converted on export Glotfile stores every placeholder in a single canonical form — — no matter what style the source file used. Import normalises to it; export converts it to each adapter's native style. So you edit and validate against one syntax, and each output file still gets the flavour its framework expects. Format Native style Example On import → stored On export ← stored --- --- --- --- --- Flutter ARB ICU named (already canonical) Vue I18n single braces (already canonical) Laravel colon Rails YAML percent-brace i18next double braces Angular XLIFF placeholder element A translation that drops, renames, or adds a placeholder relative to the source is: - rejected by AI translation (not written), and - flagged by the rule. Because everything is canonical internally, the editor and the checks only ever look for — they never mistake an unrelated colon or percent sign (a time like , a ratio ) for a placeholder. printf formats ( , in gettext and Apple ) are export-only and lossy — they sit outside the canonical guarantee. See Lossy conversions below. Escaping a literal To write text that looks like a placeholder but should be left alone, use ICU apostrophe quoting : You write You get --- --- a literal the literal text (not a token) a literal apostrophe Quoted spans are treated as plain text, so is never flagged by the checks or altered by AI translation. ARB output (ICU) preserves the quoting on round-trip. ICU plural and select ICU constructs like: are preserved structurally. The rule flags a string where one side (source or translation) is an ICU plural/select and the other isn't. See also Plurals for the dedicated plural-key model. Lossy conversions Not every format can represent every construct. When export hits one it can't faithfully convert, it warns and writes safe output rather than emitting something broken: Treat these warnings as a prompt to simplify the string or pick a different adapter for that output. Related - Plurals · Output Formats · Checks and Validation · How Translation Works`},{id:`guides/continuous-integration`,title:`Continuous Integration`,section:`Guides`,html:`<h1>Continuous Integration</h1>
2188
+ `,text:`Placeholders and ICU Translations must keep the machinery the source string carries: interpolation placeholders and ICU plural/select structure. Glotfile protects both during translation, validation, and export. One canonical form, converted on export Glotfile stores every placeholder in a single canonical form — — no matter what style the source file used. Import normalises to it; export converts it to each adapter's native style. So you edit and validate against one syntax, and each output file still gets the flavour its framework expects. Format Native style Example On import → stored On export ← stored --- --- --- --- --- Flutter ARB ICU named (already canonical) Vue I18n single braces (already canonical) Laravel colon Rails YAML percent-brace i18next double braces Angular XLIFF placeholder element A translation that drops, renames, or adds a placeholder relative to the source is: - rejected by AI translation (not written), and - flagged by the rule. Because everything is canonical internally, the editor and the checks only ever look for — they never mistake an unrelated colon or percent sign (a time like , a ratio ) for a placeholder. printf formats ( , in gettext and Apple ) are export-only and lossy — they sit outside the canonical guarantee. See Lossy conversions below. Literals — text that looks like a placeholder To write text that looks like a placeholder but should be left alone, mark it with ICU apostrophe quoting : You write Means --- --- a literal the literal text (not a token) a literal apostrophe Quoted spans are treated as plain text everywhere: the editor doesn't highlight them, the check ignores them, and AI translation leaves them untouched. On export , each format renders a literal in its own native way so the runtime won't interpolate it: Format A literal exports as A literal Fully escapable? --- --- --- --- Flutter ARB / Angular (ICU apostrophe — native) ✅ Vue I18n (vue literal interpolation) — ✅ gettext / Apple (braces are plain text) ✅ Rails YAML (braces are plain text) best-effort ✅ (braces) i18next — but a literal is emitted as-is — ⚠️ partial Laravel — but a literal can't be protected — ⚠️ partial Two formats can't fully escape Neither i18next ( ) nor Laravel ( ) has a way to mark its own interpolation syntax as literal, so glotfile emits the best it can and warns with : - i18next — a literal whose content is a token (you wrote ) is written verbatim and i18next will substitute it at runtime. A single-brace literal → is safe, since i18next only interpolates . - Laravel — a literal that matches a real placeholder in the same string collapses to the same , and Laravel interpolates both. On import , the inverse runs wherever it's unambiguous: vue , ICU apostrophes, and a bare in a Laravel/Rails file (whose interpolation is / , so braces are literal) all become canonical literals. i18next import stays lenient — a single-brace is kept as a placeholder — so an i18next literal round-trip is best-effort. ICU plural and select ICU constructs like: are preserved structurally. The rule flags a string where one side (source or translation) is an ICU plural/select and the other isn't. See also Plurals for the dedicated plural-key model. Lossy conversions Not every format can represent every construct. When export hits one it can't faithfully convert, it warns and writes safe output rather than emitting something broken: Treat these warnings as a prompt to simplify the string or pick a different adapter for that output. Related - Plurals · Output Formats · Checks and Validation · How Translation Works`},{id:`guides/continuous-integration`,title:`Continuous Integration`,section:`Guides`,html:`<h1>Continuous Integration</h1>
2116
2189
  <p>Glotfile is built to run in CI so a broken or stale translation state can&#39;t reach <code>main</code>. The key command is <code>glotfile check</code>; <code>glotfile lint</code> gives you finer control.</p>
2117
2190
  <h2>The one-liner</h2>
2118
2191
  <p>Add a single step that validates the catalog <strong>and</strong> confirms exports are up to date:</p>
@@ -2296,4 +2369,4 @@ Only the AI calls you trigger. The UI is local-only; no credentials or screensho
2296
2369
  <ul>
2297
2370
  <li>Home · Installation · AI Providers · Continuous Integration</li>
2298
2371
  </ul>
2299
- `,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.6`)),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();
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();
@@ -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-amdKG3Do.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-CVA535xu.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BaHu118N.css">
9
9
  </head>
10
10
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glotfile",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Local-first, git-native translation management.",
5
5
  "license": "MIT",
6
6
  "author": "James Dempster",
package/skill/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: glotfile
3
- description: Manage translations in a repo that uses glotfile (a local-first, git-native translation catalog). Use when working in a project containing a glotfile.json or glotfile/ directory, or when the user asks to add/edit/translate/export strings, work with locales/i18n, or run any `glotfile` command. Covers the CLI workflows, the glotfile.json schema, adding and editing strings, glossary, and project conventions.
3
+ description: Use whenever working in a repo that contains a glotfile.json file or a glotfile/ directory, or when the user asks to add, edit, translate, export, import, lint, or prune localized strings, manage locales/i18n/translations, onboard or set up a translation catalog, work with a translation glossary, or run any `glotfile` command even if they don't say "glotfile" by name. glotfile is a local-first, git-native translation manager whose committed state file is the single source of truth for every locale.
4
4
  ---
5
5
 
6
6
  # Managing glotfile
@@ -10,10 +10,12 @@ The state file is auto-detected: a `glotfile/` directory (split layout) wins ove
10
10
  single `glotfile.json`.
11
11
 
12
12
  ## serve
13
- `glotfile serve [--dev]` — start the local web UI (the default command when none is
14
- given). Opens a browser at a local URL. `--dev` runs the UI from source with hot reload
15
- (the API runs on a separate port; open the Vite URL, not the API port). With
16
- `config.autoExport` on (the default), serving re-exports to disk on every change.
13
+ `glotfile serve [--no-open]` — start the local web UI (the default command when none is
14
+ given). Opens a browser at a local URL; pass `--no-open` to skip launching the browser
15
+ (useful when driving glotfile headlessly). With `config.autoExport` on (the default),
16
+ serving re-exports to disk on every change. (Ignore the `--dev` flag shown in `--help`:
17
+ it's for developing glotfile itself — in that mode this process serves the API only and
18
+ the UI comes from a separate Vite server, so plain `serve` is what you want.)
17
19
 
18
20
  ## export
19
21
  `glotfile export [--adapter <name>] [--watch]` — write the locale files for every
@@ -25,13 +27,17 @@ Adapter names: `flutter-arb`, `laravel-php`, `i18next-json`, `vue-i18n-json`,
25
27
  `gettext-po`, `apple-strings`, `apple-stringsdict`, `angular-xliff`, `rails-yaml`.
26
28
 
27
29
  ## translate
28
- `glotfile translate [--all] [--estimate] [--locale <list>] [--key <glob>]` AI-translate
29
- strings into the target locales and write the results back into the state file.
30
+ `glotfile translate [--all] [--estimate] [--locale <list>] [--key <glob>] [--batch [--wait]]`
31
+ — AI-translate strings into the target locales and write the results back into the state file.
30
32
  - By default only **empty** values are translated (existing translations are left alone).
31
33
  - `--all` — re-translate every string, overwriting existing translations.
32
34
  - `--estimate` — print batch/token/cost estimates and translate nothing.
33
35
  - `--locale fr,de` — restrict to these target locales (alias: `--locales`).
34
36
  - `--key <glob>` — only keys matching the glob (e.g. `auth.*`).
37
+ - `--batch` — submit through the provider's batch API (~50% cheaper, runs asynchronously;
38
+ **anthropic only**). Returns immediately with a pending batch; track and apply it with
39
+ `glotfile batch` (see below). Reach for this when the user wants a large/cheap translate.
40
+ - `--wait` — with `--batch`, stay attached and poll until the batch finishes, then apply.
35
41
 
36
42
  Requires a configured AI provider + API key in per-machine local settings.
37
43
 
@@ -61,6 +67,7 @@ flat i18next `<lng>.json` files look like vue-i18n, so they need an explicit
61
67
  `--format i18next-json` (the `public/locales/<lng>/<ns>.json` layout auto-detects).
62
68
  - `--source <dir>` — directory to import from (default: the state file's directory).
63
69
  - `--source-locale <code>` — which locale is the source of truth.
70
+ - `--locales <list>` — comma-separated locales to import (default: every locale found).
64
71
  - `--cldr` — expand CLDR plural forms.
65
72
  - `--force` — overwrite an existing `glotfile.json`.
66
73
 
@@ -81,9 +88,10 @@ one-time `import`). Format is auto-detected if omitted.
81
88
  `messages.xlf`.
82
89
 
83
90
  ## build-context
84
- `glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>]` —
91
+ `glotfile build-context [--all] [--key <glob>] [--limit <n>] [--since <date>] [--batch]` —
85
92
  AI-generate per-key context (where/how a string is used) to improve translation quality.
86
- **Requires a prior `glotfile scan`** to index code references.
93
+ **Requires a prior `glotfile scan`** to index code references. `--batch` submits through the
94
+ batch API (anthropic only), like `translate --batch`.
87
95
 
88
96
  ## scan
89
97
  `glotfile scan` — index the codebase's references to translation keys, writing
@@ -108,3 +116,12 @@ git diffs on large catalogs. All commands work with either layout.
108
116
  `glotfile skill [--print] [--force]` — install this Claude Code skill into the current
109
117
  repo's `.claude/skills/glotfile/`. `--print` writes `SKILL.md` to stdout instead;
110
118
  `--force` overwrites an existing install.
119
+
120
+ ## batch
121
+ `glotfile batch [status|apply|cancel]` — manage a pending batch translation that was
122
+ submitted with `glotfile translate --batch`. The batch runs server-side; this command
123
+ checks on it from a later session (the handle is stored locally).
124
+ - `status` (default) — show the pending batch's progress.
125
+ - `apply` — fetch the finished results and write the translations into the state file
126
+ (this happens automatically once the batch has finished).
127
+ - `cancel` — cancel the pending batch and discard its handle.