kasy-cli 1.29.0 → 1.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,7 +42,7 @@ const { generateFirebaseProject } = require('../scaffold/backends/firebase/gener
42
42
  const { generateSupabaseProject } = require('../scaffold/backends/supabase/generator');
43
43
  const { generateApiProject } = require('../scaffold/backends/api/generator');
44
44
  const { createProjectAndGetKeys, setupLinkedProject, checkLoggedIn, getOrgsList, getProjectsByOrg, getProjectKeys, classifyCreateError } = require('../scaffold/backends/supabase/deploy');
45
- const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, writeGoogleIosUrlScheme, writeGoogleIosUrlSchemeFromClientId } = require('../scaffold/shared/post-build');
45
+ const { writeSupabaseGoogleAuthOptions, readSupabaseGoogleCredentials, getGoogleClientSecretViaGcloud, flutterfireConfigure, writeGoogleAuthOptions, ensureGoogleServiceInfoPlist, writeGoogleIosUrlScheme, writeGoogleIosUrlSchemeFromClientId } = require('../scaffold/shared/post-build');
46
46
  const { toPackageName } = require('../scaffold/backends/firebase/tokens');
47
47
  const { setupFromScratch, setupExistingProject, listBillingAccounts, listGcpOrganizations, checkGcloudAuth, getFirebaseAccount, getGcloudInstallInstructions, enableAuthProviders, ensureFirebaseAuthInitialized, registerDebugSha1 } = require('../scaffold/backends/firebase/setup-from-scratch');
48
48
  const { enableAuthViaFirebaseCli } = require('../scaffold/backends/firebase/enable-auth-via-cli');
@@ -1857,6 +1857,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
1857
1857
  includeWeb: answers.includeWeb !== false,
1858
1858
  });
1859
1859
  rerunSpinner.stop(tr('new.google.refreshConfigs'));
1860
+ // flutterfire may not drop the iOS plist even with the iOS app
1861
+ // registered; the iOS Client ID below is read from it, so fetch it from
1862
+ // the Firebase API when missing.
1863
+ await ensureGoogleServiceInfoPlist(targetDir, answers.firebaseProjectId);
1860
1864
  } else if (cliResult.error === 'support_email_required') {
1861
1865
  ui.log.warn(tr('new.google.manualHint.noEmail'));
1862
1866
  }
@@ -2074,6 +2078,10 @@ async function runNew(directory, { language: langHint = null, backend: backendHi
2074
2078
  if (ffRerun.ok) {
2075
2079
  const gaResult = await writeGoogleAuthOptions(targetDir);
2076
2080
  printStepResult({ name: 'google-auth-options', ok: gaResult.ok, detail: gaResult.error }, language);
2081
+ // flutterfire sometimes doesn't drop the iOS GoogleService-Info.plist
2082
+ // even with the iOS app registered. Fetch it from the Firebase API so the
2083
+ // URL-scheme step (and native iOS Firebase/FCM) always has its config.
2084
+ await ensureGoogleServiceInfoPlist(targetDir, answers.firebaseProjectId);
2077
2085
  const iosResult = await writeGoogleIosUrlScheme(targetDir);
2078
2086
  printStepResult({ name: 'google-ios-url-scheme', ok: iosResult.ok, detail: iosResult.error }, language);
2079
2087
  }
@@ -9,14 +9,14 @@
9
9
  * Requires: supabase CLI installed, user logged in (supabase login).
10
10
  */
11
11
 
12
- const { exec, execFile } = require('node:child_process');
12
+ const { exec } = require('node:child_process');
13
13
  const { promisify } = require('node:util');
14
14
  const path = require('node:path');
15
+ const os = require('node:os');
15
16
  const fs = require('fs-extra');
16
17
  const { augmentedEnv } = require('../../../utils/env-tools');
17
18
 
18
19
  const execAsync = promisify(exec);
19
- const execFileAsync = promisify(execFile);
20
20
 
21
21
  /**
22
22
  * Parse JSON from a CLI's stdout tolerantly. The Supabase CLI sometimes prints
@@ -187,20 +187,102 @@ async function getProjectKeys(projectRef) {
187
187
  }
188
188
  }
189
189
 
190
+ // The Supabase CLI stores its access token via go-keyring under the service
191
+ // "Supabase CLI" with the key/user "access-token". Each OS keeps that in a
192
+ // different vault, so reading it back is per-OS. Used by the Management API
193
+ // calls below (auth providers), which the CLI itself has no command for.
194
+ const SUPABASE_KEYRING_SERVICE = 'Supabase CLI';
195
+ const SUPABASE_KEYRING_USER = 'access-token';
196
+
197
+ // Read a generic credential out of the Windows Credential Manager. go-keyring's
198
+ // wincred backend names the target "<service>:<user>" and stores the secret as a
199
+ // raw blob (UTF-8 or UTF-16LE depending on version), so we return the base64 of
200
+ // the blob and let the caller pick the right decoding.
201
+ async function readWindowsCredentialBase64(target) {
202
+ const ps = `
203
+ $ErrorActionPreference = 'Stop'
204
+ $sig = @"
205
+ using System;
206
+ using System.Runtime.InteropServices;
207
+ public class KasyCred {
208
+ [DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
209
+ public static extern bool CredRead(string target, int type, int flags, out IntPtr cred);
210
+ [DllImport("advapi32.dll")] public static extern void CredFree(IntPtr cred);
211
+ [StructLayout(LayoutKind.Sequential)] public struct CREDENTIAL {
212
+ public int Flags; public int Type; public IntPtr TargetName; public IntPtr Comment;
213
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
214
+ public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist;
215
+ public int AttributeCount; public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName;
216
+ }
217
+ }
218
+ "@
219
+ Add-Type $sig | Out-Null
220
+ $ptr = [IntPtr]::Zero
221
+ if ([KasyCred]::CredRead('${target}', 1, 0, [ref]$ptr)) {
222
+ $c = [System.Runtime.InteropServices.Marshal]::PtrToStructure($ptr, [type]([KasyCred+CREDENTIAL]))
223
+ $bytes = New-Object byte[] $c.CredentialBlobSize
224
+ [System.Runtime.InteropServices.Marshal]::Copy($c.CredentialBlob, $bytes, 0, $c.CredentialBlobSize)
225
+ [KasyCred]::CredFree($ptr)
226
+ [Convert]::ToBase64String($bytes)
227
+ }
228
+ `;
229
+ const encoded = Buffer.from(ps, 'utf16le').toString('base64');
230
+ const { stdout } = await execAsync(
231
+ `powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encoded}`,
232
+ { windowsHide: true, maxBuffer: 1024 * 1024 },
233
+ );
234
+ return (stdout || '').trim();
235
+ }
236
+
190
237
  /**
191
- * Get the Supabase access token stored by `supabase login`.
192
- * macOS: stored in Keychain under "Supabase CLI" (go-keyring-base64 encoded).
193
- * Fallback: SUPABASE_ACCESS_TOKEN environment variable.
238
+ * Get the Supabase access token stored by `supabase login`, cross-platform:
239
+ * - SUPABASE_ACCESS_TOKEN env var (any OS, also the CI path)
240
+ * - macOS: Keychain (`security`)
241
+ * - Windows: Credential Manager (CredRead via PowerShell)
242
+ * - Linux: libsecret (`secret-tool`)
243
+ * Returns null if none works (caller degrades to a manual hint).
194
244
  */
195
245
  async function getSupabaseAccessToken() {
196
246
  if (process.env.SUPABASE_ACCESS_TOKEN) return process.env.SUPABASE_ACCESS_TOKEN;
197
- try {
198
- const { stdout } = await execAsync('security find-generic-password -s "Supabase CLI" -w');
199
- const raw = stdout.trim();
200
- if (raw.startsWith('go-keyring-base64:')) {
201
- return Buffer.from(raw.replace('go-keyring-base64:', ''), 'base64').toString('utf8');
247
+
248
+ const decodeKeyring = (raw) => {
249
+ const s = (raw || '').trim();
250
+ if (!s) return null;
251
+ if (s.startsWith('go-keyring-base64:')) {
252
+ return Buffer.from(s.replace('go-keyring-base64:', ''), 'base64').toString('utf8').trim() || null;
253
+ }
254
+ return s;
255
+ };
256
+
257
+ if (process.platform === 'darwin') {
258
+ try {
259
+ const { stdout } = await execAsync(`security find-generic-password -s "${SUPABASE_KEYRING_SERVICE}" -w`);
260
+ return decodeKeyring(stdout);
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ if (process.platform === 'win32') {
267
+ try {
268
+ const b64 = await readWindowsCredentialBase64(`${SUPABASE_KEYRING_SERVICE}:${SUPABASE_KEYRING_USER}`);
269
+ if (!b64) return null;
270
+ const buf = Buffer.from(b64, 'base64');
271
+ // UTF-16LE blobs have a NUL after most bytes; UTF-8 blobs don't.
272
+ const nulCount = buf.reduce((n, b) => n + (b === 0 ? 1 : 0), 0);
273
+ const token = (nulCount > buf.length / 4 ? buf.toString('utf16le') : buf.toString('utf8')).trim();
274
+ return decodeKeyring(token);
275
+ } catch {
276
+ return null;
202
277
  }
203
- return raw || null;
278
+ }
279
+
280
+ // Linux (libsecret).
281
+ try {
282
+ const { stdout } = await execAsync(
283
+ `secret-tool lookup service "${SUPABASE_KEYRING_SERVICE}" username "${SUPABASE_KEYRING_USER}"`,
284
+ );
285
+ return decodeKeyring(stdout);
204
286
  } catch {
205
287
  return null;
206
288
  }
@@ -352,19 +434,30 @@ async function dbPush(projectDir) {
352
434
  }
353
435
 
354
436
  /**
355
- * Set a single Supabase secret safely using execFile (no shell expansion).
356
- * supabase secrets set accepts KEY=VALUE as a positional arg.
437
+ * Set a single Supabase secret.
438
+ *
439
+ * On Windows the supabase binary is `supabase.cmd`, which execFile (no shell)
440
+ * can't launch — it failed with "spawn supabase ENOENT". We go through the
441
+ * shell-based run() instead, which resolves the .cmd and uses the augmented
442
+ * PATH. To keep the secret value off the command line (no shell escaping /
443
+ * injection), we hand it to the CLI via a temporary --env-file rather than as a
444
+ * positional KEY=VALUE arg.
357
445
  */
358
446
  async function setSecret(projectDir, key, value) {
447
+ let tmpDir;
359
448
  try {
360
- await execFileAsync('supabase', ['secrets', 'set', `${key}=${value}`], {
361
- cwd: projectDir,
362
- maxBuffer: 10 * 1024 * 1024,
363
- env: process.env,
364
- });
449
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'kasy-secret-'));
450
+ const envFile = path.join(tmpDir, 'secret.env');
451
+ // The value is single-line (JSON is compacted upstream). dotenv splits on the
452
+ // first '=' only, so any '=' inside the value is preserved.
453
+ await fs.writeFile(envFile, `${key}=${value}\n`, 'utf8');
454
+ const r = await run(`supabase secrets set --env-file "${envFile}"`, projectDir);
455
+ if (!r.ok) return { ok: false, error: (r.stderr || r.error || '').slice(0, 300) };
365
456
  return { ok: true };
366
457
  } catch (err) {
367
458
  return { ok: false, error: err.message };
459
+ } finally {
460
+ if (tmpDir) await fs.remove(tmpDir).catch(() => {});
368
461
  }
369
462
  }
370
463
 
@@ -14,12 +14,14 @@ const { promisify } = require('node:util');
14
14
  const fs = require('fs-extra');
15
15
  const path = require('node:path');
16
16
  const os = require('node:os');
17
+ const { augmentedEnv } = require('../../utils/env-tools');
17
18
 
18
19
  const execAsync = promisify(exec);
19
20
 
20
21
  async function run(cmd) {
21
22
  try {
22
- const { stdout, stderr } = await execAsync(cmd, { maxBuffer: 5 * 1024 * 1024 });
23
+ // augmentedEnv so a gcloud installed earlier this run is found on Windows.
24
+ const { stdout, stderr } = await execAsync(cmd, { maxBuffer: 5 * 1024 * 1024, env: augmentedEnv() });
23
25
  return { ok: true, stdout: stdout.trim(), stderr: stderr.trim() };
24
26
  } catch (err) {
25
27
  return {
@@ -86,17 +88,31 @@ async function createFcmServiceAccountKey(projectId) {
86
88
  return { ok: false, error: 'projectId is required' };
87
89
  }
88
90
 
89
- const saResult = await findFirebaseAdminSdkSA(projectId.trim());
90
- if (!saResult.ok) {
91
- return { ok: false, error: saResult.error };
92
- }
93
-
94
- const saEmail = saResult.email;
91
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
95
92
 
96
- // Ensure the service account can send FCM messages (idempotent)
97
- const roleResult = await grantFcmAdminRole(projectId.trim(), saEmail);
98
- if (!roleResult.ok) {
99
- return { ok: false, error: roleResult.error };
93
+ // On a brand-new project the Firebase Admin SDK service account is provisioned
94
+ // asynchronously after Firebase is added, and the IAM role grant also needs a
95
+ // moment to take. Both can fail immediately on the first try, which is why
96
+ // push used to come out half-configured on fresh Supabase/API projects. Retry
97
+ // the discover+grant with backoff before giving up.
98
+ let saEmail = '';
99
+ let lastErr = 'Firebase Admin SDK service account not found';
100
+ for (let attempt = 1; attempt <= 5; attempt++) {
101
+ const saResult = await findFirebaseAdminSdkSA(projectId.trim());
102
+ if (saResult.ok) {
103
+ const roleResult = await grantFcmAdminRole(projectId.trim(), saResult.email);
104
+ if (roleResult.ok) {
105
+ saEmail = saResult.email;
106
+ break;
107
+ }
108
+ lastErr = roleResult.error;
109
+ } else {
110
+ lastErr = saResult.error;
111
+ }
112
+ if (attempt < 5) await sleep(8000);
113
+ }
114
+ if (!saEmail) {
115
+ return { ok: false, error: lastErr };
100
116
  }
101
117
 
102
118
  const tmpPath = path.join(os.tmpdir(), `kasy-fcm-${Date.now()}.json`);
@@ -105,7 +121,6 @@ async function createFcmServiceAccountKey(projectId) {
105
121
  // IAM permission takes a moment to propagate — so the first key-create often
106
122
  // fails with "permission still propagating". Back off and retry a few times
107
123
  // before giving up, instead of leaving push half-configured.
108
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
109
124
  let keyResult;
110
125
  for (let attempt = 1; attempt <= 4; attempt++) {
111
126
  keyResult = await run(
@@ -287,6 +287,50 @@ function syncGoogleIosUrlSchemeContent(infoPlistContent, reversedClientId) {
287
287
  };
288
288
  }
289
289
 
290
+ /**
291
+ * Make sure ios/Runner/GoogleService-Info.plist exists. `flutterfire configure`
292
+ * is supposed to download it, but in practice it sometimes doesn't drop the iOS
293
+ * file even though the iOS app is registered in Firebase (the Android
294
+ * google-services.json still lands fine). When that happens the native iOS
295
+ * Firebase config is missing AND Google Sign-In's REVERSED_CLIENT_ID can't be
296
+ * read, so the URL-scheme step fails. We fetch the plist straight from the
297
+ * Firebase API (`firebase apps:sdkconfig IOS`) — the authoritative source — and
298
+ * write it ourselves. No-op when the file is already there. Best-effort.
299
+ *
300
+ * @returns {Promise<{ ok: boolean, existed?: boolean, error?: string }>}
301
+ */
302
+ async function ensureGoogleServiceInfoPlist(projectDir, projectId) {
303
+ const plistPath = path.join(projectDir, 'ios', 'Runner', 'GoogleService-Info.plist');
304
+ if (await fs.pathExists(plistPath)) {
305
+ return { ok: true, existed: true };
306
+ }
307
+ if (!projectId) {
308
+ return { ok: false, error: 'GoogleService-Info.plist missing and no projectId to fetch it' };
309
+ }
310
+ // No app id: the CLI auto-selects the single iOS app in the project, which is
311
+ // exactly our case (kasy creates one iOS app per project).
312
+ const result = await run(
313
+ `firebase apps:sdkconfig IOS --project ${projectId}`,
314
+ projectDir,
315
+ 120_000,
316
+ );
317
+ if (!result.ok) {
318
+ return { ok: false, error: (result.stderr || result.error || '').slice(0, 300) };
319
+ }
320
+ // The command prints a spinner line before the XML; keep only the plist body.
321
+ const xmlStart = result.stdout.indexOf('<?xml');
322
+ if (xmlStart === -1 || !result.stdout.includes('REVERSED_CLIENT_ID')) {
323
+ return { ok: false, error: 'Unexpected apps:sdkconfig output (no plist body)' };
324
+ }
325
+ try {
326
+ await fs.ensureDir(path.dirname(plistPath));
327
+ await fs.writeFile(plistPath, result.stdout.slice(xmlStart), 'utf8');
328
+ return { ok: true, existed: false };
329
+ } catch (err) {
330
+ return { ok: false, error: `Failed to write GoogleService-Info.plist: ${err.message}` };
331
+ }
332
+ }
333
+
290
334
  /**
291
335
  * Register the REVERSED_CLIENT_ID from GoogleService-Info.plist as a URL scheme
292
336
  * in ios/Runner/Info.plist so that Google Sign-In can redirect back to the app
@@ -886,6 +930,7 @@ module.exports = {
886
930
  dartFix,
887
931
  flutterfireConfigure,
888
932
  writeGoogleAuthOptions,
933
+ ensureGoogleServiceInfoPlist,
889
934
  writeGoogleIosUrlScheme,
890
935
  writeSupabaseGoogleAuthOptions,
891
936
  writeGoogleIosUrlSchemeFromClientId,
@@ -550,7 +550,7 @@ module.exports = {
550
550
  'new.firebase.q.appName.hint': 'e.g.: My Amazing App',
551
551
  'new.firebase.q.appName.required': 'App name is required.',
552
552
 
553
- 'new.firebase.q.bundleId': 'What is the unique identifier (Bundle ID) of your app?',
553
+ 'new.firebase.q.bundleId': 'How would you like the unique identifier (Bundle ID) of your app?',
554
554
  'new.firebase.q.bundleId.hint': 'It works like an address for your app, e.g.: com.mycompany.myapp',
555
555
  'new.firebase.q.bundleId.invalid': 'Invalid format. Use: com.company.app',
556
556
  'new.firebase.q.bundleId.required': 'Bundle ID is required.',
@@ -674,7 +674,7 @@ module.exports = {
674
674
  'new.firebase.interactive.step2': '2. Upgrade to Blaze Plan (Pay-as-you-go): ',
675
675
  'new.firebase.interactive.step3': '3. Enable Authentication (Email/Password): ',
676
676
 
677
- 'new.firebase.interactive.prompt1': 'Did you create the Firestore Database and enabled Authentication?',
677
+ 'new.firebase.interactive.prompt1': 'Did you create the Firestore Database and enable Authentication?',
678
678
  'new.firebase.interactive.prompt2': 'Did you upgrade to the Blaze Plan? (Required for Cloud Functions)',
679
679
  'new.firebase.interactive.googleAuthNote': '* Enable Google Sign-In manually (Email/Password and Anonymous are already enabled): ',
680
680
  'new.firebase.interactive.billingNeeded': 'Blaze plan not yet active. Activate it at the link above, then wait.',
@@ -104,7 +104,7 @@ module.exports = {
104
104
  'modules.feature.base.home.description': 'Pantalla principal de la app después del login',
105
105
  'modules.feature.base.settings.description': 'Tema, idioma, cuenta y preferencias',
106
106
  'modules.feature.base.notifications.description': 'Notificaciones push via Firebase Cloud Messaging (funciona con cualquier backend)',
107
- 'modules.feature.base.subscription.description': 'Pantalla y modelo de suscripcion premium (anade RevenueCat para habilitar pagos reales)',
107
+ 'modules.feature.base.subscription.description': 'Pantalla y modelo de suscripción premium (añade RevenueCat para habilitar pagos reales)',
108
108
  'modules.backend.firebase.description': 'Adapter de backend Firebase',
109
109
  'modules.backend.supabase.description': 'Adapter de backend Supabase',
110
110
  'modules.backend.api.description': 'Adapter de backend API REST/GraphQL',
@@ -116,7 +116,7 @@ module.exports = {
116
116
  'modules.feature.web.description': 'Soporte Web/PWA',
117
117
  'modules.feature.widget.description': 'Widget de pantalla inicial iOS/Android',
118
118
  'modules.feature.ai_chat.description': 'Pantalla de chat con IA usando OpenAI o Gemini',
119
- 'modules.feature.feedback.description': 'Solicitudes y votacion de features dentro de la app',
119
+ 'modules.feature.feedback.description': 'Solicitudes y votación de features dentro de la app',
120
120
  'modules.feature.local_reminders.description': 'Recordatorios locales programados por el usuario (sin servidor)',
121
121
  'modules.feature.ci.description': 'CI/CD: GitHub/GitLab (tests + build) + Codemagic (publicación en tiendas)',
122
122
  'checks.checking': 'Verificando {name}...',
@@ -165,7 +165,7 @@ module.exports = {
165
165
  'license.expired': '❌ Tu suscripción ha expirado. Renueva en kasy.dev.',
166
166
  'license.inactive': '❌ Tu clave fue desactivada. Contacta soporte en kasy.dev.',
167
167
  'license.offlineWarning': '⚠️ No se pudo conectar al servidor — continuando sin conexión.',
168
- 'license.subscriptionExpired': '❌ Tu suscripcion expiró. Las actualizaciones requieren plan activo. Renueva en kasy.dev.\n Tus proyectos siguen funcionando — solo las actualizaciones están bloqueadas.',
168
+ 'license.subscriptionExpired': '❌ Tu suscripción expiró. Las actualizaciones requieren plan activo. Renueva en kasy.dev.\n Tus proyectos siguen funcionando — solo las actualizaciones están bloqueadas.',
169
169
  'prompt.license.enter': '👉 Ingresa tu clave de activación (XXXX-XXXX-XXXX-XXXX)',
170
170
  'prompt.license.invalid': 'Formato inválido. Usa XXXX-XXXX-XXXX-XXXX.',
171
171
  'prompt.appName.enter': 'Ingresa el nombre de tu app',
@@ -176,7 +176,7 @@ module.exports = {
176
176
  'prompt.backend.select': 'Elige el proveedor de backend',
177
177
  'prompt.features.select': 'Elige las features opcionales a incluir',
178
178
  'prompt.features.instructions': 'Espacio para marcar, enter para confirmar',
179
- 'prompt.multiselect.instructions': '\nInstrucciones:\n ↑/↓: Resaltar opción\n ←/→/[space]: Marcar/desmarcar\n a: Marcar/desmarcar todos\n enter/return: Confirmar',
179
+ 'prompt.multiselect.instructions': '\nInstrucciónes:\n ↑/↓: Resaltar opción\n ←/→/[space]: Marcar/desmarcar\n a: Marcar/desmarcar todos\n enter/return: Confirmar',
180
180
  'prompt.multiselect.warnDisabled': 'Usa ↓ para ir a una opción seleccionable, Space para marcar, Enter para confirmar',
181
181
  'prompt.firebase.projectId.enter': 'Ingresa el Firebase Project ID',
182
182
  'prompt.firebase.projectId.required': 'Firebase Project ID es obligatorio.',
@@ -293,20 +293,20 @@ module.exports = {
293
293
  'setup.license.invalid': 'Formato de licencia inválido. Se espera XXXX-XXXX-XXXX-XXXX.',
294
294
  'setup.error.targetNotEmpty': 'El directorio de destino no está vacío: {path}',
295
295
  'setup.error.targetExists': 'El directorio de destino ya existe: {path}',
296
- 'setup.error.coreMissing': 'No se encontro la carpeta del core template: {path}',
296
+ 'setup.error.coreMissing': 'No se encontró la carpeta del core template: {path}',
297
297
  'setup.error.requiredChecksFailed': 'Fallo en verificaciones obligatorias. Ejecuta `kasy doctor`.',
298
298
  'setup.error.generatingFailed': 'Error al generar el proyecto.',
299
299
  'setup.error.conflictHint': 'Consejo: Elimina la carpeta del proyecto y ejecuta de nuevo, o usa un directorio vacío.',
300
300
  'setup.spinner.generating': 'Generando archivos del proyecto...',
301
301
  'setup.success.created': 'Proyecto creado correctamente.',
302
302
  'setup.success.complete': '✓ Setup completado',
303
- 'setup.success.location': 'Ubicacion',
303
+ 'setup.success.location': 'Ubicación',
304
304
  'setup.success.nextSteps': 'Siguientes pasos',
305
305
  'setup.success.stepPubGet': 'flutter pub get',
306
306
  'setup.success.stepRun': 'flutter run',
307
307
  'validate.title': 'Kasy Validate',
308
308
  'validate.success': '✓ Matriz de validación aprobada.',
309
- 'validate.failed': 'La matriz de validación fallo.',
309
+ 'validate.failed': 'La matriz de validación falló.',
310
310
  'validate.error': 'Una o más combinaciones de validación fallaron.',
311
311
  'validate.projectNotFound': 'Proyecto no encontrado',
312
312
  'validate.ok': 'ok',
@@ -318,18 +318,18 @@ module.exports = {
318
318
  'new.banner': '✨ Nuevo app Flutter',
319
319
  'new.subtitle': 'Elige el backend: Firebase, Supabase o API REST.',
320
320
  'new.subtitle2': '🔒 El setup corre en tu máquina con tus credenciales — sin acceso de terceros.',
321
- 'new.q.backend': '¿Donde quieres guardar los datos de tu app?',
321
+ 'new.q.backend': '¿Dónde quieres guardar los datos de tu app?',
322
322
  'new.q.backend.firebase.desc': 'El más fácil para empezar — auth, base de datos y storage listos',
323
323
  'new.q.backend.supabase.desc': 'Base de datos SQL (PostgreSQL) con más control',
324
324
  'new.q.backend.api.desc': 'Ya tienes tu propio servidor',
325
325
 
326
- 'new.q.mode': '¿Como quieres crear la app?',
326
+ 'new.q.mode': '¿Cómo quieres crear la app?',
327
327
  'new.q.mode.quick': '⚡ Rápido (recomendado): todo listo, cero configuración',
328
328
  'new.q.mode.advanced': '🛠 Paso a paso: elegir cada detalle',
329
329
 
330
330
  'new.q.preset': '¿Qué features incluir?',
331
331
  'new.q.preset.starter': '⚡ Starter — analytics + errores + onboarding',
332
- 'new.q.preset.saas': '💰 SaaS — suscripciones + analytics + onboarding + feedback',
332
+ 'new.q.preset.saas': '💰 SaaS — suscripciónes + analytics + onboarding + feedback',
333
333
  'new.q.preset.content': '📱 Contenido — crash reports + analytics + onboarding + AI chat',
334
334
  'new.q.preset.full': '🚀 Completo — todas las features',
335
335
  'new.q.preset.custom': '⚙️ Personalizar — elige feature a feature',
@@ -379,7 +379,7 @@ module.exports = {
379
379
  'deploy.q.serviceAccount': 'Ruta al service account JSON:',
380
380
  'deploy.detected.project': '✓ Proyecto Firebase detectado:',
381
381
  'deploy.detected.serviceAccount': '✓ Service account detectado:',
382
- 'deploy.error.notProject': 'No se encontro firebase.json. Ejecute kasy deploy desde dentro de la carpeta del proyecto.',
382
+ 'deploy.error.notProject': 'No se encontró firebase.json. Ejecute kasy deploy desde dentro de la carpeta del proyecto.',
383
383
 
384
384
  'cli.command.ios.description': 'Publica la app en la App Store (necesita Mac)',
385
385
  'cli.command.ios.configure.description': 'Configurar credenciales Apple para subida local del IPA',
@@ -388,7 +388,7 @@ module.exports = {
388
388
  'cli.command.ios.clean.description': 'Limpia cachés Flutter/Xcode tras fallo en build iOS',
389
389
  'cli.command.ios.picker.intro': 'Publicar en la App Store',
390
390
  'cli.command.ios.picker.message': '¿Qué quieres hacer?',
391
- 'cli.command.ios.help.before': 'Como publicar en la App Store:\n 1) kasy ios configure → hazlo una vez (guarda las credenciales Apple)\n 2) kasy ios release → ejecuta en cada nueva version (build + subida)\n\nUsa "build" si solo quieres generar el IPA, "clean" si un build fallo.\n',
391
+ 'cli.command.ios.help.before': 'Cómo publicar en la App Store:\n 1) kasy ios configure → hazlo una vez (guarda las credenciales Apple)\n 2) kasy ios release → ejecuta en cada nueva versión (build + subida)\n\nUsa "build" si solo quieres generar el IPA, "clean" si un build falló.\n',
392
392
  'cli.command.codemagic.description': 'Compila la app en la nube (sin necesitar Mac)',
393
393
  'cli.command.codemagic.configure.description': 'Configurar credenciales API de Codemagic',
394
394
  'cli.command.codemagic.release.description': 'Iniciar build del workflow iOS en Codemagic',
@@ -396,14 +396,14 @@ module.exports = {
396
396
  'cli.command.codemagic.picker.intro': 'Compilar en la nube con Codemagic',
397
397
  'cli.command.codemagic.picker.message': '¿Qué quieres hacer?',
398
398
  'cli.command.codemagic.picker.statusHint': 'Pregunta el ID del build',
399
- 'cli.command.codemagic.help.before': 'Como compilar con Codemagic (sin necesitar Mac):\n 1) kasy codemagic configure → hazlo una vez (guarda el token de la API)\n 2) kasy codemagic release → inicia un build en la nube por version\n\nUsa "status <buildId>" para seguir el progreso de un build.\n',
399
+ 'cli.command.codemagic.help.before': 'Cómo compilar con Codemagic (sin necesitar Mac):\n 1) kasy codemagic configure → hazlo una vez (guarda el token de la API)\n 2) kasy codemagic release → inicia un build en la nube por versión\n\nUsa "status <buildId>" para seguir el progreso de un build.\n',
400
400
 
401
401
  'ios.configure.title': 'App Store iOS — configuración',
402
402
  'ios.configure.bundleId': 'Bundle ID',
403
403
  'ios.configure.doc': 'Guía',
404
404
  'ios.configure.openingLinks': 'Abriendo App Store Connect en el navegador…',
405
405
  'ios.configure.note.title': 'Voy a pedir 3 cosas de App Store Connect',
406
- 'ios.configure.note.body': '1) API Key ID — en Users and Access → Keys.\n2) Issuer ID — arriba de la misma pagina.\n3) El archivo .p8 (clave privada) descargado al crear la key.\n\nEl App ID es opcional, solo si tienes varias apps.',
406
+ 'ios.configure.note.body': '1) API Key ID — en Users and Access → Keys.\n2) Issuer ID — arriba de la misma página.\n3) El archivo .p8 (clave privada) descargado al crear la key.\n\nEl App ID es opcional, solo si tienes varias apps.',
407
407
  'ios.configure.q.apiKey': 'Key ID de la API App Store Connect',
408
408
  'ios.configure.q.issuerId': 'Issuer ID (UUID)',
409
409
  'ios.configure.q.p8Path': 'Ruta del archivo AuthKey_XXXXX.p8 descargado',
@@ -419,12 +419,12 @@ module.exports = {
419
419
  'ios.release.task.building': 'Compilando IPA (puede tardar 2-5 min)…',
420
420
  'ios.release.task.uploading': 'Subiendo IPA a App Store Connect…',
421
421
  'ios.release.task.done': 'Compilacion + subida completadas',
422
- 'ios.release.task.failed': 'La compilacion fallo — mira la salida debajo',
422
+ 'ios.release.task.failed': 'La compilación falló — mira la salida debajo',
423
423
  'ios.build.title': 'Generando IPA iOS…',
424
424
  'ios.build.success': 'IPA generado',
425
425
  'ios.build.task.building': 'Compilando IPA (puede tardar 2-5 min)…',
426
426
  'ios.build.task.done': 'IPA listo en build/ios/ipa/',
427
- 'ios.build.task.failed': 'La compilacion fallo — mira la salida debajo',
427
+ 'ios.build.task.failed': 'La compilación falló — mira la salida debajo',
428
428
  'ios.error.notProject': 'No es un proyecto Flutter iOS (pubspec.yaml + ios/ requeridos).',
429
429
  'ios.error.noScript': 'scripts/release-ios.sh no encontrado. Ejecute: kasy update ios-release',
430
430
  'ios.error.notMac': 'Release iOS local requiere macOS con Xcode.',
@@ -527,7 +527,7 @@ module.exports = {
527
527
  'add.iosRelease.success': 'Archivos de release iOS agregados',
528
528
  'add.iosRelease.already': 'Archivos de release iOS ya presentes',
529
529
 
530
- 'new.api.q.baseUrl': '¿Cual es la URL base de tu API?',
530
+ 'new.api.q.baseUrl': '¿Cuál es la URL base de tu API?',
531
531
  'new.api.q.baseUrl.hint': 'https://api.example.com',
532
532
  'new.firebase.banner': '🔥 Nuevo app Flutter — Firebase',
533
533
  'new.firebase.subtitle': 'Proyecto completo: auth, Firestore, notificaciones, login social y más.',
@@ -550,7 +550,7 @@ module.exports = {
550
550
  'new.firebase.q.appName.hint': 'ej: Mi App Increible',
551
551
  'new.firebase.q.appName.required': 'El nombre de la app es obligatorio.',
552
552
 
553
- 'new.firebase.q.bundleId': '¿Cuál es el identificador único (Bundle ID) de tu app?',
553
+ 'new.firebase.q.bundleId': '¿Cómo quieres el identificador único (Bundle ID) de tu app?',
554
554
  'new.firebase.q.bundleId.hint': 'Funciona como una dirección para tu app, ej: com.miempresa.miapp',
555
555
  'new.firebase.q.bundleId.invalid': 'Formato inválido. Usa: com.empresa.app',
556
556
  'new.firebase.q.bundleId.required': 'El Bundle ID es obligatorio.',
@@ -565,9 +565,9 @@ module.exports = {
565
565
  'new.modules.header.features': '── Pantallas y features ──',
566
566
  'new.modules.header.feedback': '── Feedback (Firebase + Supabase) ──',
567
567
  'new.modules.header.ci': '── CI/CD ──',
568
- 'new.modules.header.monetization': '── Monetizacion ──',
569
- 'new.firebase.module.revenuecat': '💰 RevenueCat (suscripciones móvil)',
570
- 'new.firebase.module.stripe': '🌐 Stripe (suscripciones en web)',
568
+ 'new.modules.header.monetization': '── Monetización ──',
569
+ 'new.firebase.module.revenuecat': '💰 RevenueCat (suscripciónes móvil)',
570
+ 'new.firebase.module.stripe': '🌐 Stripe (suscripciónes en web)',
571
571
  'new.firebase.module.sentry': '🚨 Crash Reports (Sentry)',
572
572
  'new.firebase.module.analytics': '📊 Analytics (Mixpanel)',
573
573
  'new.firebase.module.facebook': '👤 Facebook (Login + Ads)',
@@ -597,7 +597,7 @@ module.exports = {
597
597
  'new.firebase.q.revenuecat.android': 'Clave API RevenueCat para Android',
598
598
  'new.firebase.q.revenuecat.ios': 'Clave API RevenueCat para iOS',
599
599
  'new.firebase.q.paywall': '¿Qué estilo de paywall?',
600
- 'new.firebase.q.paywall.hint': 'Layout de la pantalla de suscripciones — puedes cambiarlo después en RevenueCat',
600
+ 'new.firebase.q.paywall.hint': 'Layout de la pantalla de suscripciónes — puedes cambiarlo después en RevenueCat',
601
601
  'new.firebase.q.stripe.secretKey': 'Stripe Secret Key (sk_test_… / sk_live_…) — opcional, configurar después',
602
602
  'new.firebase.q.stripe.webhookSecret': 'Stripe Webhook Signing Secret (whsec_…) — opcional, configurar después',
603
603
  'new.firebase.q.stripe.webhookSecret.hint': 'En Stripe → Developers → Webhooks → tu endpoint',
@@ -652,7 +652,7 @@ module.exports = {
652
652
  'new.firebase.success.googleSignIn': '• Login con Gmail: 1) Activa en Firebase Console → Auth → Sign-in method → Google. 2) El CLI rellena kGoogleWebClientId (google_auth_options.dart) desde google-services.json para Android/iOS. Web usa signInWithPopup.',
653
653
  'new.firebase.success.sentry': '• Sentry: Para reportes de error en producción. Obtén DSN en sentry.io. Ya en launch.json para dev. Para release: --dart-define=SENTRY_DSN=xxx',
654
654
  'new.firebase.success.mixpanel': '• Mixpanel: Para analytics en producción. Ya en launch.json para dev. Para release: --dart-define=MIXPANEL_TOKEN=xxx',
655
- 'new.firebase.success.web': '• Web (adicional): Android e iOS funcionan con normalidad. Para probar en browser: "flutter run -d chrome" o "flutter run -d web-server --web-port=5000". Para publicar: "flutter build web". Las notificaciones push son exclusivas de mobile y se desactivan automaticamente en web.',
655
+ 'new.firebase.success.web': '• Web (adicional): Android e iOS funcionan con normalidad. Para probar en browser: "flutter run -d chrome" o "flutter run -d web-server --web-port=5000". Para publicar: "flutter build web". Las notificaciones push son exclusivas de mobile y se desactivan automáticamente en web.',
656
656
 
657
657
  'new.firebase.q.deploy': '¿Hacer deploy de Cloud Functions + reglas de Firestore ahora?',
658
658
  'new.firebase.q.deploy.hint': 'Requiere plan Blaze y firebase-tools instalado globalmente',
@@ -676,7 +676,7 @@ module.exports = {
676
676
 
677
677
  'new.firebase.interactive.prompt1': '¿Ya creaste Firestore y habilitaste Autenticación en el enlace arriba?',
678
678
  'new.firebase.interactive.prompt2': '¿Hiciste el upgrade al Plan Blaze? (Requerido para Cloud Functions)',
679
- 'new.firebase.interactive.googleAuthNote': '* Activa Google Sign-In manualmente (Email/Contraseña y Anônimo ya fueron activados): ',
679
+ 'new.firebase.interactive.googleAuthNote': '* Activa Google Sign-In manualmente (Email/Contraseña y Anónimo ya fueron activados): ',
680
680
  'new.firebase.interactive.billingNeeded': 'Plan Blaze aún no activo. Actívalo en el enlace de arriba y espera la detección automática.',
681
681
  'new.firebase.interactive.billingWaiting': 'Verificando estado del Blaze...',
682
682
  'new.firebase.interactive.billingTimeout': 'Plan Blaze no confirmado tras el tiempo límite. Despliegue omitido — ejecuta manualmente cuando estés listo.',
@@ -705,15 +705,15 @@ module.exports = {
705
705
  'new.supabase.prereq.login': ' Si creas manualmente: ten URL y anon key del proyecto listos',
706
706
  'new.supabase.loginRequired': '⚠️ Debes estar conectado para crear proyecto. Ejecuta supabase login primero.',
707
707
  'new.supabase.loginCommand': ' supabase login',
708
- 'new.supabase.success.done.db': '• Base de datos: tablas, politicas RLS',
709
- 'new.supabase.success.done.storage': '• Storage: bucket kasy con politicas',
708
+ 'new.supabase.success.done.db': '• Base de datos: tablas, políticas RLS',
709
+ 'new.supabase.success.done.storage': '• Storage: bucket kasy con políticas',
710
710
  'new.supabase.success.done.webhook': '• Edge Function revenuecat-webhook',
711
711
  'new.supabase.success.done.secrets': '• Secrets de Edge Function (si se informaron)',
712
712
  'new.supabase.success.done.launch': '• launch.json con Sentry, Mixpanel, RevenueCat',
713
713
  'new.supabase.success.auth': '• Auth: Email ya activado. Activa Google, Apple y Facebook en: {authUrl}',
714
- 'new.supabase.success.storage': '• Storage: Bucket kasy creado con politicas (listo)',
714
+ 'new.supabase.success.storage': '• Storage: Bucket kasy creado con políticas (listo)',
715
715
  'new.supabase.success.fcm': '• Push (FCM): Configura en Firebase Console (plan Blaze). App lista para Supabase + FCM. URL: {fcmUrl}',
716
- 'new.supabase.success.deployLater': '• Deploy del backend cuando estes listo (desde dentro de la carpeta del proyecto):',
716
+ 'new.supabase.success.deployLater': '• Deploy del backend cuando estés listo (desde dentro de la carpeta del proyecto):',
717
717
 
718
718
  'new.api.prereq.1': '1. Tu API REST corriendo y accesible',
719
719
  'new.api.prereq.2': '2. URL base de la API (ej: https://api.yourapp.com)',
@@ -725,11 +725,11 @@ module.exports = {
725
725
  'new.outdated.hint': 'los proyectos creados ahora no tendrán las últimas mejoras.',
726
726
  'new.outdated.upgradeNow': '¿Actualizar a la última versión antes de crear? (requiere suscripción activa)',
727
727
  'new.outdated.upgraded': '¡kasy actualizado! Ejecuta kasy new nuevamente.',
728
- 'new.success.title': '¡Proyecto creado con exito!',
728
+ 'new.success.title': '¡Proyecto creado con éxito!',
729
729
  'new.success.featuresInstalled': 'Recursos activados:',
730
730
  'new.success.bundleId': 'Identificador de la app (bundle ID)',
731
731
  'new.success.bundleId.hint': 'Identificador único de tu app en Android, iOS y Firebase (push).',
732
- 'new.success.nextSteps': 'Proximos pasos:',
732
+ 'new.success.nextSteps': 'Próximos pasos:',
733
733
  'new.success.step.cd': 'Ve a la carpeta del proyecto:',
734
734
  'new.success.step.deploy': 'Sube el servidor a Firebase (DB + funciones):',
735
735
  'new.success.step.configure': 'Configura claves opcionales cuando las tengas (RevenueCat, Sentry, etc.):',
@@ -747,7 +747,7 @@ module.exports = {
747
747
  'new.fcm.failSupabase': 'no generada (permiso de GCP aún propagando); define FIREBASE_SERVICE_ACCOUNT_JSON en los secrets de Supabase',
748
748
  'new.fcm.failApi': 'no generada (permiso de GCP aún propagando); ejecuta el comando de nuevo en unos minutos',
749
749
  'new.sha1.registering': 'Registrando SHA-1 para Google Sign-In (Android)…',
750
- 'new.sha1.failed': 'SHA-1 no añadido automaticamente: {error}',
750
+ 'new.sha1.failed': 'SHA-1 no añadido automáticamente: {error}',
751
751
  'new.sha1.manual': 'Agregalo manualmente para que Google Sign-In funcione en Android:',
752
752
  'new.sha1.skipped.apiFailed': 'SHA-1 no añadido automáticamente. Motivo: {error}',
753
753
  'new.sha1.skipped.other': 'SHA-1 no añadido: {reason}',
@@ -798,7 +798,7 @@ module.exports = {
798
798
  'reset.prompt.xcodeOpen.skip': 'Dejar Xcode abierto e intentar igual',
799
799
  'reset.prompt.xcodeOpen.cancel': 'Cancelar reset',
800
800
  'reset.info.xcodeClosed': 'Xcode cerrado.',
801
- 'reset.prompt.reinstallNow': 'App removida. Verifica en el dispositivo y reinstalar ahora?',
801
+ 'reset.prompt.reinstallNow': 'App removida. Verifica en el dispositivo y ¿reinstalar ahora?',
802
802
  'reset.prompt.clearLauncherCache': '¿Limpiar también la caché del launcher ({pkg})? Soluciona el preview gris de los widgets.',
803
803
  'reset.success.launcherCleared': 'Caché del launcher limpiada ({pkg}). Pantalla de inicio intacta.',
804
804
  'reset.warn.launcherCacheFailed': 'No se pudo limpiar la caché del launcher.',
@@ -812,7 +812,7 @@ module.exports = {
812
812
  'run.updateHint.suffix': 'para ver las novedades',
813
813
  'run.spinner.building': 'Iniciando Flutter…',
814
814
  'run.spinner.ready': 'Flutter listo — usa r (reload), R (restart), q (salir)',
815
- 'run.spinner.failed': 'Flutter run fallo antes de iniciar',
815
+ 'run.spinner.failed': 'Flutter run falló antes de iniciar',
816
816
  'run.stage.gradleFirstTime': 'Compilando Android (1.ª vez tarda 5-15 min — Gradle bajando dependencias)…',
817
817
  'run.stage.gradle': 'Compilando Android…',
818
818
  'run.stage.xcode': 'Compilando iOS…',
@@ -820,7 +820,7 @@ module.exports = {
820
820
  'run.stage.installing': 'Instalando en el dispositivo…',
821
821
  'run.stage.syncing': 'Sincronizando archivos con el dispositivo…',
822
822
  'run.stage.buildSuccess': 'Build listo — abriendo la app…',
823
- 'run.error.notFlutterProject': 'No se encontro pubspec.yaml. Ejecuta este comando dentro de un proyecto Flutter.',
823
+ 'run.error.notFlutterProject': 'No se encontró pubspec.yaml. Ejecuta este comando dentro de un proyecto Flutter.',
824
824
  'run.error.flutterNotFound': 'Flutter no encontrado. Verifica que Flutter este instalado y en el PATH.',
825
825
  'run.rc.usingTest': 'RevenueCat: usando clave de prueba (test_) — simulador/emulador',
826
826
  'run.rc.usingProd': 'RevenueCat: usando claves de producción — dispositivo físico',
@@ -930,26 +930,26 @@ module.exports = {
930
930
 
931
931
  // notifications command
932
932
  'cli.command.notifications.description': 'Edita los textos de las notificaciones locales y recordatorios',
933
- 'cli.command.notifications.text.description': 'Define titulos y mensajes de notificaciones locales',
933
+ 'cli.command.notifications.text.description': 'Define títulos y mensajes de notificaciones locales',
934
934
  'cli.command.notifications.picker.intro': 'Editar textos de las notificaciones',
935
935
  'cli.command.notifications.picker.message': '¿Qué quieres hacer?',
936
- 'notifications.error.notKasyProject': 'No se encontro kit_setup.json. Ejecuta dentro de un proyecto Kasy.',
936
+ 'notifications.error.notKasyProject': 'No se encontró kit_setup.json. Ejecuta dentro de un proyecto Kasy.',
937
937
  'notifications.error.noI18n': 'lib/i18n/*.i18n.json no encontrado en este proyecto.',
938
938
  'notifications.error.noFeatures': 'lib/core/config/features.dart no encontrado.',
939
939
  'notifications.error.notEnabled': 'Notificaciones locales desactivadas. Ejecuta: kasy add local_reminders',
940
940
  'notifications.error.cancelled': 'Cancelado.',
941
941
  'notifications.prompt.intro': 'Textos de notificación local',
942
942
  'notifications.prompt.hint': 'Demo Home = prueba instantanea en Features. Recordatorio = alerta programada en Ajustes.',
943
- 'notifications.prompt.demoTitle': 'Demo Home — titulo de la notificación',
943
+ 'notifications.prompt.demoTitle': 'Demo Home — título de la notificación',
944
944
  'notifications.prompt.demoBody': 'Demo Home — descripción / cuerpo',
945
- 'notifications.prompt.reminderTitle': 'Recordatorio programado — titulo',
945
+ 'notifications.prompt.reminderTitle': 'Recordatorio programado — título',
946
946
  'notifications.prompt.reminderBody': 'Recordatorio programado — cuerpo',
947
947
  'notifications.prompt.required': 'Obligatorio.',
948
948
  'notifications.writing': 'Actualizando lib/i18n/*.i18n.json...',
949
949
  'notifications.written': 'Actualizado: {langs}',
950
950
  'notifications.slang': 'Ejecutando dart run slang...',
951
951
  'notifications.slangDone': 'Traducciones regeneradas',
952
- 'notifications.slangFailed': 'dart run slang fallo — ejecuta manualmente en el proyecto',
952
+ 'notifications.slangFailed': 'dart run slang falló — ejecuta manualmente en el proyecto',
953
953
  'notifications.done': 'Textos de notificación local actualizados.',
954
954
  'notifications.summary.demo': 'Home → Features (demo):',
955
955
  'notifications.summary.reminder': 'Ajustes → Recordatorios (programado):',
@@ -962,7 +962,7 @@ module.exports = {
962
962
  'add.applying': 'Agregando feature: {module}',
963
963
  'add.applyingPatch': 'Aplicando cambios de la feature...',
964
964
  'add.patchApplied': 'Patch aplicado',
965
- 'add.patchFailed': 'Patch fallo — revisa la salida anterior',
965
+ 'add.patchFailed': 'Patch falló — revisa la salida anterior',
966
966
  'add.pubGet': 'Instalando paquetes de Flutter (flutter pub get)...',
967
967
  'add.pubGetDone': 'Dependencias actualizadas',
968
968
  'add.pubGetFailed': 'Falló al instalar paquetes de Flutter — ejecuta `flutter pub get` manualmente',
@@ -980,31 +980,31 @@ module.exports = {
980
980
  'add.prompt.rcAndroidKey': 'RevenueCat Android API key (deja en blanco para configurar después):',
981
981
  'add.prompt.rcIosKey': 'RevenueCat iOS API key (deja en blanco para configurar después):',
982
982
  'add.note.facebook': 'Agrega tu Facebook App ID y token en .vscode/launch.json (FB_APP_ID, FB_CLIENT_TOKEN).',
983
- 'new.q.ai_chat.configureNow': 'Configurar el agente de Chat IA ahora?',
983
+ 'new.q.ai_chat.configureNow': '¿Configurar el agente de Chat IA ahora?',
984
984
  'new.q.ai_chat.configureNow.hint': 'Puedes omitir y ejecutar "kasy add ai_chat" después',
985
985
  'add.ai_chat.reconfigure': 'Feature ya activa — reconfigurando solo las credenciales.',
986
986
  'add.prompt.aiProvider': 'Proveedor de IA:',
987
- 'add.prompt.aiSystemPrompt': 'Instruccion del agente — system prompt (deja en blanco para ninguna):\n Ejemplo: "Eres un asistente de soporte del app Fitsync. Solo responde sobre entrenamientos."\n >',
987
+ 'add.prompt.aiSystemPrompt': 'Instrucción del agente — system prompt (deja en blanco para ninguna):\n Ejemplo: "Eres un asistente de soporte del app Fitsync. Solo responde sobre entrenamientos."\n >',
988
988
  'add.prompt.aiApiKey': 'Clave de API (OpenAI o Gemini) — se guarda en el servidor, nunca en el app (deja en blanco para configurar después):',
989
989
  'add.prompt.aiEndpoint': 'URL de tu endpoint IA (deja en blanco para configurar después):',
990
990
  'add.ai_chat.settingSecret': 'Guardando clave de API como secret en el servidor...',
991
991
  'add.ai_chat.secretSet': 'Clave de API guardada como secret',
992
- 'add.ai_chat.secretFailed': 'No se pudo guardar el secret automaticamenteconfiguralo manualmente (ver instrucciones abajo)',
992
+ 'add.ai_chat.secretFailed': 'No se pudo guardar el secret automáticamenteconfigúralo manualmente (ver instrucciones abajo)',
993
993
  'add.ai_chat.skipSecret': 'Clave de API omitida — configura antes del deploy via CLI del servidor',
994
- 'add.ai_chat.deploying': 'Desplegando funcion IA en el servidor...',
995
- 'add.ai_chat.deployed': 'Funcion IA desplegada con exito',
996
- 'add.ai_chat.deployFailed': 'Deploy automático fallo — despliega manualmente (ver instrucciones abajo)',
997
- 'add.ai_chat.nextSteps.firebase': '\n Proximos pasos:\n 1. El AI_CHAT_ENDPOINT en .vscode/launch.json ya fue pre-llenado.\n 2. Corre el app: kasy run\n',
998
- 'add.ai_chat.nextSteps.firebase.deployFailed': '\n Proximos pasos:\n 1. Deploy manual: firebase deploy --only functions:aiChat\n 2. El AI_CHAT_ENDPOINT en .vscode/launch.json ya fue pre-llenado.\n 3. Corre el app: kasy run\n',
999
- 'add.ai_chat.nextSteps.supabase': '\n Proximos pasos:\n 1. El AI_CHAT_ENDPOINT en .vscode/launch.json ya fue pre-llenado.\n 2. Corre el app: kasy run\n',
1000
- 'add.ai_chat.nextSteps.supabase.deployFailed': '\n Proximos pasos:\n 1. Deploy manual: supabase functions deploy ai-chat --no-verify-jwt\n 2. El AI_CHAT_ENDPOINT en .vscode/launch.json ya fue pre-llenado.\n 3. Corre el app: kasy run\n',
1001
- 'add.ai_chat.nextSteps.api': '\n Proximos pasos:\n 1. Crea un endpoint en tu servidor que acepte {message, history} y llame tu IA.\n 2. Actualiza AI_CHAT_ENDPOINT en .vscode/launch.json con la URL de tu endpoint.\n 3. Corre el app: kasy run\n',
994
+ 'add.ai_chat.deploying': 'Desplegando función IA en el servidor...',
995
+ 'add.ai_chat.deployed': 'Función IA desplegada con éxito',
996
+ 'add.ai_chat.deployFailed': 'Deploy automático falló — despliega manualmente (ver instrucciones abajo)',
997
+ 'add.ai_chat.nextSteps.firebase': '\n Próximos pasos:\n 1. El AI_CHAT_ENDPOINT en .vscode/launch.json ya fue pre-llenado.\n 2. Corre el app: kasy run\n',
998
+ 'add.ai_chat.nextSteps.firebase.deployFailed': '\n Próximos pasos:\n 1. Deploy manual: firebase deploy --only functions:aiChat\n 2. El AI_CHAT_ENDPOINT en .vscode/launch.json ya fue pre-llenado.\n 3. Corre el app: kasy run\n',
999
+ 'add.ai_chat.nextSteps.supabase': '\n Próximos pasos:\n 1. El AI_CHAT_ENDPOINT en .vscode/launch.json ya fue pre-llenado.\n 2. Corre el app: kasy run\n',
1000
+ 'add.ai_chat.nextSteps.supabase.deployFailed': '\n Próximos pasos:\n 1. Deploy manual: supabase functions deploy ai-chat --no-verify-jwt\n 2. El AI_CHAT_ENDPOINT en .vscode/launch.json ya fue pre-llenado.\n 3. Corre el app: kasy run\n',
1001
+ 'add.ai_chat.nextSteps.api': '\n Próximos pasos:\n 1. Crea un endpoint en tu servidor que acepte {message, history} y llame tu IA.\n 2. Actualiza AI_CHAT_ENDPOINT en .vscode/launch.json con la URL de tu endpoint.\n 3. Corre el app: kasy run\n',
1002
1002
  'cli.command.remove.description': 'Elimina algo que ya no usas (ej: kasy remove sentry)',
1003
1003
  'remove.error.noModule': 'Ingresa el nombre de la feature. Uso: kasy remove <feature>',
1004
1004
  'remove.error.notKasyProject': 'kit_setup.json no encontrado. Ejecuta este comando dentro de un proyecto Kasy.',
1005
1005
  'remove.error.unknownModule': 'Feature desconocida: {module}\nDisponibles: {list}',
1006
1006
  'remove.error.notActive': 'La feature "{module}" no está activa en este proyecto.',
1007
- 'remove.confirm': 'Eliminar la feature "{module}"? Esto borrara archivos y dependencias.',
1007
+ 'remove.confirm': '¿Eliminar la feature "{module}"? Esto borrará archivos y dependencias.',
1008
1008
  'remove.cancelled': 'Cancelado.',
1009
1009
  'remove.removing': 'Eliminando feature: {module}',
1010
1010
  'remove.pubGet': 'Instalando paquetes de Flutter (flutter pub get)...',
@@ -1014,8 +1014,8 @@ module.exports = {
1014
1014
  'remove.buildRunnerDone': 'Generación de código completada',
1015
1015
  'remove.buildRunnerFailed': 'Generación de código falló — ejecuta `dart run build_runner build` manualmente',
1016
1016
  'remove.success': 'Feature "{module}" eliminada exitosamente.',
1017
- 'remove.warn.ci': 'Archivos de CI eliminados. Si tenias workflows personalizados, restauralos desde git.',
1018
- 'remove.warn.sentry.shared': 'sentry_flutter conservado — todavia requerido por features activas (revenuecat/facebook).',
1017
+ 'remove.warn.ci': 'Archivos de CI eliminados. Si tenías workflows personalizados, restáuralos desde git.',
1018
+ 'remove.warn.sentry.shared': 'sentry_flutter conservado — todavía requerido por features activas (revenuecat/facebook).',
1019
1019
  'cli.command.update.description': 'Actualiza partes de tu app a la última versión (ej: kasy update components)',
1020
1020
  'cli.command.update.targetArg': 'Objetivo a actualizar (ej.: revenuecat, sentry, components)',
1021
1021
  'update.error.noProject': 'kit_setup.json no encontrado. Ejecuta dentro de un proyecto Kasy.',
@@ -1031,9 +1031,9 @@ module.exports = {
1031
1031
  'update.howToUpdateComponents': 'Para actualizar componentes base:',
1032
1032
  'update.warn.commit': 'Esto sobrescribirá los archivos de la feature "{module}". Asegúrate de haber hecho commit antes.',
1033
1033
  'update.warn.commitComponents': 'Esto sobrescribirá archivos de los componentes base. Asegúrate de haber hecho commit antes.',
1034
- 'update.confirm': 'Sobrescribir archivos de la feature "{module}" con la versión más reciente?',
1035
- 'update.confirmComponents': 'Sobrescribir archivos de los componentes base con la versión más reciente?',
1036
- 'update.confirmCore': 'Sobrescribir archivos de core (animaciones, widgets, tema, herramientas dev) con la versión más reciente?',
1034
+ 'update.confirm': '¿Sobrescribir archivos de la feature "{module}" con la versión más reciente?',
1035
+ 'update.confirmComponents': '¿Sobrescribir archivos de los componentes base con la versión más reciente?',
1036
+ 'update.confirmCore': '¿Sobrescribir archivos de core (animaciones, widgets, tema, herramientas dev) con la versión más reciente?',
1037
1037
  'update.cancelled': 'Cancelado.',
1038
1038
  'update.applying': 'Aplicando actualización de la feature: {module}',
1039
1039
  'update.applyingComponents': 'Aplicando actualización de componentes base...',
@@ -43,7 +43,7 @@ module.exports = {
43
43
  'cli.command.setup.langName': 'idioma',
44
44
  'cli.command.setup.langOption': 'Idioma dos prompts (en, pt, es)',
45
45
  'cli.command.setup.backendOption': 'Adapter de backend (firebase, supabase, api)',
46
- 'cli.command.setup.featuresOption': 'Features opcionais separadas por virgula (web,widget,ai_chat,revenuecat,ci)',
46
+ 'cli.command.setup.featuresOption': 'Features opcionais separadas por vírgula (web,widget,ai_chat,revenuecat,ci)',
47
47
  'cli.command.help.paramName': 'comando',
48
48
  'cli.command.doctor.description': 'Verifica se o seu computador está pronto para rodar a Kasy',
49
49
  'cli.command.modules.description': 'Mostra o que já vem incluso e o que você pode adicionar',
@@ -79,7 +79,7 @@ module.exports = {
79
79
  'setup.flutterfire.installing': 'Instalando FlutterFire CLI… (pode levar 1-2 min)',
80
80
  'setup.installingNamed': 'Instalando {name}…',
81
81
  'setup.warn.hang': 'Se travar, execute manualmente: flutterfire --version',
82
- 'setup.warn.supabase': 'Usando Supabase ou API propria? O Firebase ainda ajuda com push e remote config.',
82
+ 'setup.warn.supabase': 'Usando Supabase ou API própria? O Firebase ainda ajuda com push e remote config.',
83
83
  'doctor.title': 'Kasy Doctor',
84
84
  'doctor.baseEnvironment': 'Ambiente base',
85
85
  'doctor.optionalBackend': 'Ferramentas opcionais de backend',
@@ -91,7 +91,7 @@ module.exports = {
91
91
  'doctor.gcpBilling.title': 'Google Cloud Billing (Firebase Blaze)',
92
92
  'doctor.gcpBilling.found': '{count} conta(s) de faturamento ativa(s):',
93
93
  'doctor.gcpBilling.missing': 'Nenhuma conta de faturamento encontrada. Crie uma antes de rodar `kasy new` com Firebase:',
94
- 'doctor.requiredMissing': 'Dependencias obrigatórias ausentes. Corrija os erros acima e execute o doctor novamente.',
94
+ 'doctor.requiredMissing': 'Dependências obrigatórias ausentes. Corrija os erros acima e execute o doctor novamente.',
95
95
  'doctor.requiredPassed': '✓ Verificações obrigatórias de ambiente aprovadas.',
96
96
  'modules.backends': 'Backends disponíveis:',
97
97
  'modules.featuresBase': 'Sempre incluído:',
@@ -101,8 +101,8 @@ module.exports = {
101
101
  'modules.tag.enhances': 'ativa {target}',
102
102
  'modules.hint.subscriptionNoRc': 'Dica: a tela de Subscriptions está inclusa mas inativa. Rode `kasy add revenuecat` para habilitar pagamentos reais.',
103
103
  'modules.feature.base.authentication.description': 'Cadastro, login e gerenciamento de conta',
104
- 'modules.feature.base.home.description': 'Tela principal do app apos o login',
105
- 'modules.feature.base.settings.description': 'Tema, idioma, conta e preferencias',
104
+ 'modules.feature.base.home.description': 'Tela principal do app após o login',
105
+ 'modules.feature.base.settings.description': 'Tema, idioma, conta e preferências',
106
106
  'modules.feature.base.notifications.description': 'Notificações push via Firebase Cloud Messaging (funciona com qualquer backend)',
107
107
  'modules.feature.base.subscription.description': 'Tela e modelo de assinatura premium (adicione RevenueCat para habilitar pagamentos reais)',
108
108
  'modules.backend.firebase.description': 'Adapter de backend Firebase',
@@ -116,8 +116,8 @@ module.exports = {
116
116
  'modules.feature.web.description': 'Suporte Web/PWA',
117
117
  'modules.feature.widget.description': 'Widget de tela inicial iOS/Android',
118
118
  'modules.feature.ai_chat.description': 'Tela de chat com IA usando OpenAI ou Gemini',
119
- 'modules.feature.feedback.description': 'Pedidos e votacao de features dentro do app',
120
- 'modules.feature.local_reminders.description': 'Lembretes locais agendados pelo usuario (sem servidor)',
119
+ 'modules.feature.feedback.description': 'Pedidos e votação de features dentro do app',
120
+ 'modules.feature.local_reminders.description': 'Lembretes locais agendados pelo usuário (sem servidor)',
121
121
  'modules.feature.ci.description': 'CI/CD: GitHub/GitLab (testes + build) + Codemagic (publicação nas lojas)',
122
122
  'checks.checking': 'Verificando {name}...',
123
123
  'checks.found': '{name} encontrado',
@@ -151,13 +151,13 @@ module.exports = {
151
151
  'error.hint.supabaseLogin': 'Você não está logado no Supabase. Execute: supabase login',
152
152
  'error.hint.gcloudAuth': 'Você precisa autenticar com gcloud. Execute: gcloud auth login',
153
153
  'error.hint.flutterRunFailed': 'Flutter não conseguiu rodar o app. Tente de novo com --verbose para ver a saída completa.',
154
- 'checks.diagnostic.xcodeLicense': '{name} está instalado, mas o Xcode precisa que a licenca seja aceita. Execute: sudo xcodebuild -license',
154
+ 'checks.diagnostic.xcodeLicense': '{name} está instalado, mas o Xcode precisa que a licença seja aceita. Execute: sudo xcodebuild -license',
155
155
  'checks.diagnostic.xcodeCli': '{name} está instalado, mas faltam as Command Line Tools do Xcode. Execute: xcode-select --install',
156
156
  'banner.title': 'Kasy CLI · Gerador Flutter SaaS',
157
157
  'welcome.firstRun': 'Bem-vindo ao Kasy CLI!',
158
158
  'welcome.chooseLanguage': 'Primeiro, escolha seu idioma:',
159
159
  'prompt.language.select': 'Escolha seu idioma',
160
- 'prompt.cancelled': 'Comando cancelado pelo usuario.',
160
+ 'prompt.cancelled': 'Comando cancelado pelo usuário.',
161
161
  'license.required': '🔑 Chave de ativação necessária para usar o Kasy CLI',
162
162
  'license.checking': 'Validando chave de ativação...',
163
163
  'license.saved': '✅ Chave validada com sucesso',
@@ -175,9 +175,9 @@ module.exports = {
175
175
  'prompt.bundleId.invalid': 'O bundle ID deve seguir o formato com.company.app.',
176
176
  'prompt.backend.select': 'Escolha o provedor de backend',
177
177
  'prompt.features.select': 'Escolha as features opcionais para incluir',
178
- 'prompt.features.instructions': 'Espaco para marcar, enter para confirmar',
179
- 'prompt.multiselect.instructions': '\nInstrucoes:\n ↑/↓: Destacar opção\n ←/→/[space]: Marcar/desmarcar\n a: Marcar/desmarcar todos\n enter/return: Confirmar',
180
- 'prompt.multiselect.warnDisabled': 'Use ↓ para ir a uma opção selecionavel, Space para marcar, Enter para confirmar',
178
+ 'prompt.features.instructions': 'Espaço para marcar, enter para confirmar',
179
+ 'prompt.multiselect.instructions': '\nInstruções:\n ↑/↓: Destacar opção\n ←/→/[space]: Marcar/desmarcar\n a: Marcar/desmarcar todos\n enter/return: Confirmar',
180
+ 'prompt.multiselect.warnDisabled': 'Use ↓ para ir a uma opção selecionável, Space para marcar, Enter para confirmar',
181
181
  'prompt.firebase.projectId.enter': 'Digite o Firebase Project ID',
182
182
  'prompt.firebase.projectId.required': 'Firebase Project ID é obrigatório.',
183
183
  'prompt.supabase.url.enter': 'Digite a URL do Supabase',
@@ -236,7 +236,7 @@ module.exports = {
236
236
  'new.firebase.billing.stillMissing': 'Ainda não encontrei nenhuma conta de faturamento ativa. Finalize a criação no Console e rode `kasy new` novamente.',
237
237
  'new.firebase.create.installTitle': 'Para instalar o gcloud CLI, execute:',
238
238
  'new.firebase.create.installCommand': 'Comando',
239
- 'new.firebase.create.installAfter': 'Depois faca login',
239
+ 'new.firebase.create.installAfter': 'Depois faça login',
240
240
  'new.firebase.create.installUrl': 'Ou baixe em',
241
241
  'new.firebase.create.authCommand': 'Execute: gcloud auth login',
242
242
  'new.firebase.create.gcloudMissing': 'O Google Cloud CLI (gcloud) é necessário para criar o projeto Firebase do zero, e ainda não está instalado. Vou instalar automaticamente agora.',
@@ -286,9 +286,9 @@ module.exports = {
286
286
  'new.supabase.projectsRequired': 'Nenhum projeto encontrado nesta organização.',
287
287
  'new.supabase.q.dbPassword.existing': 'Senha do banco do projeto (necessária para vincular e aplicar migrations)',
288
288
  'new.supabase.existingLinked': 'Projeto pronto para vincular e aplicar migrations.',
289
- 'setup.license.loaded': '✓ Chave de licenca carregada da configuração local.',
290
- 'setup.license.saved': '✓ Chave de licenca validada e salva.',
291
- 'setup.license.invalid': 'Formato de licenca inválido. Esperado XXXX-XXXX-XXXX-XXXX.',
289
+ 'setup.license.loaded': '✓ Chave de licença carregada da configuração local.',
290
+ 'setup.license.saved': '✓ Chave de licença validada e salva.',
291
+ 'setup.license.invalid': 'Formato de licença inválido. Esperado XXXX-XXXX-XXXX-XXXX.',
292
292
  'setup.error.targetNotEmpty': 'O diretório de destino não está vazio: {path}',
293
293
  'setup.error.targetExists': 'O diretório de destino já existe: {path}',
294
294
  'setup.error.coreMissing': 'Pasta do core template não encontrada: {path}',
@@ -297,15 +297,15 @@ module.exports = {
297
297
  'setup.error.conflictHint': 'Dica: Remova a pasta do projeto e execute novamente, ou use um diretório vazio.',
298
298
  'setup.spinner.generating': 'Gerando arquivos do projeto...',
299
299
  'setup.success.created': 'Projeto criado com sucesso.',
300
- 'setup.success.complete': '✓ Setup concluido',
300
+ 'setup.success.complete': '✓ Setup concluído',
301
301
  'setup.success.location': 'Local',
302
- 'setup.success.nextSteps': 'Proximos passos',
302
+ 'setup.success.nextSteps': 'Próximos passos',
303
303
  'setup.success.stepPubGet': 'flutter pub get',
304
304
  'setup.success.stepRun': 'flutter run',
305
305
  'validate.title': 'Kasy Validate',
306
- 'validate.success': '✓ Matriz de validacao aprovada.',
307
- 'validate.failed': 'Matriz de validacao falhou.',
308
- 'validate.error': 'Uma ou mais combinacoes de validacao falharam.',
306
+ 'validate.success': '✓ Matriz de validação aprovada.',
307
+ 'validate.failed': 'Matriz de validação falhou.',
308
+ 'validate.error': 'Uma ou mais combinações de validação falharam.',
309
309
  'validate.projectNotFound': 'Projeto não encontrado',
310
310
  'validate.ok': 'ok',
311
311
  'validate.fail': 'falhou',
@@ -331,7 +331,7 @@ module.exports = {
331
331
  'new.q.preset.content': '📱 Conteúdo — crash reports + analytics + onboarding + AI chat',
332
332
  'new.q.preset.full': '🚀 Completo — todas as features',
333
333
  'new.q.preset.custom': '⚙️ Personalizar — escolha feature a feature',
334
- 'new.q.preset.none': '○ Nenhum — so o core',
334
+ 'new.q.preset.none': '○ Nenhum — o core',
335
335
 
336
336
  'new.firebase.success.deployStep': '• Deploy do backend (de dentro da pasta do projeto):',
337
337
 
@@ -525,7 +525,7 @@ module.exports = {
525
525
  'add.iosRelease.success': 'Arquivos de release iOS adicionados',
526
526
  'add.iosRelease.already': 'Arquivos de release iOS já existem',
527
527
 
528
- 'new.api.q.baseUrl': 'Qual e a URL base da sua API?',
528
+ 'new.api.q.baseUrl': 'Qual é a URL base da sua API?',
529
529
  'new.api.q.baseUrl.hint': 'https://api.example.com',
530
530
  'new.firebase.banner': '🔥 Novo app Flutter — Firebase',
531
531
  'new.firebase.subtitle': 'Projeto completo: auth, Firestore, notificações, login social e muito mais.',
@@ -541,31 +541,31 @@ module.exports = {
541
541
  'new.firebase.create.successPush': 'Projeto Firebase criado para FCM + Remote Config.',
542
542
  'new.firebase.prereq.1': '1. Firebase CLI instalado (npm i -g firebase-tools) + firebase login',
543
543
  'new.firebase.prereq.2': '2. Projeto Firebase criado em console.firebase.google.com',
544
- 'new.firebase.prereq.3': '3. Plano Blaze ativado (cartao de credito — necessário para Cloud Functions)',
544
+ 'new.firebase.prereq.3': '3. Plano Blaze ativado (cartão de crédito — necessário para Cloud Functions)',
545
545
  'new.firebase.prereq.4': '4. gcloud CLI instalado + gcloud auth login (usado para ativar APIs automaticamente)',
546
- 'new.firebase.prereq.5': '5. Antes do deploy: ative Secret Manager API e Firebase Storage (ver PREREQUISITES.md ou links apos gerar)',
546
+ 'new.firebase.prereq.5': '5. Antes do deploy: ative Secret Manager API e Firebase Storage (ver PREREQUISITES.md ou links após gerar)',
547
547
  'new.firebase.prereq.doc': ' Checklist completo: PREREQUISITES.md',
548
548
 
549
- 'new.firebase.q.appName': 'Qual e o nome do seu app?',
550
- 'new.firebase.q.appName.hint': 'ex: Meu App Incrivel',
549
+ 'new.firebase.q.appName': 'Qual é o nome do seu app?',
550
+ 'new.firebase.q.appName.hint': 'ex: Meu App Incrível',
551
551
  'new.firebase.q.appName.required': 'O nome do app é obrigatório.',
552
552
 
553
- 'new.firebase.q.bundleId': 'Qual e o identificador único (Bundle ID) do seu app?',
553
+ 'new.firebase.q.bundleId': 'Como você quer o identificador único (Bundle ID) do seu app?',
554
554
  'new.firebase.q.bundleId.hint': 'Funciona como um endereço para o seu app, ex: com.minhaempresa.meuapp',
555
555
  'new.firebase.q.bundleId.invalid': 'Formato inválido. Use: com.empresa.app',
556
556
  'new.firebase.q.bundleId.required': 'O Bundle ID é obrigatório.',
557
557
 
558
- 'new.firebase.q.projectId': 'Qual e o ID do seu projeto Firebase?',
558
+ 'new.firebase.q.projectId': 'Qual é o ID do seu projeto Firebase?',
559
559
  'new.firebase.q.projectId.hint': 'Encontre no Firebase Console → seu projeto → Configurações',
560
560
  'new.firebase.q.projectId.required': 'O Firebase Project ID é obrigatório.',
561
561
 
562
562
  'new.firebase.q.modules': 'Quais features opcionais você quer incluir?',
563
- 'new.firebase.q.modules.hint': 'Espaco para selecionar, Enter para confirmar',
563
+ 'new.firebase.q.modules.hint': 'Espaço para selecionar, Enter para confirmar',
564
564
  'new.modules.header.common': '── Comuns (todos os backends) ──',
565
565
  'new.modules.header.features': '── Telas e features ──',
566
566
  'new.modules.header.feedback': '── Feedback (Firebase + Supabase) ──',
567
567
  'new.modules.header.ci': '── CI/CD ──',
568
- 'new.modules.header.monetization': '── Monetizacao ──',
568
+ 'new.modules.header.monetization': '── Monetização ──',
569
569
  'new.firebase.module.revenuecat': '💰 RevenueCat (assinaturas mobile)',
570
570
  'new.firebase.module.stripe': '🌐 Stripe (assinaturas na web)',
571
571
  'new.firebase.module.sentry': '🚨 Crash Reports (Sentry)',
@@ -582,7 +582,7 @@ module.exports = {
582
582
  'new.firebase.q.secrets.configureNow': 'Configurar as secrets do servidor agora? (webhook RevenueCat, Meta Ads)',
583
583
  'new.firebase.q.secrets.configureNow.hint': 'Se não agora, veja o README para os comandos de configuração depois',
584
584
  'new.firebase.q.secrets.later': '• Secrets do servidor não configuradas — serão configuradas durante `kasy deploy`.',
585
- 'new.firebase.q.revenuecat.webhookKey': 'Chave secreta do webhook (um valor aleatorio foi sugerido — pressione Enter para aceitar ou digite o seu)',
585
+ 'new.firebase.q.revenuecat.webhookKey': 'Chave secreta do webhook (um valor aleatório foi sugerido — pressione Enter para aceitar ou digite o seu)',
586
586
  'new.firebase.q.revenuecat.webhookKey.hint': 'Salve esse valor. No painel RevenueCat, cole como: Bearer <esse-valor>',
587
587
  'new.firebase.q.revenuecat.metaToken': 'Meta Access Token (Conversions API, opcional)',
588
588
  'new.firebase.q.revenuecat.metaDataset': 'Meta Dataset ID / Pixel ID (opcional)',
@@ -609,12 +609,12 @@ module.exports = {
609
609
  'new.firebase.q.mixpanel.token': 'Token do Mixpanel (deixe em branco para configurar depois)',
610
610
  'new.firebase.q.facebook.appId': 'App ID do Facebook',
611
611
  'new.firebase.q.facebook.appId.required': 'App ID do Facebook é obrigatório.',
612
- 'new.firebase.q.facebook.appId.invalid': 'App ID do Facebook deve ser numerico (ex: 1234567890).',
612
+ 'new.firebase.q.facebook.appId.invalid': 'App ID do Facebook deve ser numérico (ex: 1234567890).',
613
613
  'new.firebase.q.facebook.token': 'Token do App Facebook',
614
614
  'new.firebase.q.facebook.token.required': 'Token do App Facebook é obrigatório.',
615
615
  'new.firebase.q.revenuecat.android.required': 'Chave Android do RevenueCat é obrigatória.',
616
616
  'new.firebase.q.revenuecat.ios.required': 'Chave iOS do RevenueCat é obrigatória.',
617
- 'new.firebase.q.revenuecat.metaDataset.invalid': 'Pixel ID deve ser numerico (ex: 1234567890).',
617
+ 'new.firebase.q.revenuecat.metaDataset.invalid': 'Pixel ID deve ser numérico (ex: 1234567890).',
618
618
 
619
619
  'new.firebase.confirm.title': 'Resumo da configuração:',
620
620
  'new.firebase.confirm.app': 'App',
@@ -648,7 +648,7 @@ module.exports = {
648
648
  'new.firebase.success.alreadyDone': 'Já configurado pela CLI:',
649
649
  'new.firebase.success.manualNeeded': 'Configure manualmente (se necessário):',
650
650
  'new.firebase.success.apn': '• Chave APN (push iOS): Firebase Console → Config. do Projeto → Cloud Messaging',
651
- 'new.firebase.success.social': '• Ative o login social no Firebase Console → Authentication (Google e Apple já estão no código; so ative la)',
651
+ 'new.firebase.success.social': '• Ative o login social no Firebase Console → Authentication (Google e Apple já estão no código; ative )',
652
652
  'new.firebase.success.googleSignIn': '• Login com Gmail: 1) Ative em Firebase Console → Auth → Sign-in method → Google. 2) O CLI preenche kGoogleWebClientId (google_auth_options.dart) do google-services.json para Android/iOS. Web usa signInWithPopup.',
653
653
  'new.firebase.success.sentry': '• Sentry: Para relatórios de erro em produção. Pegue o DSN em sentry.io. Já no launch.json para dev. Para release: --dart-define=SENTRY_DSN=xxx',
654
654
  'new.firebase.success.mixpanel': '• Mixpanel: Para analytics em produção. Já no launch.json para dev. Para release: --dart-define=MIXPANEL_TOKEN=xxx',
@@ -679,7 +679,7 @@ module.exports = {
679
679
  'new.firebase.interactive.googleAuthNote': '* Ative o Google Sign-In manualmente (Email/Senha e Anônimo já foram ativados): ',
680
680
  'new.firebase.interactive.billingNeeded': 'Plano Blaze ainda não ativo. Ative no link acima e aguarde a detecção automática.',
681
681
  'new.firebase.interactive.billingWaiting': 'Verificando status do Blaze...',
682
- 'new.firebase.interactive.billingTimeout': 'Plano Blaze não confirmado apos o tempo limite. Deploy ignorado — rode manualmente quando estiver pronto.',
682
+ 'new.firebase.interactive.billingTimeout': 'Plano Blaze não confirmado após o tempo limite. Deploy ignorado — rode manualmente quando estiver pronto.',
683
683
  'new.firebase.interactive.authWarn': 'Não foi possível ativar Email/Senha e Anônimo automaticamente. Ative manualmente:',
684
684
  'new.firebase.localhostWarn': 'Não foi possível autorizar localhost para login web. Se for testar login no navegador, adicione "localhost" em Authorized domains:',
685
685
  'new.firebase.existing.apisFailed': 'Não foi possível ativar APIs:',
@@ -705,21 +705,21 @@ module.exports = {
705
705
  'new.supabase.prereq.login': ' Se criar manualmente: tenha URL e anon key do projeto prontos',
706
706
  'new.supabase.loginRequired': '⚠️ Você precisa estar logado para criar projeto. Execute supabase login primeiro.',
707
707
  'new.supabase.loginCommand': ' supabase login',
708
- 'new.supabase.success.done.db': '• Banco: tabelas, politicas RLS',
709
- 'new.supabase.success.done.storage': '• Storage: bucket kasy com politicas',
708
+ 'new.supabase.success.done.db': '• Banco: tabelas, políticas RLS',
709
+ 'new.supabase.success.done.storage': '• Storage: bucket kasy com políticas',
710
710
  'new.supabase.success.done.webhook': '• Edge Function revenuecat-webhook',
711
711
  'new.supabase.success.done.secrets': '• Secrets da Edge Function (se informados)',
712
712
  'new.supabase.success.done.launch': '• launch.json com Sentry, Mixpanel, RevenueCat',
713
713
  'new.supabase.success.auth': '• Auth: Email já vem ativado. Ative Google, Apple e Facebook em: {authUrl}',
714
- 'new.supabase.success.storage': '• Storage: Bucket kasy criado com politicas (já pronto)',
714
+ 'new.supabase.success.storage': '• Storage: Bucket kasy criado com políticas (já pronto)',
715
715
  'new.supabase.success.fcm': '• Push (FCM): Configure no Firebase Console (plano Blaze). App já pronto para Supabase + FCM. URL: {fcmUrl}',
716
716
  'new.supabase.success.deployLater': '• Deploy do backend quando estiver pronto (de dentro da pasta do projeto):',
717
717
 
718
- 'new.api.prereq.1': '1. Sua API REST rodando e acessivel',
718
+ 'new.api.prereq.1': '1. Sua API REST rodando e acessível',
719
719
  'new.api.prereq.2': '2. URL base da API (ex: https://api.yourapp.com)',
720
720
  'new.api.prereq.3': '3. Firebase CLI obrigatório para notificações push (FCM)',
721
721
  'new.api.success.backendUrl': '• Aponte o BACKEND_URL para sua API (launch.json)',
722
- 'new.api.success.fcm': '• Firebase e necessário para notificações push (FCM) — configure a chave APNs no console do Firebase',
722
+ 'new.api.success.fcm': '• Firebase é necessário para notificações push (FCM) — configure a chave APNs no console do Firebase',
723
723
  'new.api.success.auth': '• Implemente os endpoints de auth social (Google, Apple) no seu backend',
724
724
 
725
725
  'new.outdated.hint': 'projetos criados agora não terão as últimas melhorias.',
@@ -729,7 +729,7 @@ module.exports = {
729
729
  'new.success.featuresInstalled': 'Recursos ativados:',
730
730
  'new.success.bundleId': 'Identificador do app (bundle ID)',
731
731
  'new.success.bundleId.hint': 'Identificador único do seu app no Android, iOS e Firebase (push).',
732
- 'new.success.nextSteps': 'Proximos passos:',
732
+ 'new.success.nextSteps': 'Próximos passos:',
733
733
  'new.success.step.cd': 'Entre na pasta do projeto:',
734
734
  'new.success.step.deploy': 'Suba o servidor pro Firebase (banco + funções):',
735
735
  'new.success.step.configure': 'Configure chaves opcionais quando tiver (RevenueCat, Sentry, etc.):',
@@ -835,7 +835,7 @@ module.exports = {
835
835
  'doctor.project.appName': 'Nome do app',
836
836
  'doctor.project.backend': 'Backend',
837
837
  'doctor.project.bundleId': 'Bundle ID',
838
- 'doctor.project.pubGet': 'Dependencias instaladas (pubspec.lock presente)',
838
+ 'doctor.project.pubGet': 'Dependências instaladas (pubspec.lock presente)',
839
839
  'doctor.project.pubGetMissing': 'Execute flutter pub get — pubspec.lock não encontrado',
840
840
  'doctor.project.modules': 'Features ativas',
841
841
  'doctor.project.noModules': 'Nenhuma feature opcional ativa',
@@ -948,7 +948,7 @@ module.exports = {
948
948
  'notifications.writing': 'Atualizando lib/i18n/*.i18n.json...',
949
949
  'notifications.written': 'Atualizado: {langs}',
950
950
  'notifications.slang': 'Executando dart run slang...',
951
- 'notifications.slangDone': 'Traducoes regeneradas',
951
+ 'notifications.slangDone': 'Traduções regeneradas',
952
952
  'notifications.slangFailed': 'dart run slang falhou — execute manualmente no projeto',
953
953
  'notifications.done': 'Textos de notificação local atualizados.',
954
954
  'notifications.summary.demo': 'Home → Features (demo):',
@@ -957,14 +957,14 @@ module.exports = {
957
957
  'add.list.title': 'Features do projeto',
958
958
  'add.error.noModule': 'Informe o nome da feature ou use --list para ver as disponíveis.',
959
959
  'add.error.notKasyProject': 'kit_setup.json não encontrado. Execute este comando dentro de um projeto Kasy.',
960
- 'add.error.unknownModule': 'Feature desconhecida: {module}\nDisponiveis: {list}',
960
+ 'add.error.unknownModule': 'Feature desconhecida: {module}\nDisponíveis: {list}',
961
961
  'add.alreadyActive': 'A feature "{module}" já está ativa neste projeto.',
962
962
  'add.applying': 'Adicionando feature: {module}',
963
963
  'add.applyingPatch': 'Aplicando mudanças da feature...',
964
964
  'add.patchApplied': 'Patch aplicado',
965
965
  'add.patchFailed': 'Patch falhou — verifique a saída acima',
966
966
  'add.pubGet': 'Instalando pacotes do Flutter (flutter pub get)...',
967
- 'add.pubGetDone': 'Dependencias atualizadas',
967
+ 'add.pubGetDone': 'Dependências atualizadas',
968
968
  'add.pubGetFailed': 'Falha ao instalar pacotes do Flutter — execute `flutter pub get` manualmente',
969
969
  'add.buildRunner': 'Gerando código (Riverpod/Freezed)...',
970
970
  'add.buildRunnerDone': 'Geração de código concluída',
@@ -984,7 +984,7 @@ module.exports = {
984
984
  'new.q.ai_chat.configureNow.hint': 'Pode pular e executar "kasy add ai_chat" depois',
985
985
  'add.ai_chat.reconfigure': 'Feature já ativa — reconfigurando apenas as credenciais.',
986
986
  'add.prompt.aiProvider': 'Provedor de IA:',
987
- 'add.prompt.aiSystemPrompt': 'Instrução do agente — system prompt (deixe em branco para nenhuma):\n Exemplo: "Você e um assistente de suporte do app Fitsync. Responda apenas sobre treinos."\n >',
987
+ 'add.prompt.aiSystemPrompt': 'Instrução do agente — system prompt (deixe em branco para nenhuma):\n Exemplo: "Você é um assistente de suporte do app Fitsync. Responda apenas sobre treinos."\n >',
988
988
  'add.prompt.aiApiKey': 'Chave de API (OpenAI ou Gemini) — fica no servidor, nunca no app (deixe em branco para configurar depois):',
989
989
  'add.prompt.aiEndpoint': 'URL do seu endpoint IA (deixe em branco para configurar depois):',
990
990
  'add.ai_chat.settingSecret': 'Salvando chave de API como secret no servidor...',
@@ -993,22 +993,22 @@ module.exports = {
993
993
  'add.ai_chat.skipSecret': 'Chave de API ignorada — configure antes do deploy via CLI do servidor',
994
994
  'add.ai_chat.deploying': 'Fazendo deploy da função IA no servidor...',
995
995
  'add.ai_chat.deployed': 'Função IA deployada com sucesso',
996
- 'add.ai_chat.deployFailed': 'Deploy automático falhou — faca o deploy manualmente (veja instruções abaixo)',
997
- 'add.ai_chat.nextSteps.firebase': '\n Proximos passos:\n 1. O AI_CHAT_ENDPOINT no .vscode/launch.json já foi preenchido.\n 2. Rode o app: kasy run\n',
998
- 'add.ai_chat.nextSteps.firebase.deployFailed': '\n Proximos passos:\n 1. Deploy manual: firebase deploy --only functions:aiChat\n 2. O AI_CHAT_ENDPOINT no .vscode/launch.json já foi preenchido.\n 3. Rode o app: kasy run\n',
999
- 'add.ai_chat.nextSteps.supabase': '\n Proximos passos:\n 1. O AI_CHAT_ENDPOINT no .vscode/launch.json já foi preenchido.\n 2. Rode o app: kasy run\n',
1000
- 'add.ai_chat.nextSteps.supabase.deployFailed': '\n Proximos passos:\n 1. Deploy manual: supabase functions deploy ai-chat --no-verify-jwt\n 2. O AI_CHAT_ENDPOINT no .vscode/launch.json já foi preenchido.\n 3. Rode o app: kasy run\n',
1001
- 'add.ai_chat.nextSteps.api': '\n Proximos passos:\n 1. Crie um endpoint no seu servidor que aceite {message, history} e chame sua IA.\n 2. Atualize AI_CHAT_ENDPOINT no .vscode/launch.json com a URL do seu endpoint.\n 3. Rode o app: kasy run\n',
996
+ 'add.ai_chat.deployFailed': 'Deploy automático falhou — faça o deploy manualmente (veja instruções abaixo)',
997
+ 'add.ai_chat.nextSteps.firebase': '\n Próximos passos:\n 1. O AI_CHAT_ENDPOINT no .vscode/launch.json já foi preenchido.\n 2. Rode o app: kasy run\n',
998
+ 'add.ai_chat.nextSteps.firebase.deployFailed': '\n Próximos passos:\n 1. Deploy manual: firebase deploy --only functions:aiChat\n 2. O AI_CHAT_ENDPOINT no .vscode/launch.json já foi preenchido.\n 3. Rode o app: kasy run\n',
999
+ 'add.ai_chat.nextSteps.supabase': '\n Próximos passos:\n 1. O AI_CHAT_ENDPOINT no .vscode/launch.json já foi preenchido.\n 2. Rode o app: kasy run\n',
1000
+ 'add.ai_chat.nextSteps.supabase.deployFailed': '\n Próximos passos:\n 1. Deploy manual: supabase functions deploy ai-chat --no-verify-jwt\n 2. O AI_CHAT_ENDPOINT no .vscode/launch.json já foi preenchido.\n 3. Rode o app: kasy run\n',
1001
+ 'add.ai_chat.nextSteps.api': '\n Próximos passos:\n 1. Crie um endpoint no seu servidor que aceite {message, history} e chame sua IA.\n 2. Atualize AI_CHAT_ENDPOINT no .vscode/launch.json com a URL do seu endpoint.\n 3. Rode o app: kasy run\n',
1002
1002
  'cli.command.remove.description': 'Remove algo que você não usa mais (ex: kasy remove sentry)',
1003
1003
  'remove.error.noModule': 'Informe o nome da feature. Uso: kasy remove <feature>',
1004
1004
  'remove.error.notKasyProject': 'kit_setup.json não encontrado. Execute este comando dentro de um projeto Kasy.',
1005
- 'remove.error.unknownModule': 'Feature desconhecida: {module}\nDisponiveis: {list}',
1005
+ 'remove.error.unknownModule': 'Feature desconhecida: {module}\nDisponíveis: {list}',
1006
1006
  'remove.error.notActive': 'A feature "{module}" não está ativa neste projeto.',
1007
1007
  'remove.confirm': 'Remover a feature "{module}"? Isso vai deletar arquivos e dependências.',
1008
1008
  'remove.cancelled': 'Cancelado.',
1009
1009
  'remove.removing': 'Removendo feature: {module}',
1010
1010
  'remove.pubGet': 'Instalando pacotes do Flutter (flutter pub get)...',
1011
- 'remove.pubGetDone': 'Dependencias atualizadas',
1011
+ 'remove.pubGetDone': 'Dependências atualizadas',
1012
1012
  'remove.pubGetFailed': 'Falha ao instalar pacotes do Flutter — execute `flutter pub get` manualmente',
1013
1013
  'remove.buildRunner': 'Gerando código (Riverpod/Freezed)...',
1014
1014
  'remove.buildRunnerDone': 'Geração de código concluída',
@@ -1020,20 +1020,20 @@ module.exports = {
1020
1020
  'cli.command.update.targetArg': 'Alvo para atualizar (ex.: revenuecat, sentry, components)',
1021
1021
  'update.error.noProject': 'kit_setup.json não encontrado. Execute dentro de um projeto Kasy.',
1022
1022
  'update.error.unknownModule': 'Feature desconhecida: {module}\nDisponíveis: {list}',
1023
- 'update.error.unknownTarget': 'Alvo de atualização desconhecido: {module}\nDisponiveis: {list}',
1023
+ 'update.error.unknownTarget': 'Alvo de atualização desconhecido: {module}\nDisponíveis: {list}',
1024
1024
  'update.error.notActive': 'A feature "{module}" não está ativa neste projeto.',
1025
1025
  'update.alreadyUpToDate': 'Projeto já está atualizado (v{version}).',
1026
1026
  'update.status': 'Projeto: v{from} → CLI: v{to}',
1027
1027
  'update.noVersion': 'Projeto foi gerado sem rastreamento de versão. Todas as features podem ser atualizadas.',
1028
1028
  'update.changesTitle': 'Atualizações disponíveis:',
1029
- 'update.reapplyTitle': 'Sem mudancas novas — pode reaplicar se precisar:',
1029
+ 'update.reapplyTitle': 'Sem mudanças novas — pode reaplicar se precisar:',
1030
1030
  'update.howToUpdate': 'Para atualizar uma feature:',
1031
1031
  'update.howToUpdateComponents': 'Para atualizar componentes base:',
1032
- 'update.warn.commit': 'Isso vai sobrescrever os arquivos da feature "{module}". Faca commit de tudo antes de continuar.',
1033
- 'update.warn.commitComponents': 'Isso vai sobrescrever arquivos dos componentes base. Faca commit de tudo antes de continuar.',
1032
+ 'update.warn.commit': 'Isso vai sobrescrever os arquivos da feature "{module}". Faça commit de tudo antes de continuar.',
1033
+ 'update.warn.commitComponents': 'Isso vai sobrescrever arquivos dos componentes base. Faça commit de tudo antes de continuar.',
1034
1034
  'update.confirm': 'Sobrescrever arquivos da feature "{module}" com a versão mais recente?',
1035
1035
  'update.confirmComponents': 'Sobrescrever arquivos dos componentes base com a versão mais recente?',
1036
- 'update.confirmCore': 'Sobrescrever arquivos do core (animacoes, widgets, tema, ferramentas de dev) com a versão mais recente?',
1036
+ 'update.confirmCore': 'Sobrescrever arquivos do core (animações, widgets, tema, ferramentas de dev) com a versão mais recente?',
1037
1037
  'update.cancelled': 'Cancelado.',
1038
1038
  'update.applying': 'Aplicando atualização da feature: {module}',
1039
1039
  'update.applyingComponents': 'Aplicando atualização dos componentes base...',
@@ -1046,7 +1046,7 @@ module.exports = {
1046
1046
  'update.noPatch': 'Feature "{module}" não tem arquivos para atualizar (feature so de configuração).',
1047
1047
  'update.noComponentFiles': 'Nenhum arquivo de componente base foi encontrado para atualizar.',
1048
1048
  'update.pubGet': 'Instalando pacotes do Flutter (flutter pub get)...',
1049
- 'update.pubGetDone': 'Dependencias atualizadas',
1049
+ 'update.pubGetDone': 'Dependências atualizadas',
1050
1050
  'update.pubGetFailed': 'Falha ao instalar pacotes do Flutter — execute `flutter pub get` manualmente',
1051
1051
  'update.buildRunner': 'Gerando código (Riverpod/Freezed)...',
1052
1052
  'update.buildRunnerDone': 'Geração de código concluída',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kasy-cli",
3
- "version": "1.29.0",
3
+ "version": "1.31.0",
4
4
  "description": "CLI for scaffolding production-ready Flutter SaaS apps with Firebase, Supabase, or API REST backends.",
5
5
  "bin": {
6
6
  "kasy": "./bin/kasy.js"
@@ -15,6 +15,23 @@ import 'package:kasy_kit/core/widgets/responsive_layout.dart';
15
15
  import 'package:kasy_kit/features/settings/ui/widgets/kasy_user_avatar.dart';
16
16
  import 'package:kasy_kit/i18n/translations.g.dart';
17
17
 
18
+ /// The bottom-bar tab the user last opened, held at top level so it outlives the
19
+ /// [BottomMenu] remount that happens whenever the responsive layout flips
20
+ /// small↔large. Toggling the web device preview does exactly that: the app
21
+ /// renders inside a phone-width frame when on and at full desktop width when
22
+ /// off, so each toggle rebuilds [bart.BartScaffold] from scratch (its index
23
+ /// notifier starts at 0). Persisting the tab here lets [BottomMenu] restore it
24
+ /// instead of snapping back to the first tab. Null until the user opens a tab.
25
+ final ValueNotifier<String?> activeTabRouteNotifier = ValueNotifier<String?>(
26
+ null,
27
+ );
28
+
29
+ /// Records the active tab so it survives the next remount. Wired to
30
+ /// [bart.BartScaffold.onRouteChanged].
31
+ void _rememberActiveTab(bart.BartMenuRoute route) {
32
+ activeTabRouteNotifier.value = route.path;
33
+ }
34
+
18
35
  /// Bottom navigation host powered by Bart (https://pub.dev/packages/bart).
19
36
  ///
20
37
  /// [ResponsiveLayout] swaps between three [bart.BartScaffold]s (small / medium /
@@ -89,6 +106,7 @@ class BottomMenu extends StatelessWidget {
89
106
  showBottomBarOnStart: showBottomBarOnStart,
90
107
  scaffoldOptions: scaffoldOptions,
91
108
  sideBarOptions: connectedSidebar(),
109
+ onRouteChanged: _rememberActiveTab,
92
110
  ),
93
111
  );
94
112
 
@@ -104,6 +122,7 @@ class BottomMenu extends StatelessWidget {
104
122
  initialRoute: resolvedInitialRoute,
105
123
  showBottomBarOnStart: showBottomBarOnStart,
106
124
  scaffoldOptions: scaffoldOptions,
125
+ onRouteChanged: _rememberActiveTab,
107
126
  ),
108
127
  medium: connectedScaffold(),
109
128
  large: connectedScaffold(),
@@ -115,17 +134,44 @@ class BottomMenu extends StatelessWidget {
115
134
  if (route != null) {
116
135
  return route;
117
136
  }
137
+ // Restore the last tab across a remount. This is the reliable source: the
138
+ // browser URL is contended by both GoRouter and Bart, but this notifier is
139
+ // owned solely by the bottom bar and lives above the rebuilt subtree.
140
+ //
141
+ // The value is returned as a BARE tab name (e.g. "settings", not
142
+ // "/settings"). Bart's NestedNavigator matches routes by their exact path,
143
+ // which has no leading slash; passing a "/"-prefixed route makes Flutter's
144
+ // Navigator split it into segments that never match, so it falls back to the
145
+ // first tab (home). See bart's nested_navigator.dart onGenerateRoute.
146
+ final String? lastTab = activeTabRouteNotifier.value;
147
+ if (lastTab != null && _isKnownTab(lastTab)) {
148
+ return _bareTab(lastTab);
149
+ }
118
150
  if (!kIsWeb) {
119
151
  return null;
120
152
  }
121
153
  final path = _initialWebPath(Uri.base);
122
154
  final segments = Uri.parse(path).pathSegments;
123
- if (segments.length < 2) {
155
+ if (segments.isEmpty) {
124
156
  return null;
125
157
  }
158
+ // A single segment is a bottom-bar tab (home/notifications/settings/…).
159
+ // Bart keeps the browser URL in sync via history.pushState, so honoring it
160
+ // here also restores the tab on a hard reload (F5). Only accept it when it
161
+ // maps to a real tab; anything else falls back to the default tab.
162
+ if (segments.length == 1) {
163
+ return _isKnownTab(segments.first) ? _bareTab(segments.first) : null;
164
+ }
126
165
  return '/${segments.join('/')}';
127
166
  }
128
167
 
168
+ String _bareTab(String path) => path.replaceAll('/', '');
169
+
170
+ bool _isKnownTab(String path) {
171
+ final String tab = _bareTab(path);
172
+ return subRoutes().any((r) => _bareTab(r.path) == tab);
173
+ }
174
+
129
175
  String _initialWebPath(Uri uri) {
130
176
  final path = uri.path;
131
177
  if (path != '/' && path.isNotEmpty) {
@@ -104,14 +104,6 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
104
104
  late final ValueNotifier<AppLocale> _localeNotifier;
105
105
  final _screenshotKey = GlobalKey();
106
106
 
107
- // Keeps the wrapped app's Element alive across enable/disable. Toggling the
108
- // preview moves MaterialApp.router between tree positions (in/out of the
109
- // device frame); without a stable GlobalKey the Router would be rebuilt from
110
- // scratch and navigation would snap back to the initial route ('/'). The
111
- // GlobalKey makes Flutter reparent the same Element, preserving the current
112
- // screen.
113
- final _appKey = GlobalKey();
114
-
115
107
  static const _textScaleSteps = [1.0, 1.3, 1.5];
116
108
 
117
109
  List<DeviceInfo> get _devices => switch (_platform) {
@@ -211,11 +203,11 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
211
203
  Future<void> _bootstrap() async {
212
204
  final prefs = await SharedPreferences.getInstance();
213
205
 
214
- // Default OFF: the web app renders at its real desktop proportions instead
215
- // of inside a simulated device frame (which distorts scale/proportion).
216
- // Devs who want to preview the mobile app in a device frame toggle it on
217
- // (shortcut), and that choice persists for subsequent launches.
218
- final savedEnabled = prefs.getBool(webDevicePreviewEnabledPrefKey) ?? false;
206
+ // Default ON: previewing the mobile app inside a device frame is the
207
+ // expected first view for this mobile-first template. Devs who prefer the
208
+ // real desktop proportions toggle it off (shortcut), and that choice
209
+ // persists for subsequent launches.
210
+ final savedEnabled = prefs.getBool(webDevicePreviewEnabledPrefKey) ?? true;
219
211
  final savedPlatform = prefs.getInt(_platformPrefKey);
220
212
  final savedIosIndex = prefs.getInt(_iosIndexPrefKey);
221
213
  final savedAndroidIndex = prefs.getInt(_androidIndexPrefKey);
@@ -418,7 +410,7 @@ class _WebDevicePreviewState extends State<WebDevicePreview>
418
410
  deviceNotifier: _deviceNotifier,
419
411
  frameVisibleNotifier: _frameVisibleNotifier,
420
412
  landscapeNotifier: _landscapeNotifier,
421
- child: KeyedSubtree(key: _appKey, child: widget.child),
413
+ child: widget.child,
422
414
  ),
423
415
  ),
424
416
  ),