diff --git a/resources/js/i18n/messages/de/blog.ts b/resources/js/i18n/messages/de/blog.ts
index f0416d22..31e03b30 100644
--- a/resources/js/i18n/messages/de/blog.ts
+++ b/resources/js/i18n/messages/de/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "Noch keine Beiträge.",
"guides": "Anleitungen",
+ "news": "Neuigkeiten",
"releases": "Versionshinweise"
},
"latest": "Neuester Versionsbeitrag:
{title}.",
diff --git a/resources/js/i18n/messages/de/pricing.ts b/resources/js/i18n/messages/de/pricing.ts
index 8ce28464..b66689ac 100644
--- a/resources/js/i18n/messages/de/pricing.ts
+++ b/resources/js/i18n/messages/de/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "Verlängert sich monatlich, bis du kündigst.",
"yearly": "Verlängert sich jährlich, bis du kündigst. {percent} % günstiger als zwölf Monatszahlungen.",
- "yearlyByTier": "Verlängert sich jährlich, bis du kündigst. Starter ist {starterPercent} % günstiger als zwölf Monatszahlungen, Team {teamPercent} % günstiger.",
+ "yearlyByTier": "Verlängert sich jährlich, bis du kündigst. Pro ist {starterPercent} % günstiger als zwölf Monatszahlungen, Team {teamPercent} % günstiger.",
"lifetime": "Einmalige Zahlung, ohne Ablaufdatum."
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "Ergänzt die Mac-App um Funktionen wie {examples}.",
"activation": {
"one": "Eine Person, {count} Mac.",
"other": "Eine Person, bis zu {count} Macs."
},
"includesTitle": "Alles aus Kostenlos, plus",
- "cta": "Starter kaufen"
+ "cta": "Pro kaufen"
},
"team": {
"name": "Team",
"description": "Teile Verbindungen und gespeicherte Abfragen mit deinem Team.",
"activation": "Ein aktivierter Mac pro Arbeitsplatz.",
- "includesTitle": "Alles aus Starter, plus",
- "cta": "Team kaufen"
+ "includesTitle": "Alles aus Pro, plus",
+ "cta": "Team kaufen",
+ "subscriptionOnly": "Team gibt es monatlich oder jährlich."
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "Für Unternehmen, die mehr Kontrolle über die Nutzung von TablePro brauchen.",
+ "price": "Kontakt",
+ "activation": "Preis je Unternehmen.",
+ "includesTitle": "Sag uns, was du brauchst, zum Beispiel",
+ "includes": [
+ "Anmeldung mit den Konten deines Unternehmens",
+ "Verbindliche Richtlinien für Team-Verbindungen",
+ "Audit von Schreibzugriffen auf Produktion",
+ "Verteilung per MDM",
+ "Rechnungen und Bestellscheine"
+ ],
+ "cta": "Kontakt aufnehmen"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "pro Arbeitsplatz und Monat",
- "yearly": "pro Arbeitsplatz und Jahr",
- "lifetime": "pro Arbeitsplatz, einmalig"
+ "yearly": "pro Arbeitsplatz und Monat, jährlich abgerechnet"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} Arbeitsplatz: {total} pro Jahr",
"other": "{count} Arbeitsplätze: {total} pro Jahr"
- },
- "lifetime": {
- "one": "{count} Arbeitsplatz: {total}, einmalige Zahlung",
- "other": "{count} Arbeitsplätze: {total}, einmalige Zahlung"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "Einer pro Arbeitsplatz",
"everythingElse": "Alles andere in der App",
"everythingElseDetail": "Alle unterstützten Datenbanksysteme, SQL-Editor, KI-Assistent, MCP-Server und Safe Mode",
- "iphoneNote": "Die iPhone- und iPad-App hat keine Bezahlfunktionen. iCloud Sync ist dort kostenlos; für die Synchronisierung mit einem Mac benötigt der Mac Starter oder Team."
+ "iphoneNote": "Die iPhone- und iPad-App hat keine Bezahlfunktionen. iCloud Sync ist dort kostenlos; für die Synchronisierung mit einem Mac benötigt der Mac Pro oder Team."
},
"regional": {
"note": "Preise für dein Land ({country}): {percent} % Rabatt, wird beim Kauf abgezogen.",
diff --git a/resources/js/i18n/messages/en/blog.ts b/resources/js/i18n/messages/en/blog.ts
index 658b93e1..25e5551a 100644
--- a/resources/js/i18n/messages/en/blog.ts
+++ b/resources/js/i18n/messages/en/blog.ts
@@ -18,6 +18,7 @@ export default {
index: {
empty: 'No posts yet.',
guides: 'Guides',
+ news: 'News',
releases: 'Release notes',
},
latest: 'Latest release post:
{title}.',
diff --git a/resources/js/i18n/messages/en/pricing.ts b/resources/js/i18n/messages/en/pricing.ts
index e671de59..c14388f2 100644
--- a/resources/js/i18n/messages/en/pricing.ts
+++ b/resources/js/i18n/messages/en/pricing.ts
@@ -34,7 +34,7 @@ export default {
captions: {
monthly: 'Renews every month until you cancel.',
yearly: 'Renews every year until you cancel. {percent}% less than twelve monthly payments.',
- yearlyByTier: 'Renews every year until you cancel. Starter is {starterPercent}% less than twelve monthly payments, Team {teamPercent}% less.',
+ yearlyByTier: 'Renews every year until you cancel. Pro is {starterPercent}% less than twelve monthly payments, Team {teamPercent}% less.',
lifetime: 'Paid once, no expiry date.',
},
tiers: {
@@ -52,22 +52,38 @@ export default {
],
},
starter: {
- name: 'Starter',
- // `{examples}`: the highlighted Starter features from paid-features.json, joined for the language.
+ name: 'Pro',
+ // `{examples}`: the highlighted Pro features from paid-features.json, joined for the language.
description: 'Adds features such as {examples} to the Mac app.',
activation: {
one: 'One person, {count} Mac.',
other: 'One person, up to {count} Macs.',
},
includesTitle: 'Everything in Free, plus',
- cta: 'Get Starter',
+ cta: 'Get Pro',
},
team: {
name: 'Team',
description: 'Share connections and saved queries with your team.',
activation: 'One activated Mac per seat.',
- includesTitle: 'Everything in Starter, plus',
+ includesTitle: 'Everything in Pro, plus',
cta: 'Get Team',
+ subscriptionOnly: 'Team is sold monthly or yearly.',
+ },
+ enterprise: {
+ name: 'Enterprise',
+ description: 'For a company that needs more control over how TablePro is used.',
+ price: 'Contact us',
+ activation: 'Priced per company.',
+ includesTitle: 'Tell us what you need, such as',
+ includes: [
+ 'Sign-in with your company accounts',
+ 'Policies enforced on team connections',
+ 'An audit of writes to production',
+ 'Deployment through MDM',
+ 'Invoices and purchase orders',
+ ],
+ cta: 'Contact us',
},
},
/** The unit beside a price. */
@@ -79,8 +95,7 @@ export default {
},
team: {
monthly: 'per seat, per month',
- yearly: 'per seat, per year',
- lifetime: 'per seat, paid once',
+ yearly: 'per seat, per month, billed yearly',
},
},
seats: {
@@ -97,7 +112,6 @@ export default {
total: {
monthly: { one: '{count} seat: {total} per month', other: '{count} seats: {total} per month' },
yearly: { one: '{count} seat: {total} per year', other: '{count} seats: {total} per year' },
- lifetime: { one: '{count} seat: {total}, paid once', other: '{count} seats: {total}, paid once' },
},
},
prioritySupport: {
@@ -133,7 +147,7 @@ export default {
macsTeam: 'One per seat',
everythingElse: 'Everything else in the app',
everythingElseDetail: 'Every supported engine, the SQL editor, the AI assistant, the MCP server and Safe Mode',
- iphoneNote: 'The iPhone and iPad app has no paid features. iCloud Sync is free there; to sync with a Mac, the Mac needs Starter or Team.',
+ iphoneNote: 'The iPhone and iPad app has no paid features. iCloud Sync is free there; to sync with a Mac, the Mac needs Pro or Team.',
},
/**
* The regional discount checkout applies in some countries, shown with the
diff --git a/resources/js/i18n/messages/es/blog.ts b/resources/js/i18n/messages/es/blog.ts
index 33e6e9d3..85546f9e 100644
--- a/resources/js/i18n/messages/es/blog.ts
+++ b/resources/js/i18n/messages/es/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "Todavía no hay publicaciones.",
"guides": "Guías",
+ "news": "Novedades",
"releases": "Notas de versión"
},
"latest": "Último artículo de versión:
{title}.",
diff --git a/resources/js/i18n/messages/es/pricing.ts b/resources/js/i18n/messages/es/pricing.ts
index 63faaecc..11d7e463 100644
--- a/resources/js/i18n/messages/es/pricing.ts
+++ b/resources/js/i18n/messages/es/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "Se renueva cada mes hasta que canceles.",
"yearly": "Se renueva cada año hasta que canceles. Un {percent}% menos que doce pagos mensuales.",
- "yearlyByTier": "Se renueva cada año hasta que canceles. Starter cuesta un {starterPercent}% menos que doce pagos mensuales; Team, un {teamPercent}% menos.",
+ "yearlyByTier": "Se renueva cada año hasta que canceles. Pro cuesta un {starterPercent}% menos que doce pagos mensuales; Team, un {teamPercent}% menos.",
"lifetime": "Un solo pago, sin fecha de caducidad."
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "Añade a la app para Mac funciones como {examples}.",
"activation": {
"one": "Una persona, {count} Mac.",
"other": "Una persona, hasta {count} Mac."
},
"includesTitle": "Todo lo del plan Gratis, más",
- "cta": "Comprar Starter"
+ "cta": "Comprar Pro"
},
"team": {
"name": "Team",
"description": "Comparte conexiones y consultas guardadas con tu equipo.",
"activation": "Un Mac activado por puesto.",
- "includesTitle": "Todo lo de Starter, más",
- "cta": "Comprar Team"
+ "includesTitle": "Todo lo de Pro, más",
+ "cta": "Comprar Team",
+ "subscriptionOnly": "Team se vende con pago mensual o anual."
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "Para empresas que necesitan más control sobre cómo se usa TablePro.",
+ "price": "Contáctanos",
+ "activation": "Precio por empresa.",
+ "includesTitle": "Cuéntanos qué necesitas, por ejemplo",
+ "includes": [
+ "Inicio de sesión con las cuentas de tu empresa",
+ "Políticas obligatorias en las conexiones del equipo",
+ "Auditoría de escrituras en producción",
+ "Despliegue mediante MDM",
+ "Facturas y órdenes de compra"
+ ],
+ "cta": "Contáctanos"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "por puesto, al mes",
- "yearly": "por puesto, al año",
- "lifetime": "por puesto, pago único"
+ "yearly": "por puesto, al mes, facturado anualmente"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} puesto: {total} al año",
"other": "{count} puestos: {total} al año"
- },
- "lifetime": {
- "one": "{count} puesto: {total}, pago único",
- "other": "{count} puestos: {total}, pago único"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "Uno por puesto",
"everythingElse": "Todo lo demás en la app",
"everythingElseDetail": "Todos los motores compatibles, el editor SQL, el asistente de IA, el servidor MCP y Safe Mode",
- "iphoneNote": "La app para iPhone y iPad no tiene funciones de pago. iCloud Sync es gratis en ella; para sincronizar con un Mac, este necesita Starter o Team."
+ "iphoneNote": "La app para iPhone y iPad no tiene funciones de pago. iCloud Sync es gratis en ella; para sincronizar con un Mac, este necesita Pro o Team."
},
"regional": {
"note": "Precios para tu país ({country}): {percent}% de descuento, aplicado al finalizar la compra.",
diff --git a/resources/js/i18n/messages/fr/blog.ts b/resources/js/i18n/messages/fr/blog.ts
index 0f95960d..39b57ddb 100644
--- a/resources/js/i18n/messages/fr/blog.ts
+++ b/resources/js/i18n/messages/fr/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "Aucun article pour le moment.",
"guides": "Guides",
+ "news": "Actualités",
"releases": "Notes de version"
},
"latest": "Dernier article de version :
{title}.",
diff --git a/resources/js/i18n/messages/fr/pricing.ts b/resources/js/i18n/messages/fr/pricing.ts
index 23b7cb09..6e32d4f5 100644
--- a/resources/js/i18n/messages/fr/pricing.ts
+++ b/resources/js/i18n/messages/fr/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "Renouvellement chaque mois jusqu’à résiliation.",
"yearly": "Renouvellement chaque année jusqu’à résiliation. {percent} % de moins que douze paiements mensuels.",
- "yearlyByTier": "Renouvellement chaque année jusqu’à résiliation. Starter coûte {starterPercent} % de moins que douze paiements mensuels, Team {teamPercent} % de moins.",
+ "yearlyByTier": "Renouvellement chaque année jusqu’à résiliation. Pro coûte {starterPercent} % de moins que douze paiements mensuels, Team {teamPercent} % de moins.",
"lifetime": "Un seul paiement, sans date d’expiration."
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "Ajoute à l’application Mac des fonctionnalités comme {examples}.",
"activation": {
"one": "Une personne, {count} Mac.",
"other": "Une personne, jusqu’à {count} Mac."
},
"includesTitle": "Tout ce qui est gratuit, plus",
- "cta": "Acheter Starter"
+ "cta": "Acheter Pro"
},
"team": {
"name": "Team",
"description": "Partagez connexions et requêtes enregistrées avec votre équipe.",
"activation": "Un Mac activé par poste.",
- "includesTitle": "Tout ce que contient Starter, plus",
- "cta": "Acheter Team"
+ "includesTitle": "Tout ce que contient Pro, plus",
+ "cta": "Acheter Team",
+ "subscriptionOnly": "Team est vendu au mois ou à l’année."
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "Pour une entreprise qui veut plus de contrôle sur l’usage de TablePro.",
+ "price": "Nous contacter",
+ "activation": "Tarif par entreprise.",
+ "includesTitle": "Dites-nous ce qu’il vous faut, par exemple",
+ "includes": [
+ "Connexion avec les comptes de votre entreprise",
+ "Règles imposées sur les connexions de l’équipe",
+ "Audit des écritures en production",
+ "Déploiement par MDM",
+ "Factures et bons de commande"
+ ],
+ "cta": "Nous contacter"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "par poste et par mois",
- "yearly": "par poste et par an",
- "lifetime": "par poste, paiement unique"
+ "yearly": "par poste et par mois, facturé à l’année"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} poste : {total} par an",
"other": "{count} postes : {total} par an"
- },
- "lifetime": {
- "one": "{count} poste : {total}, paiement unique",
- "other": "{count} postes : {total}, paiement unique"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "Un par poste",
"everythingElse": "Tout le reste de l’application",
"everythingElseDetail": "Tous les moteurs pris en charge, l’éditeur SQL, l’assistant IA, le serveur MCP et Safe Mode",
- "iphoneNote": "L’application iPhone et iPad n’a aucune fonctionnalité payante. iCloud Sync y est gratuit ; pour synchroniser avec un Mac, celui-ci doit disposer de Starter ou Team."
+ "iphoneNote": "L’application iPhone et iPad n’a aucune fonctionnalité payante. iCloud Sync y est gratuit ; pour synchroniser avec un Mac, celui-ci doit disposer de Pro ou Team."
},
"regional": {
"note": "Prix pour votre pays ({country}) : {percent} % de réduction, appliquée lors du règlement.",
diff --git a/resources/js/i18n/messages/id/blog.ts b/resources/js/i18n/messages/id/blog.ts
index 67337d00..ff3a1fc1 100644
--- a/resources/js/i18n/messages/id/blog.ts
+++ b/resources/js/i18n/messages/id/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "Belum ada artikel.",
"guides": "Panduan",
+ "news": "Berita",
"releases": "Catatan rilis"
},
"latest": "Artikel rilis terbaru:
{title}.",
diff --git a/resources/js/i18n/messages/id/pricing.ts b/resources/js/i18n/messages/id/pricing.ts
index 5bf5a5b0..4b58668c 100644
--- a/resources/js/i18n/messages/id/pricing.ts
+++ b/resources/js/i18n/messages/id/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "Diperpanjang setiap bulan hingga Anda membatalkannya.",
"yearly": "Diperpanjang setiap tahun hingga Anda membatalkannya. {percent}% lebih murah daripada dua belas pembayaran bulanan.",
- "yearlyByTier": "Diperpanjang setiap tahun hingga Anda membatalkannya. Starter {starterPercent}% lebih murah daripada dua belas pembayaran bulanan, Team {teamPercent}% lebih murah.",
+ "yearlyByTier": "Diperpanjang setiap tahun hingga Anda membatalkannya. Pro {starterPercent}% lebih murah daripada dua belas pembayaran bulanan, Team {teamPercent}% lebih murah.",
"lifetime": "Dibayar sekali, tanpa tanggal kedaluwarsa."
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "Menambahkan fitur seperti {examples} ke aplikasi Mac.",
"activation": {
"one": "Satu orang, {count} Mac.",
"other": "Satu orang, hingga {count} Mac."
},
"includesTitle": "Semua fitur Gratis, ditambah",
- "cta": "Beli Starter"
+ "cta": "Beli Pro"
},
"team": {
"name": "Team",
"description": "Bagikan koneksi dan kueri tersimpan dengan tim Anda.",
"activation": "Satu Mac yang diaktifkan per seat.",
- "includesTitle": "Semua fitur Starter, ditambah",
- "cta": "Beli Team"
+ "includesTitle": "Semua fitur Pro, ditambah",
+ "cta": "Beli Team",
+ "subscriptionOnly": "Team dijual bulanan atau tahunan."
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "Untuk perusahaan yang butuh kontrol lebih atas penggunaan TablePro.",
+ "price": "Hubungi kami",
+ "activation": "Harga per perusahaan.",
+ "includesTitle": "Beri tahu kebutuhan Anda, misalnya",
+ "includes": [
+ "Masuk dengan akun perusahaan",
+ "Kebijakan wajib pada koneksi tim",
+ "Audit penulisan ke production",
+ "Penyebaran melalui MDM",
+ "Faktur dan purchase order"
+ ],
+ "cta": "Hubungi kami"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "per seat, per bulan",
- "yearly": "per seat, per tahun",
- "lifetime": "per seat, sekali bayar"
+ "yearly": "per seat, per bulan, ditagih tahunan"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} seat: {total} per tahun",
"other": "{count} seat: {total} per tahun"
- },
- "lifetime": {
- "one": "{count} seat: {total}, sekali bayar",
- "other": "{count} seat: {total}, sekali bayar"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "Satu per seat",
"everythingElse": "Semua fitur lain dalam aplikasi",
"everythingElseDetail": "Semua mesin database yang didukung, editor SQL, asisten AI, server MCP, dan Safe Mode",
- "iphoneNote": "Aplikasi iPhone dan iPad tidak memiliki fitur berbayar. iCloud Sync gratis di sana; untuk sinkronisasi dengan Mac, Mac memerlukan Starter atau Team."
+ "iphoneNote": "Aplikasi iPhone dan iPad tidak memiliki fitur berbayar. iCloud Sync gratis di sana; untuk sinkronisasi dengan Mac, Mac memerlukan Pro atau Team."
},
"regional": {
"note": "Harga untuk negara Anda ({country}): diskon {percent}%, diterapkan saat checkout.",
diff --git a/resources/js/i18n/messages/it/blog.ts b/resources/js/i18n/messages/it/blog.ts
index f1c0f0ee..4cb61a91 100644
--- a/resources/js/i18n/messages/it/blog.ts
+++ b/resources/js/i18n/messages/it/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "Ancora nessun articolo.",
"guides": "Guide",
+ "news": "Novità",
"releases": "Note di rilascio"
},
"latest": "Ultimo post di rilascio:
{title}.",
diff --git a/resources/js/i18n/messages/it/pricing.ts b/resources/js/i18n/messages/it/pricing.ts
index 4e06e889..0031a5ca 100644
--- a/resources/js/i18n/messages/it/pricing.ts
+++ b/resources/js/i18n/messages/it/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "Si rinnova ogni mese fino alla disdetta.",
"yearly": "Si rinnova ogni anno fino alla disdetta. {percent}% in meno rispetto a dodici pagamenti mensili.",
- "yearlyByTier": "Si rinnova ogni anno fino alla disdetta. Starter costa il {starterPercent}% in meno rispetto a dodici pagamenti mensili, Team il {teamPercent}% in meno.",
+ "yearlyByTier": "Si rinnova ogni anno fino alla disdetta. Pro costa il {starterPercent}% in meno rispetto a dodici pagamenti mensili, Team il {teamPercent}% in meno.",
"lifetime": "Un solo pagamento, senza scadenza."
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "Aggiunge all’app per Mac funzionalità come {examples}.",
"activation": {
"one": "Una persona, {count} Mac.",
"other": "Una persona, fino a {count} Mac."
},
"includesTitle": "Tutto il piano Gratuito, più",
- "cta": "Acquista Starter"
+ "cta": "Acquista Pro"
},
"team": {
"name": "Team",
"description": "Condividi connessioni e query salvate con il team.",
"activation": "Un Mac attivato per posto.",
- "includesTitle": "Tutto il piano Starter, più",
- "cta": "Acquista Team"
+ "includesTitle": "Tutto il piano Pro, più",
+ "cta": "Acquista Team",
+ "subscriptionOnly": "Team si vende con pagamento mensile o annuale."
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "Per le aziende che vogliono più controllo sull’uso di TablePro.",
+ "price": "Contattaci",
+ "activation": "Prezzo per azienda.",
+ "includesTitle": "Dicci di cosa hai bisogno, per esempio",
+ "includes": [
+ "Accesso con gli account aziendali",
+ "Criteri imposti sulle connessioni del team",
+ "Audit delle scritture in produzione",
+ "Distribuzione tramite MDM",
+ "Fatture e ordini di acquisto"
+ ],
+ "cta": "Contattaci"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "per posto, al mese",
- "yearly": "per posto, all’anno",
- "lifetime": "per posto, un solo pagamento"
+ "yearly": "per posto, al mese, fatturato annualmente"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} posto: {total} all’anno",
"other": "{count} posti: {total} all’anno"
- },
- "lifetime": {
- "one": "{count} posto: {total}, un solo pagamento",
- "other": "{count} posti: {total}, un solo pagamento"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "Uno per posto",
"everythingElse": "Tutto il resto dell’app",
"everythingElseDetail": "Ogni motore supportato, l’editor SQL, l’assistente IA, il server MCP e Safe Mode",
- "iphoneNote": "L’app per iPhone e iPad non ha funzionalità a pagamento. iCloud Sync è gratuito su questi dispositivi; per sincronizzare con un Mac, il Mac deve avere Starter o Team."
+ "iphoneNote": "L’app per iPhone e iPad non ha funzionalità a pagamento. iCloud Sync è gratuito su questi dispositivi; per sincronizzare con un Mac, il Mac deve avere Pro o Team."
},
"regional": {
"note": "Prezzi per il tuo paese ({country}): {percent}% di sconto, applicato al checkout.",
diff --git a/resources/js/i18n/messages/ja/blog.ts b/resources/js/i18n/messages/ja/blog.ts
index 9746489d..803a92d9 100644
--- a/resources/js/i18n/messages/ja/blog.ts
+++ b/resources/js/i18n/messages/ja/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "記事はまだありません。",
"guides": "ガイド",
+ "news": "お知らせ",
"releases": "リリースノート"
},
"latest": "最新のリリース記事:
{title}。",
diff --git a/resources/js/i18n/messages/ja/pricing.ts b/resources/js/i18n/messages/ja/pricing.ts
index ab9b0b32..9dafbf4d 100644
--- a/resources/js/i18n/messages/ja/pricing.ts
+++ b/resources/js/i18n/messages/ja/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "解約するまで毎月更新されます。",
"yearly": "解約するまで毎年更新されます。月払い 12 回分より {percent}% 安くなります。",
- "yearlyByTier": "解約するまで毎年更新されます。月払い 12 回分と比べ、Starter は {starterPercent}%、Team は {teamPercent}% 安くなります。",
+ "yearlyByTier": "解約するまで毎年更新されます。月払い 12 回分と比べ、Pro は {starterPercent}%、Team は {teamPercent}% 安くなります。",
"lifetime": "一度のお支払いで、有効期限はありません。"
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "Mac アプリに {examples} などの機能を追加します。",
"activation": {
"one": "1 人、Mac {count} 台。",
"other": "1 人、Mac 最大 {count} 台。"
},
"includesTitle": "無料プランの全機能に加えて",
- "cta": "Starter を購入"
+ "cta": "Pro を購入"
},
"team": {
"name": "Team",
"description": "接続と保存済みクエリをチームで共有。",
"activation": "1 シートにつき、認証済みの Mac 1 台。",
- "includesTitle": "Starter の全機能に加えて",
- "cta": "Team を購入"
+ "includesTitle": "Pro の全機能に加えて",
+ "cta": "Team を購入",
+ "subscriptionOnly": "Team は月額または年額でのご提供です。"
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "TablePro の利用を組織として管理したい企業向けです。",
+ "price": "お問い合わせ",
+ "activation": "企業ごとにお見積もりします。",
+ "includesTitle": "必要なものをお知らせください。例えば",
+ "includes": [
+ "会社アカウントでのサインイン",
+ "チームの接続に適用するポリシー",
+ "本番環境への書き込みの監査",
+ "MDM による配布",
+ "請求書と発注書"
+ ],
+ "cta": "お問い合わせ"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "1 シートあたりの月額",
- "yearly": "1 シートあたりの年額",
- "lifetime": "1 シートあたり、一度のお支払い"
+ "yearly": "1 シートあたりの月額(年払い)"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} シート:年額 {total}",
"other": "{count} シート:年額 {total}"
- },
- "lifetime": {
- "one": "{count} シート:{total}、一度のお支払い",
- "other": "{count} シート:{total}、一度のお支払い"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "1 シートにつき 1 台",
"everythingElse": "アプリのその他すべての機能",
"everythingElseDetail": "すべての対応エンジン、SQL エディター、AI アシスタント、MCP サーバー、Safe Mode",
- "iphoneNote": "iPhone・iPad アプリに有料機能はありません。iCloud Sync は無料です。Mac と同期するには、Mac 側に Starter または Team が必要です。"
+ "iphoneNote": "iPhone・iPad アプリに有料機能はありません。iCloud Sync は無料です。Mac と同期するには、Mac 側に Pro または Team が必要です。"
},
"regional": {
"note": "アクセス元の国({country})向けの価格:チェックアウト時に {percent}% 割引になります。",
diff --git a/resources/js/i18n/messages/ko/blog.ts b/resources/js/i18n/messages/ko/blog.ts
index 111b5403..1119a020 100644
--- a/resources/js/i18n/messages/ko/blog.ts
+++ b/resources/js/i18n/messages/ko/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "아직 게시물이 없습니다.",
"guides": "가이드",
+ "news": "소식",
"releases": "릴리스 노트"
},
"latest": "최신 릴리스 글:
{title}.",
diff --git a/resources/js/i18n/messages/ko/pricing.ts b/resources/js/i18n/messages/ko/pricing.ts
index 305c03ab..6f3b21c1 100644
--- a/resources/js/i18n/messages/ko/pricing.ts
+++ b/resources/js/i18n/messages/ko/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "취소할 때까지 매월 갱신됩니다.",
"yearly": "취소할 때까지 매년 갱신됩니다. 월간 결제 12회보다 {percent}% 저렴합니다.",
- "yearlyByTier": "취소할 때까지 매년 갱신됩니다. 월간 결제 12회보다 Starter는 {starterPercent}%, Team은 {teamPercent}% 저렴합니다.",
+ "yearlyByTier": "취소할 때까지 매년 갱신됩니다. 월간 결제 12회보다 Pro는 {starterPercent}%, Team은 {teamPercent}% 저렴합니다.",
"lifetime": "한 번 결제하며 만료일은 없습니다."
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "Mac 앱에 {examples} 같은 기능을 추가합니다.",
"activation": {
"one": "1명, Mac {count}대.",
"other": "1명, Mac 최대 {count}대."
},
"includesTitle": "무료 플랜의 모든 기능에 더해",
- "cta": "Starter 구매"
+ "cta": "Pro 구매"
},
"team": {
"name": "Team",
"description": "팀과 연결 및 저장된 쿼리를 공유하세요.",
"activation": "좌석당 활성화된 Mac 1대.",
- "includesTitle": "Starter의 모든 기능에 더해",
- "cta": "Team 구매"
+ "includesTitle": "Pro의 모든 기능에 더해",
+ "cta": "Team 구매",
+ "subscriptionOnly": "Team은 월간 또는 연간으로만 판매합니다."
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "TablePro 사용을 더 세밀하게 관리해야 하는 회사를 위한 플랜입니다.",
+ "price": "문의하기",
+ "activation": "회사별로 가격을 정합니다.",
+ "includesTitle": "필요한 기능을 알려 주세요. 예를 들면",
+ "includes": [
+ "회사 계정으로 로그인",
+ "팀 연결에 적용되는 정책",
+ "프로덕션 쓰기 감사",
+ "MDM 배포",
+ "청구서와 구매 주문서"
+ ],
+ "cta": "문의하기"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "좌석당 월",
- "yearly": "좌석당 연",
- "lifetime": "좌석당 일회 결제"
+ "yearly": "좌석당 월, 연간 청구"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count}좌석: 연 {total}",
"other": "{count}좌석: 연 {total}"
- },
- "lifetime": {
- "one": "{count}좌석: {total}, 일회 결제",
- "other": "{count}좌석: {total}, 일회 결제"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "좌석당 한 대",
"everythingElse": "앱의 나머지 모든 기능",
"everythingElseDetail": "지원하는 모든 엔진, SQL 편집기, AI 어시스턴트, MCP 서버 및 Safe Mode",
- "iphoneNote": "iPhone 및 iPad 앱에는 유료 기능이 없습니다. 해당 앱의 iCloud Sync는 무료이며, Mac과 동기화하려면 Mac에 Starter 또는 Team이 필요합니다."
+ "iphoneNote": "iPhone 및 iPad 앱에는 유료 기능이 없습니다. 해당 앱의 iCloud Sync는 무료이며, Mac과 동기화하려면 Mac에 Pro 또는 Team이 필요합니다."
},
"regional": {
"note": "접속 국가({country}) 기준 가격: 결제 시 {percent}% 할인됩니다.",
diff --git a/resources/js/i18n/messages/pt-BR/blog.ts b/resources/js/i18n/messages/pt-BR/blog.ts
index 055fde1e..2b6956e0 100644
--- a/resources/js/i18n/messages/pt-BR/blog.ts
+++ b/resources/js/i18n/messages/pt-BR/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "Nenhum artigo ainda.",
"guides": "Guias",
+ "news": "Novidades",
"releases": "Notas de versão"
},
"latest": "Último post de lançamento:
{title}.",
diff --git a/resources/js/i18n/messages/pt-BR/pricing.ts b/resources/js/i18n/messages/pt-BR/pricing.ts
index fc68f1a6..fddf649f 100644
--- a/resources/js/i18n/messages/pt-BR/pricing.ts
+++ b/resources/js/i18n/messages/pt-BR/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "Renovação mensal até você cancelar.",
"yearly": "Renovação anual até você cancelar. {percent}% menos que doze pagamentos mensais.",
- "yearlyByTier": "Renovação anual até você cancelar. Starter custa {starterPercent}% menos que doze pagamentos mensais; Team, {teamPercent}% menos.",
+ "yearlyByTier": "Renovação anual até você cancelar. Pro custa {starterPercent}% menos que doze pagamentos mensais; Team, {teamPercent}% menos.",
"lifetime": "Pago uma vez, sem data de expiração."
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "Adiciona ao app para Mac recursos como {examples}.",
"activation": {
"one": "Uma pessoa, {count} Mac.",
"other": "Uma pessoa, até {count} Macs."
},
"includesTitle": "Tudo do plano Grátis, mais",
- "cta": "Comprar Starter"
+ "cta": "Comprar Pro"
},
"team": {
"name": "Team",
"description": "Compartilhe conexões e consultas salvas com sua equipe.",
"activation": "Um Mac ativado por vaga.",
- "includesTitle": "Tudo do Starter, mais",
- "cta": "Comprar Team"
+ "includesTitle": "Tudo do Pro, mais",
+ "cta": "Comprar Team",
+ "subscriptionOnly": "O Team é vendido por mês ou por ano."
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "Para empresas que precisam de mais controle sobre o uso do TablePro.",
+ "price": "Fale conosco",
+ "activation": "Preço por empresa.",
+ "includesTitle": "Conte o que você precisa, por exemplo",
+ "includes": [
+ "Login com as contas da empresa",
+ "Políticas obrigatórias nas conexões da equipe",
+ "Auditoria de gravações em produção",
+ "Implantação via MDM",
+ "Faturas e pedidos de compra"
+ ],
+ "cta": "Fale conosco"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "por vaga, por mês",
- "yearly": "por vaga, por ano",
- "lifetime": "por vaga, pagamento único"
+ "yearly": "por vaga, por mês, cobrado anualmente"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} vaga: {total} por ano",
"other": "{count} vagas: {total} por ano"
- },
- "lifetime": {
- "one": "{count} vaga: {total}, pagamento único",
- "other": "{count} vagas: {total}, pagamento único"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "Um por vaga",
"everythingElse": "Todo o restante do app",
"everythingElseDetail": "Todos os mecanismos compatíveis, o editor SQL, o assistente de IA, o servidor MCP e o Safe Mode",
- "iphoneNote": "O app para iPhone e iPad não tem recursos pagos. O iCloud Sync é gratuito nesses dispositivos; para sincronizar com um Mac, o Mac precisa de Starter ou Team."
+ "iphoneNote": "O app para iPhone e iPad não tem recursos pagos. O iCloud Sync é gratuito nesses dispositivos; para sincronizar com um Mac, o Mac precisa de Pro ou Team."
},
"regional": {
"note": "Preços para o seu país ({country}): {percent}% de desconto, aplicado no checkout.",
diff --git a/resources/js/i18n/messages/vi/blog.ts b/resources/js/i18n/messages/vi/blog.ts
index 81e0761b..0f56cefb 100644
--- a/resources/js/i18n/messages/vi/blog.ts
+++ b/resources/js/i18n/messages/vi/blog.ts
@@ -4,6 +4,7 @@ export default {
index: {
empty: 'Chưa có bài viết nào.',
guides: 'Hướng dẫn',
+ news: 'Tin tức',
releases: 'Ghi chú phát hành',
},
latest: 'Bài phát hành mới nhất:
{title}.',
diff --git a/resources/js/i18n/messages/vi/pricing.ts b/resources/js/i18n/messages/vi/pricing.ts
index ffa59cd5..8a0f4e98 100644
--- a/resources/js/i18n/messages/vi/pricing.ts
+++ b/resources/js/i18n/messages/vi/pricing.ts
@@ -15,7 +15,7 @@ export default {
captions: {
monthly: 'Tự động gia hạn mỗi tháng cho tới khi bạn hủy.',
yearly: 'Tự động gia hạn mỗi năm cho tới khi bạn hủy. Rẻ hơn {percent}% so với trả theo tháng trong một năm.',
- yearlyByTier: 'Tự động gia hạn mỗi năm cho tới khi bạn hủy. So với trả theo tháng trong một năm, gói Starter rẻ hơn {starterPercent}%, gói Team rẻ hơn {teamPercent}%.',
+ yearlyByTier: 'Tự động gia hạn mỗi năm cho tới khi bạn hủy. So với trả theo tháng trong một năm, gói Pro rẻ hơn {starterPercent}%, gói Team rẻ hơn {teamPercent}%.',
lifetime: 'Trả một lần, không có ngày hết hạn.',
},
tiers: {
@@ -33,20 +33,36 @@ export default {
],
},
starter: {
- name: 'Starter',
+ name: 'Pro',
description: 'Bổ sung cho ứng dụng Mac các tính năng như {examples}.',
activation: {
other: 'Một người, tối đa {count} máy Mac.',
},
includesTitle: 'Mọi thứ trong gói Miễn phí, cộng thêm',
- cta: 'Mua gói Starter',
+ cta: 'Mua gói Pro',
},
team: {
name: 'Team',
description: 'Chia sẻ connection và query đã lưu với nhóm.',
activation: 'Một máy Mac đã kích hoạt cho mỗi seat.',
- includesTitle: 'Mọi thứ trong gói Starter, cộng thêm',
+ includesTitle: 'Mọi thứ trong gói Pro, cộng thêm',
cta: 'Mua gói Team',
+ subscriptionOnly: 'Gói Team bán theo tháng hoặc theo năm.',
+ },
+ enterprise: {
+ name: 'Enterprise',
+ description: 'Dành cho công ty cần kiểm soát cách TablePro được dùng.',
+ price: 'Liên hệ',
+ activation: 'Báo giá theo từng công ty.',
+ includesTitle: 'Cho chúng tôi biết bạn cần gì, ví dụ',
+ includes: [
+ 'Đăng nhập bằng tài khoản công ty',
+ 'Policy bắt buộc trên connection của nhóm',
+ 'Audit các lệnh ghi vào production',
+ 'Triển khai qua MDM',
+ 'Hóa đơn và purchase order',
+ ],
+ cta: 'Liên hệ',
},
},
units: {
@@ -57,8 +73,7 @@ export default {
},
team: {
monthly: 'mỗi seat, mỗi tháng',
- yearly: 'mỗi seat, mỗi năm',
- lifetime: 'mỗi seat, trả một lần',
+ yearly: 'mỗi seat, mỗi tháng, trả theo năm',
},
},
seats: {
@@ -72,7 +87,6 @@ export default {
total: {
monthly: { other: '{count} seat: {total} mỗi tháng' },
yearly: { other: '{count} seat: {total} mỗi năm' },
- lifetime: { other: '{count} seat: {total}, trả một lần' },
},
},
prioritySupport: {
@@ -101,7 +115,7 @@ export default {
macsTeam: 'Một máy mỗi seat',
everythingElse: 'Mọi phần còn lại của ứng dụng',
everythingElseDetail: 'Mọi engine được hỗ trợ, SQL editor, trợ lý AI, MCP server và Safe Mode',
- iphoneNote: 'Ứng dụng cho iPhone và iPad không có tính năng trả phí. Trên iPhone và iPad, iCloud Sync miễn phí; muốn đồng bộ với máy Mac thì máy Mac cần gói Starter hoặc Team.',
+ iphoneNote: 'Ứng dụng cho iPhone và iPad không có tính năng trả phí. Trên iPhone và iPad, iCloud Sync miễn phí; muốn đồng bộ với máy Mac thì máy Mac cần gói Pro hoặc Team.',
},
regional: {
note: 'Giá cho quốc gia của bạn ({country}): giảm {percent}%, áp dụng khi thanh toán.',
diff --git a/resources/js/i18n/messages/zh-Hans/blog.ts b/resources/js/i18n/messages/zh-Hans/blog.ts
index b1d575ca..6abf094e 100644
--- a/resources/js/i18n/messages/zh-Hans/blog.ts
+++ b/resources/js/i18n/messages/zh-Hans/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "暂无文章。",
"guides": "指南",
+ "news": "公告",
"releases": "发行说明"
},
"latest": "最新版本文章:
{title}。",
diff --git a/resources/js/i18n/messages/zh-Hans/pricing.ts b/resources/js/i18n/messages/zh-Hans/pricing.ts
index 8abc3542..14cd4dde 100644
--- a/resources/js/i18n/messages/zh-Hans/pricing.ts
+++ b/resources/js/i18n/messages/zh-Hans/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "取消前每月自动续订。",
"yearly": "取消前每年自动续订。比按月支付十二次低 {percent}%。",
- "yearlyByTier": "取消前每年自动续订。相比按月支付十二次,Starter 低 {starterPercent}%,Team 低 {teamPercent}%。",
+ "yearlyByTier": "取消前每年自动续订。相比按月支付十二次,Pro 低 {starterPercent}%,Team 低 {teamPercent}%。",
"lifetime": "一次付费,无到期日。"
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "为 Mac 应用添加 {examples} 等功能。",
"activation": {
"one": "1 人,{count} 台 Mac。",
"other": "1 人,最多 {count} 台 Mac。"
},
"includesTitle": "免费版的全部功能,外加",
- "cta": "购买 Starter"
+ "cta": "购买 Pro"
},
"team": {
"name": "Team",
"description": "与团队共享连接和已保存的查询。",
"activation": "每个席位对应 1 台已激活的 Mac。",
- "includesTitle": "Starter 的全部功能,外加",
- "cta": "购买 Team"
+ "includesTitle": "Pro 的全部功能,外加",
+ "cta": "购买 Team",
+ "subscriptionOnly": "Team 按月或按年订阅。"
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "适合需要统一管理 TablePro 使用方式的公司。",
+ "price": "联系我们",
+ "activation": "按公司报价。",
+ "includesTitle": "告诉我们您的需求,例如",
+ "includes": [
+ "使用公司账号登录",
+ "对团队连接强制执行的策略",
+ "生产环境写入审计",
+ "通过 MDM 部署",
+ "发票和采购订单"
+ ],
+ "cta": "联系我们"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "每席位每月",
- "yearly": "每席位每年",
- "lifetime": "每席位一次付费"
+ "yearly": "每席位每月,按年付费"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} 个席位:每年 {total}",
"other": "{count} 个席位:每年 {total}"
- },
- "lifetime": {
- "one": "{count} 个席位:{total},一次付费",
- "other": "{count} 个席位:{total},一次付费"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "每席位一台",
"everythingElse": "应用中的其他所有功能",
"everythingElseDetail": "所有受支持的引擎、SQL 编辑器、AI 助手、MCP 服务器和 Safe Mode",
- "iphoneNote": "iPhone 和 iPad 应用没有付费功能,iCloud Sync 在该应用中免费。若要与 Mac 同步,Mac 需要 Starter 或 Team。"
+ "iphoneNote": "iPhone 和 iPad 应用没有付费功能,iCloud Sync 在该应用中免费。若要与 Mac 同步,Mac 需要 Pro 或 Team。"
},
"regional": {
"note": "您所在国家或地区({country})的价格:结账时享受 {percent}% 折扣。",
diff --git a/resources/js/i18n/messages/zh-Hant/blog.ts b/resources/js/i18n/messages/zh-Hant/blog.ts
index d107e152..3b73cf33 100644
--- a/resources/js/i18n/messages/zh-Hant/blog.ts
+++ b/resources/js/i18n/messages/zh-Hant/blog.ts
@@ -4,6 +4,7 @@ export default {
"index": {
"empty": "尚無文章。",
"guides": "指南",
+ "news": "公告",
"releases": "版本說明"
},
"latest": "最新版本文章:
{title}。",
diff --git a/resources/js/i18n/messages/zh-Hant/pricing.ts b/resources/js/i18n/messages/zh-Hant/pricing.ts
index b4450358..d174e9e2 100644
--- a/resources/js/i18n/messages/zh-Hant/pricing.ts
+++ b/resources/js/i18n/messages/zh-Hant/pricing.ts
@@ -15,7 +15,7 @@ export default {
"captions": {
"monthly": "取消前每月自動續訂。",
"yearly": "取消前每年自動續訂。比按月支付十二次低 {percent}%。",
- "yearlyByTier": "取消前每年自動續訂。相較於按月支付十二次,Starter 低 {starterPercent}%,Team 低 {teamPercent}%。",
+ "yearlyByTier": "取消前每年自動續訂。相較於按月支付十二次,Pro 低 {starterPercent}%,Team 低 {teamPercent}%。",
"lifetime": "一次付費,無到期日。"
},
"tiers": {
@@ -33,21 +33,37 @@ export default {
]
},
"starter": {
- "name": "Starter",
+ "name": "Pro",
"description": "為 Mac App 加入 {examples} 等功能。",
"activation": {
"one": "1 人,{count} 台 Mac。",
"other": "1 人,最多 {count} 台 Mac。"
},
"includesTitle": "免費版的所有功能,另加",
- "cta": "購買 Starter"
+ "cta": "購買 Pro"
},
"team": {
"name": "Team",
"description": "與團隊共用連線和已儲存的查詢。",
"activation": "每個席位對應 1 台已啟用的 Mac。",
- "includesTitle": "Starter 的所有功能,另加",
- "cta": "購買 Team"
+ "includesTitle": "Pro 的所有功能,另加",
+ "cta": "購買 Team",
+ "subscriptionOnly": "Team 按月或按年訂閱。"
+ },
+ "enterprise": {
+ "name": "Enterprise",
+ "description": "適合需要統一管理 TablePro 使用方式的公司。",
+ "price": "聯絡我們",
+ "activation": "依公司報價。",
+ "includesTitle": "告訴我們您的需求,例如",
+ "includes": [
+ "使用公司帳號登入",
+ "對團隊連線強制執行的政策",
+ "正式環境寫入稽核",
+ "透過 MDM 部署",
+ "發票與採購單"
+ ],
+ "cta": "聯絡我們"
}
},
"units": {
@@ -58,8 +74,7 @@ export default {
},
"team": {
"monthly": "每席位每月",
- "yearly": "每席位每年",
- "lifetime": "每席位一次付費"
+ "yearly": "每席位每月,按年付費"
}
},
"seats": {
@@ -78,10 +93,6 @@ export default {
"yearly": {
"one": "{count} 個席位:每年 {total}",
"other": "{count} 個席位:每年 {total}"
- },
- "lifetime": {
- "one": "{count} 個席位:{total},一次付費",
- "other": "{count} 個席位:{total},一次付費"
}
}
},
@@ -116,7 +127,7 @@ export default {
"macsTeam": "每席位一台",
"everythingElse": "App 中的其他所有功能",
"everythingElseDetail": "所有支援的引擎、SQL 編輯器、AI 助理、MCP 伺服器與 Safe Mode",
- "iphoneNote": "iPhone 與 iPad App 沒有付費功能,iCloud Sync 在此 App 中免費。若要與 Mac 同步,Mac 需要 Starter 或 Team。"
+ "iphoneNote": "iPhone 與 iPad App 沒有付費功能,iCloud Sync 在此 App 中免費。若要與 Mac 同步,Mac 需要 Pro 或 Team。"
},
"regional": {
"note": "您所在國家或地區({country})的價格:結帳時享有 {percent}% 折扣。",
diff --git a/resources/js/lib/data/paid-features.ts b/resources/js/lib/data/paid-features.ts
index 7bcd2777..15d0391e 100644
--- a/resources/js/lib/data/paid-features.ts
+++ b/resources/js/lib/data/paid-features.ts
@@ -1,5 +1,5 @@
/**
- * resources/data/paid-features.json, typed: the ten features a Starter or
+ * resources/data/paid-features.json, typed: the ten features a Pro or
* Team plan adds to the Mac app, in display order.
*
* The names are byte-identical to `ProFeature.displayName` in the app, and
@@ -29,7 +29,7 @@ export interface PaidFeature {
proFeature: string;
/** `ProFeature.displayName`, never translated. */
name: string;
- /** The lowest plan that includes it. Team includes everything in Starter. */
+ /** The lowest plan that includes it. Team includes everything in Pro. */
tier: PaidTierId;
platforms: PlatformId[];
sinceAppVersion: string;
diff --git a/resources/js/lib/data/pricing.ts b/resources/js/lib/data/pricing.ts
index 781cfa6d..9eae798e 100644
--- a/resources/js/lib/data/pricing.ts
+++ b/resources/js/lib/data/pricing.ts
@@ -20,6 +20,9 @@ export type TierId = 'free' | PaidTierId;
export type CyclePrices = Record
;
+/** Team is a subscription: it has no one-time price. */
+export type TeamCycle = Exclude;
+
export interface StarterTier {
/** One license, activated on up to `activations` Macs. */
unit: 'license';
@@ -30,7 +33,7 @@ export interface StarterTier {
export interface TeamTier {
/** Priced per seat; a seat is one activated Mac. */
unit: 'seat';
- prices: CyclePrices;
+ prices: Record;
seats: { min: number; max: number };
activationsPerSeat: number;
/** Team customers' emails are answered first, within this many business days. */
@@ -61,30 +64,39 @@ export interface PricingData {
*/
export const PRICING = data as PricingData;
-export function tierPrice(tier: PaidTierId, cycle: BillingCycle): number {
- return PRICING.tiers[tier].prices[cycle];
+/** What a plan costs in a cycle, or undefined for a cycle it is not sold in (Team one-time). */
+export function tierPrice(tier: PaidTierId, cycle: BillingCycle): number | undefined {
+ const prices: Partial = PRICING.tiers[tier].prices;
+
+ return prices[cycle];
+}
+
+/** Whether checkout sells the plan in the cycle. */
+export function sells(tier: PaidTierId, cycle: BillingCycle): boolean {
+ return tierPrice(tier, cycle) !== undefined;
}
/**
* What a Team purchase costs for `seats` seats, clamped to the allowed range.
* Rounded to cents, because `1.25 × 7` is not exact in binary floating point.
*/
-export function teamTotal(seats: number, cycle: BillingCycle): number {
+export function teamTotal(seats: number, cycle: TeamCycle): number {
const { min, max } = PRICING.tiers.team.seats;
const count = Math.min(max, Math.max(min, Math.round(seats)));
- return Math.round(tierPrice('team', cycle) * count * 100) / 100;
+ return Math.round(PRICING.tiers.team.prices[cycle] * count * 100) / 100;
}
/**
* How much a yearly plan saves against twelve monthly payments, as a whole
* percentage rounded down, so the page never overstates it. Computed, never
- * typed: Starter and Team both give 33 today.
+ * typed: Pro gives 34 and Team 16 today.
*/
export function yearlySavingsPercent(tier: PaidTierId): number {
- const twelveMonths = tierPrice(tier, 'monthly') * 12;
+ const { monthly, yearly } = PRICING.tiers[tier].prices;
+ const twelveMonths = monthly * 12;
- return Math.floor(((twelveMonths - tierPrice(tier, 'yearly')) / twelveMonths) * 100);
+ return Math.floor(((twelveMonths - yearly) / twelveMonths) * 100);
}
/** A price as structured data needs it: a locale-neutral decimal string, `"2.99"`, `"24"`. */
diff --git a/resources/js/lib/structured-data.ts b/resources/js/lib/structured-data.ts
index ce230494..e326dd28 100644
--- a/resources/js/lib/structured-data.ts
+++ b/resources/js/lib/structured-data.ts
@@ -62,7 +62,7 @@ export interface Crumb {
export type BillingCycle = 'monthly' | 'yearly' | 'lifetime';
export interface OfferInput {
- /** The plan's visible name in the page's language, such as "Starter, yearly". */
+ /** The plan's visible name in the page's language, such as "Pro, yearly". */
name: string;
price: number;
/** ISO 4217, from `pricing.json`. */
diff --git a/resources/js/pages/Blog/Index.tsx b/resources/js/pages/Blog/Index.tsx
index 99fc53dd..4f05ebb7 100644
--- a/resources/js/pages/Blog/Index.tsx
+++ b/resources/js/pages/Blog/Index.tsx
@@ -19,16 +19,17 @@ interface BlogIndexContent {
interface Props {
content: BlogIndexContent;
- /** Newest first. On `/vi/blog`, the Vietnamese guides and the English release posts, each of those marked as English. */
+ /** Newest first. On `/vi/blog`, the Vietnamese guides and news and the English release posts, each of those marked as English. */
posts: PostSummary[];
}
/**
* `/blog` and `/vi/blog` (sitemap §A.5; design-system §8.9).
*
- * Two lists, each newest first: the guides, then the release posts under the
- * line on what they are, with the changelog linked, and the release email
- * signup. The guides list is left out while there are none.
+ * Three lists, each newest first: news about TablePro itself, the guides,
+ * then the release posts under the line on what they are, with the changelog
+ * linked, and the release email signup. The news and guides lists are left
+ * out while they are empty.
*
* `/vi/blog` has Vietnamese chrome. An English post keeps its English title
* and description, marked `lang="en"` and labelled "(tiếng Anh)", and links
@@ -41,6 +42,7 @@ export default function BlogIndex({ content, posts }: Props) {
const { locale, m, path } = useI18n();
const inLanguage = LOCALES.supported[locale].hreflang;
const changelogLang = locale === 'en' ? undefined : 'en';
+ const news = posts.filter((post) => post.kind === 'news');
const guides = posts.filter((post) => post.kind === 'guide');
const releases = posts.filter((post) => post.kind === 'release');
@@ -51,7 +53,7 @@ export default function BlogIndex({ content, posts }: Props) {
url: absoluteUrl(canonicalBaseUrl, path('/blog')),
name: content.seo.title,
description: content.seo.description,
- items: [...guides, ...releases].map((post) => ({ name: post.title, path: post.url })),
+ items: [...news, ...guides, ...releases].map((post) => ({ name: post.title, path: post.url })),
},
),
]);
@@ -63,6 +65,12 @@ export default function BlogIndex({ content, posts }: Props) {
+ {news.length > 0 && (
+
+ )}
+
{guides.length > 0 && (
diff --git a/resources/js/pages/Pricing.tsx b/resources/js/pages/Pricing.tsx
index 8714dacf..3dd4f2e6 100644
--- a/resources/js/pages/Pricing.tsx
+++ b/resources/js/pages/Pricing.tsx
@@ -5,6 +5,7 @@ import { planOffers } from '@/components/pricing/offers';
import PricingPlans from '@/components/pricing/pricing-plans';
import type { PricingContent, PricingPageProps } from '@/components/pricing/types';
import SEOHead from '@/components/seo/seo-head';
+import Callout from '@/components/ui/callout';
import { accountHref, SUPPORT_EMAIL } from '@/components/site/site-links';
import Container from '@/components/ui/container';
import DescriptionList, { DescriptionItem } from '@/components/ui/description-list';
@@ -24,6 +25,9 @@ import LandingLayout from '@/layouts/landing-layout';
type Interpolate = (template: string, values: Record) => string;
+/** The post that explains the new prices. It exists in English and Vietnamese; every other page links the English one. */
+const PRICE_CHANGE_POST = 'new-prices-2026';
+
/** The `{slots}` the page copy uses, every one from pricing.json or facts.json. */
function copyValues(): Record {
return {
@@ -150,6 +154,17 @@ export default function Pricing({ content, paidFeatures, checkout, featuredEngin
{content.plans.title}
+
+
+ {content.priceChange.text}{' '}
+
+ {content.priceChange.link}
+
+
+
diff --git a/tests/Feature/Compare/ComparePagesTest.php b/tests/Feature/Compare/ComparePagesTest.php
index a58dfdb1..696faabb 100644
--- a/tests/Feature/Compare/ComparePagesTest.php
+++ b/tests/Feature/Compare/ComparePagesTest.php
@@ -228,7 +228,7 @@ function formattedDate(string $iso, string $locale): string
->toContain('id="switching"')
->toContain('id="sources"')
->toContain('$' . $basic['amount'] . ' once per license')
- ->toContain('Starter: $' . $starterMonthly . ' a month')
+ ->toContain('Pro: $' . $starterMonthly . ' a month')
->toContain('File > Import > Import from Other App…')
->toContain('"@type":"BreadcrumbList"')
->not->toContain('"Review"')
diff --git a/tests/Feature/Content/ReviewedFactsTest.php b/tests/Feature/Content/ReviewedFactsTest.php
index 66c76ad4..10fc1f3b 100644
--- a/tests/Feature/Content/ReviewedFactsTest.php
+++ b/tests/Feature/Content/ReviewedFactsTest.php
@@ -175,7 +175,7 @@ function (mixed $value) use (&$strings): void {
],
'DBeaver: no "native Mac client", no self-listing Compare & Sync' => [
'content/en/compare/dbeaver.json',
- ['It is part of Starter.'],
+ ['It is part of Pro.'],
['native Mac client', 'along with features such as {starterExamples}'],
],
'the compare method names every kind of official source' => [
diff --git a/tests/Feature/Data/PaidFeaturesDataTest.php b/tests/Feature/Data/PaidFeaturesDataTest.php
index 92dbc723..0fc3659f 100644
--- a/tests/Feature/Data/PaidFeaturesDataTest.php
+++ b/tests/Feature/Data/PaidFeaturesDataTest.php
@@ -55,7 +55,7 @@ function paidFeaturePageAnchors(): array
}
});
-it('splits eight Starter features from two Team features, as requiredTier does', function (): void {
+it('splits eight Pro features from two Team features, as requiredTier does', function (): void {
$tiers = array_column(paidFeaturesJson(), 'tier', 'proFeature');
expect(array_keys(array_filter($tiers, fn(string $tier): bool => $tier === 'starter')))->toBe([
diff --git a/tests/Feature/Data/PricingDataTest.php b/tests/Feature/Data/PricingDataTest.php
index a37db5f1..f4604f52 100644
--- a/tests/Feature/Data/PricingDataTest.php
+++ b/tests/Feature/Data/PricingDataTest.php
@@ -42,9 +42,9 @@ function pricingCents(int|float $amount): int
expect(array_keys($pricing['tiers']['starter']))->toBe(['unit', 'prices', 'activations']);
expect(array_keys($pricing['tiers']['team']))->toBe(['unit', 'prices', 'seats', 'activationsPerSeat', 'prioritySupport']);
- foreach (['starter', 'team'] as $tier) {
- expect(array_keys($pricing['tiers'][$tier]['prices']))->toBe($pricing['cycles'], "{$tier} must price every cycle, in cycle order");
- }
+ // Pro is sold in every cycle; Team is a subscription, with no one-time price.
+ expect(array_keys($pricing['tiers']['starter']['prices']))->toBe($pricing['cycles'], 'Pro must price every cycle, in cycle order');
+ expect(array_keys($pricing['tiers']['team']['prices']))->toBe(['monthly', 'yearly'], 'Team prices the subscription cycles only, in cycle order');
});
it('bills in US dollars through Polar', function (): void {
@@ -63,15 +63,15 @@ function pricingCents(int|float $amount): int
expect($tiers['free'])->toBe(['price' => 0]);
- // Starter: one license on up to two Macs.
+ // Pro (the `starter` code): one license on up to three Macs.
expect($tiers['starter']['unit'])->toBe('license');
- expect(array_map(pricingCents(...), $tiers['starter']['prices']))->toBe(['monthly' => 299, 'yearly' => 2400, 'lifetime' => 5900]);
- expect($tiers['starter']['activations'])->toBe(2);
+ expect(array_map(pricingCents(...), $tiers['starter']['prices']))->toBe(['monthly' => 499, 'yearly' => 3900, 'lifetime' => 7900]);
+ expect($tiers['starter']['activations'])->toBe(3);
- // Team: priced per seat, a seat being one activated Mac, 5 to 200 seats.
+ // Team: priced per seat, a seat being one activated Mac, 2 to 200 seats, monthly or yearly only.
expect($tiers['team']['unit'])->toBe('seat');
- expect(array_map(pricingCents(...), $tiers['team']['prices']))->toBe(['monthly' => 125, 'yearly' => 1000, 'lifetime' => 2500]);
- expect($tiers['team']['seats'])->toBe(['min' => 5, 'max' => 200]);
+ expect(array_map(pricingCents(...), $tiers['team']['prices']))->toBe(['monthly' => 1200, 'yearly' => 12000]);
+ expect($tiers['team']['seats'])->toBe(['min' => 2, 'max' => 200]);
expect($tiers['team']['activationsPerSeat'])->toBe(1);
});
diff --git a/tests/Feature/Databases/DatabasePagesTest.php b/tests/Feature/Databases/DatabasePagesTest.php
index f1a4276f..683e0a2c 100644
--- a/tests/Feature/Databases/DatabasePagesTest.php
+++ b/tests/Feature/Databases/DatabasePagesTest.php
@@ -608,7 +608,7 @@ function databasePagesFacts(string $path): array
it('links pricing from the closing band and from every plan a page names', function (): void {
requireSsr();
- $starter = '#]*href="/pricing"[^>]*>Starter#';
+ $starter = '#]*href="/pricing"[^>]*>Pro#';
$named = array_keys(array_filter(databasePagesFiles('en'), fn(array $copy): bool => preg_match('/\{[A-Za-z]+Tier\}/', json_encode($copy)) === 1));
expect($named)->toContain('postgresql-client', 'redshift-client');
diff --git a/tests/Feature/Features/FeatureContentTest.php b/tests/Feature/Features/FeatureContentTest.php
index e9bb9d6c..ca7a0740 100644
--- a/tests/Feature/Features/FeatureContentTest.php
+++ b/tests/Feature/Features/FeatureContentTest.php
@@ -121,7 +121,7 @@ function featureSchemaAssertLink(array $link, string $where): void
expect(array_keys($hub['labels']))->toBe(['number', 'tiers', 'paidBadge', 'header', 'sections', 'availability', 'download']);
expect(array_keys($hub['labels']['tiers']))->toBe(['free', 'starter', 'team']);
- expect($hub['labels']['tiers']['starter'])->toBe('Starter');
+ expect($hub['labels']['tiers']['starter'])->toBe('Pro');
expect($hub['labels']['tiers']['team'])->toBe('Team');
expect($hub['labels']['paidBadge'])->toContain('{name}')->toContain('{tier}');
expect($hub['labels']['availability']['plan'])->toContain('{tier}');
diff --git a/tests/Feature/Landing/BlogTest.php b/tests/Feature/Landing/BlogTest.php
index 092fefdd..3d43668e 100644
--- a/tests/Feature/Landing/BlogTest.php
+++ b/tests/Feature/Landing/BlogTest.php
@@ -49,6 +49,11 @@
'postgresql-ssh-tunnel-mac' => '2026-10-08',
];
+// Notices about TablePro itself (`kind: news`), in English and Vietnamese.
+const BLOG_NEWS = [
+ 'new-prices-2026' => '2026-11-02',
+];
+
const BLOG_GUIDE_PAGES = [
'postgresql-ssh-tunnel-mac' => ['/postgresql-client', '/features/connections#ssh'],
'connect-postgresql-mysql-docker-mac' => ['/postgresql-client', '/mysql-client', '/features/connections#project-folder'],
@@ -92,7 +97,7 @@ function blogMatter(string $slug, string $locale = 'en'): array
*/
function blogIndexOrder(): array
{
- $dates = [...BLOG_GUIDES, ...BLOG_PUBLISHED];
+ $dates = [...BLOG_NEWS, ...BLOG_GUIDES, ...BLOG_PUBLISHED];
$slugs = array_keys($dates);
usort($slugs, fn(string $a, string $b): int => [$dates[$b], $a] <=> [$dates[$a], $b]);
@@ -130,10 +135,14 @@ function blogSlotIds(string $html): array
->where('posts', fn($posts): bool => collect($posts)->pluck('slug')->all() === $expected)
->where('posts.0.url', '/blog/' . $expected[0])
->where('posts.0.locale', 'en')
- ->where('posts.0.date', BLOG_PUBLISHED[$expected[0]])
- ->where('posts.0.dateFormatted', 'October 20, 2026')
+ ->where('posts.0.date', [...BLOG_NEWS, ...BLOG_GUIDES, ...BLOG_PUBLISHED][$expected[0]])
+ ->where('posts.0.dateFormatted', 'November 2, 2026')
->where('posts', fn($posts): bool => collect($posts)->every(
- fn(array $post): bool => $post['kind'] === (array_key_exists($post['slug'], BLOG_GUIDES) ? 'guide' : 'release'),
+ fn(array $post): bool => $post['kind'] === match (true) {
+ array_key_exists($post['slug'], BLOG_NEWS) => 'news',
+ array_key_exists($post['slug'], BLOG_GUIDES) => 'guide',
+ default => 'release',
+ },
)));
});
@@ -145,6 +154,36 @@ function blogSlotIds(string $html): array
expect(array_intersect_key(BLOG_GUIDES, BLOG_PUBLISHED))->toBe([]);
});
+it('marks each news post as news, in English and Vietnamese, announcing no release', function (): void {
+ foreach (array_keys(BLOG_NEWS) as $slug) {
+ foreach (['en', 'vi'] as $locale) {
+ $matter = blogMatter($slug, $locale);
+
+ expect($matter['kind'] ?? null)->toBe('news', "{$slug} ({$locale}) is not marked as news")
+ ->and(array_key_exists('release', $matter))->toBeFalse("{$slug} ({$locale}) names a release");
+ }
+ }
+
+ expect(array_intersect_key(BLOG_NEWS, [...BLOG_GUIDES, ...BLOG_PUBLISHED]))->toBe([]);
+});
+
+it('renders each news post as news in English and Vietnamese', function (string $slug, string $locale): void {
+ $prefix = $locale === 'en' ? '' : "/{$locale}";
+
+ $this->get("{$prefix}/blog/{$slug}")
+ ->assertOk()
+ ->assertInertia(fn(AssertableInertia $page) => $page
+ ->component('Blog/Post')
+ ->where('locale', $locale)
+ ->where('seo.robots', 'index, follow')
+ ->where('seo.canonical', "https://localhost{$prefix}/blog/{$slug}")
+ ->where('post.slug', $slug)
+ ->where('post.kind', 'news')
+ ->where('post.title', blogMatter($slug, $locale)['title'])
+ ->where('post.date', BLOG_NEWS[$slug])
+ ->where('post.release', null));
+})->with(fn(): array => collect(array_keys(BLOG_NEWS))->crossJoin(['en', 'vi'])->all());
+
it('describes the blog as guides and release posts in every language', function (): void {
$kinds = [
'en' => ['guides', 'release'],
@@ -208,7 +247,7 @@ function blogSlotIds(string $html): array
expect(blogMatter($slug, 'vi')['tags'])->toBe(blogMatter($slug)['tags'], "{$slug}: the translation links other pages than the original");
}
- expect(glob(resource_path('blog/vi/*.md')))->toHaveCount(count(BLOG_GUIDES));
+ expect(glob(resource_path('blog/vi/*.md')))->toHaveCount(count(BLOG_GUIDES) + count(BLOG_NEWS));
});
it('gives each Vietnamese guide the same sections as the original, in the same order', function (): void {
@@ -453,15 +492,21 @@ function blogSlotIds(string $html): array
->where('seo.canonical', 'https://localhost/vi/blog')
->where('seo.alternates', fn($alternates): bool => collect($alternates)->pluck('hreflang')->all() === ['en', 'vi'])
->missing('content.seo.indexable')
- ->has('posts', count(BLOG_GUIDES) + count(BLOG_PUBLISHED))
- ->where('posts', fn($posts): bool => collect($posts)->slice(3, $guides)->every(
+ ->has('posts', count(BLOG_NEWS) + count(BLOG_GUIDES) + count(BLOG_PUBLISHED))
+ ->where('posts', fn($posts): bool => collect($posts)->slice(count(BLOG_NEWS) + 3, $guides)->every(
fn(array $post): bool => $post['kind'] === 'guide' && $post['locale'] === 'vi' && $post['url'] === "/vi/blog/{$post['slug']}",
))
- ->where('posts.0.slug', 'tablepro-0-82')
- ->where('posts.0.locale', 'en')
- ->where('posts.0.url', '/blog/tablepro-0-82')
- ->where('posts.0.title', blogMatter('tablepro-0-82')['title'])
- ->where('posts.0.dateFormatted', '20 tháng 10 năm 2026')
+ ->where('posts.0.slug', 'new-prices-2026')
+ ->where('posts.0.locale', 'vi')
+ ->where('posts.0.kind', 'news')
+ ->where('posts.0.url', '/vi/blog/new-prices-2026')
+ ->where('posts.0.title', blogMatter('new-prices-2026', 'vi')['title'])
+ ->where('posts.0.dateFormatted', '2 tháng 11 năm 2026')
+ ->where('posts.1.slug', 'tablepro-0-82')
+ ->where('posts.1.locale', 'en')
+ ->where('posts.1.url', '/blog/tablepro-0-82')
+ ->where('posts.1.title', blogMatter('tablepro-0-82')['title'])
+ ->where('posts.1.dateFormatted', '20 tháng 10 năm 2026')
->where('localization.switcher.0.href', '/blog'));
expect($response->getContent())->toContain('group('ssr');
- it('lists the guides, then the release posts under the line that says what they are', function (string $path, string $guides, string $releases, string $lead): void {
+ it('lists the news, the guides, then the release posts under the line that says what they are', function (string $path, string $news, string $guides, string $releases, string $lead): void {
$document = Dom\HTMLDocument::createFromString((string) $this->get($path)->getContent(), LIBXML_NOERROR);
$main = $document->querySelector('main');
$sections = array_map(fn($section): string => $section->getAttribute('id'), iterator_to_array($document->querySelectorAll('main > section[id]')));
- expect(array_values($sections))->toBe(['guides', 'releases'])
+ expect(array_values($sections))->toBe(['news', 'guides', 'releases'])
+ ->and($main->querySelector('#news-title')?->textContent)->toBe($news)
+ ->and($main->querySelectorAll('#news ol > li')->length)->toBe(count(BLOG_NEWS))
->and($main->querySelector('#guides-title')?->textContent)->toBe($guides)
->and($main->querySelector('#releases-title')?->textContent)->toBe($releases)
->and($main->querySelector('#releases')?->textContent)->toContain($lead)
@@ -660,8 +707,8 @@ function blogSlotIds(string $html): array
->and($main->querySelectorAll('#guides ol > li')->length)->toBe(count(BLOG_GUIDES))
->and($main->querySelectorAll('#releases ol > li')->length)->toBe(count(BLOG_PUBLISHED));
})->with([
- ['/blog', 'Guides', 'Release notes', 'Full version notes are in the changelog'],
- ['/vi/blog', 'Hướng dẫn', 'Ghi chú phát hành', 'Ghi chú đầy đủ của từng phiên bản nằm trong changelog'],
+ ['/blog', 'News', 'Guides', 'Release notes', 'Full version notes are in the changelog'],
+ ['/vi/blog', 'Tin tức', 'Hướng dẫn', 'Ghi chú phát hành', 'Ghi chú đầy đủ của từng phiên bản nằm trong changelog'],
])->group('ssr');
it('shows the dated correction on the 0.74 post', function (): void {
diff --git a/tests/Feature/Landing/FaqPageTest.php b/tests/Feature/Landing/FaqPageTest.php
index 7e402413..77ec185d 100644
--- a/tests/Feature/Landing/FaqPageTest.php
+++ b/tests/Feature/Landing/FaqPageTest.php
@@ -69,7 +69,7 @@ function faqKnownTags(): array
->where('facts.paidFeatures.team', ['Team Catalog', 'Team Library'])
->where('facts.commerce.merchant', 'Polar')
->where('facts.commerce.refundDays', 7)
- ->where('facts.commerce.teamMinSeats', 5)
+ ->where('facts.commerce.teamMinSeats', 2)
->where('links.email', 'hello@tablepro.app')
->where('links.portal', fn(string $url): bool => str_starts_with($url, 'https://'))
->has('facts.featuredEngines')
diff --git a/tests/Feature/Landing/HomepageRenderTest.php b/tests/Feature/Landing/HomepageRenderTest.php
index 1af849b4..1d907519 100644
--- a/tests/Feature/Landing/HomepageRenderTest.php
+++ b/tests/Feature/Landing/HomepageRenderTest.php
@@ -400,7 +400,7 @@ function homeSectionIds(string $html): array
}
})->with(['/', '/vi'])->group('ssr');
- it('says what Starter adds and whom it is for, every cycle\'s price and the refund window, in the pricing section', function (string $path, string $locale, array $copy, string $pattern, string $decimal): void {
+ it('says what Pro adds and whom it is for, every cycle\'s price and the refund window, in the pricing section', function (string $path, string $locale, array $copy, string $pattern, string $decimal): void {
$html = html_entity_decode(ssrHome($path), ENT_QUOTES | ENT_HTML5);
$start = (int) strpos($html, 'id="pricing"');
$section = substr($html, $start, (int) strpos($html, 'id="open-source"') - $start);
@@ -422,7 +422,9 @@ function homeSectionIds(string $html): array
->toContain('href="' . ($locale === 'en' ? '' : "/{$locale}") . '/refund-policy"');
foreach (['starter', 'team'] as $tier) {
- foreach ($pricing['tiers'][$tier]['prices'] as $amount) {
+ foreach ($pricing['tiers'][$tier]['prices'] as $cycle => $amount) {
+ // Team shows its yearly price as a month's share of it.
+ $amount = $tier === 'team' && $cycle === 'yearly' ? $amount / 12 : $amount;
$price = sprintf($pattern, str_replace('.', $decimal, is_int($amount) ? (string) $amount : number_format($amount, 2, '.', '')));
expect($section)->toContain(">{$price}<");
diff --git a/tests/Feature/Landing/IosPageTest.php b/tests/Feature/Landing/IosPageTest.php
index a7f70ba2..77c94d15 100644
--- a/tests/Feature/Landing/IosPageTest.php
+++ b/tests/Feature/Landing/IosPageTest.php
@@ -197,7 +197,7 @@ function iosPageText(string $locale): string
expect(implode("\n", $en['safeMode']['paragraphs']))->toContain('{levels}');
- expect(implode("\n", $en['mac']['paragraphs']))->toContain('Starter or Team')->toContain('free here');
+ expect(implode("\n", $en['mac']['paragraphs']))->toContain('Pro or Team')->toContain('free here');
expect($en['privacy']['paragraphs'][0])->toStartWith('Usage reporting is off by default.')
->toContain('Enable Share Usage Data in Settings > Privacy')
diff --git a/tests/Feature/Landing/LegalPagesTest.php b/tests/Feature/Landing/LegalPagesTest.php
index 6f9ac3b3..93c81ce6 100644
--- a/tests/Feature/Landing/LegalPagesTest.php
+++ b/tests/Feature/Landing/LegalPagesTest.php
@@ -144,8 +144,8 @@ function legalShape(string $document, string $locale): array
]);
it('formats the update date in each language on the server', function (): void {
- get('/privacy')->assertInertia(fn(AssertableInertia $page) => $page->where('document.updatedAtFormatted', 'October 22, 2026'));
- get('/vi/privacy')->assertInertia(fn(AssertableInertia $page) => $page->where('document.updatedAtFormatted', '22 tháng 10 năm 2026'));
+ get('/privacy')->assertInertia(fn(AssertableInertia $page) => $page->where('document.updatedAtFormatted', 'November 2, 2026'));
+ get('/vi/privacy')->assertInertia(fn(AssertableInertia $page) => $page->where('document.updatedAtFormatted', '2 tháng 11 năm 2026'));
});
it('indexes each document in every supported language', function (string $route): void {
@@ -371,7 +371,7 @@ function legalShape(string $document, string $locale): array
it('points every language at the AGPL for the apps', function (string $locale): void {
$intro = strtok(YamlFrontMatter::parse(legalSource('terms', $locale))->body(), "\n");
- expect(YamlFrontMatter::parse(legalSource('terms', $locale))->matter('updatedAt'))->toBe('2026-10-10');
+ expect(YamlFrontMatter::parse(legalSource('terms', $locale))->matter('updatedAt'))->toBe('2026-11-02');
expect($intro)->toContain('AGPLv3');
expect(legalSection('terms', $locale, 'acceptable-use'))->not->toContain('AGPL');
expect(legalSection('terms', $locale, 'warranty'))->toContain('AGPLv3')->toMatch('/(? $lines) {
+ // An English-only page, such as the brand guidelines, has no translation to hold to its names.
+ if (! isset($legal[$file])) {
+ continue;
+ }
+
foreach ($lines as $index => $line) {
$target = $plain($legal[$file][$index] ?? '');
$where = "legal/{$locale}/{$file}:" . ($index + 1);
foreach ($legalNames as $name) {
- if (str_contains($plain($line), $plain($name)) && ! str_contains($target, $plain($name))) {
+ // A whole name, so "Pro" is not found inside "TablePro"; only Latin letters bound it, as a Korean particle attaches.
+ $named = fn(string $text): bool => preg_match('/(? $write($pricing['tiers'][$tier]['prices'][$cycle]), 'hidden' => $cycle !== 'yearly'];
+ // Team has no one-time price, and shows its yearly price as a month's share of it.
+ $amount = $pricing['tiers'][$tier]['prices'][$cycle] ?? null;
+
+ if ($amount !== null) {
+ $expected[] = ['price' => $write($tier === 'team' && $cycle === 'yearly' ? $amount / 12 : $amount), 'hidden' => $cycle !== 'yearly'];
+ }
}
}
@@ -263,13 +268,13 @@ function pricingExpectedPricePoints(string $pattern, string $decimal): array
}
})->with([
'English' => [
- '/pricing', 'en', ['$0', '$24', '$10', '5 seats: $50 per year'], '$%s', '.',
+ '/pricing', 'en', ['$0', '$39', '$10', '2 seats: $240 per year'], '$%s', '.',
'Every paid plan can be refunded within {days} days of purchase, and each monthly or yearly renewal within {days} days of its charge.',
['Included', 'Not included'],
- ['no trial period', 'US dollars', 'currently on the Mac', 'Team includes Starter', 'paid features work for {graceDays} days after the last successful check', 'One activated Mac per seat.', 'Each Team seat covers one activated Mac', 'answered first, within one business day'],
+ ['no trial period', 'US dollars', 'currently on the Mac', 'Team includes Pro', 'paid features work for {graceDays} days after the last successful check', 'One activated Mac per seat.', 'Each Team seat covers one activated Mac', 'answered first, within one business day'],
],
'Vietnamese' => [
- '/vi/pricing', 'vi', ["0\u{a0}US$", "24\u{a0}US$", "10\u{a0}US$", "5 seat: 50\u{a0}US$ mỗi năm"], "%s\u{a0}US$", ',',
+ '/vi/pricing', 'vi', ["0\u{a0}US$", "39\u{a0}US$", "10\u{a0}US$", "2 seat: 240\u{a0}US$ mỗi năm"], "%s\u{a0}US$", ',',
'Mọi gói trả phí đều được hoàn tiền trong vòng {days} ngày kể từ ngày mua, và mỗi lần gia hạn theo tháng hoặc theo năm trong vòng {days} ngày kể từ ngày tính phí.',
['Có', 'Không có'],
[],
@@ -294,7 +299,7 @@ function pricingExpectedPricePoints(string $pattern, string $decimal): array
}
});
-it('says who a Starter license is for wherever it says how many Macs', function (string $locale, string $person, ?string $cardPerson = null, ?string $detailPerson = null): void {
+it('says who a Pro license is for wherever it says how many Macs', function (string $locale, string $person, ?string $cardPerson = null, ?string $detailPerson = null): void {
// The rule is the terms' own: "Using a license".
expect(File::get(resource_path("data/legal/{$locale}/terms.md")))->toContain($person);
@@ -331,8 +336,8 @@ function pricingExpectedPricePoints(string $pattern, string $decimal): array
$content = pricingPageJson("content/{$locale}/pricing.json");
// Plan names stay English everywhere, as on the buy buttons.
- expect($content['seo']['title'])->not->toBe($content['header']['title'])->toContain('Starter')->toContain('Team');
- expect($content['seo']['description'])->toContain('Starter')->toContain('Team');
+ expect($content['seo']['title'])->not->toBe($content['header']['title'])->toContain('Pro')->toContain('Team');
+ expect($content['seo']['description'])->toContain('Pro')->toContain('Team');
expect($content['license']['items']['activate']['body'])->toContain($macsPage);
expect($content['billing']['portal'])->toContain("{$billing}");
diff --git a/tests/Feature/Seo/LlmsTxtTest.php b/tests/Feature/Seo/LlmsTxtTest.php
index 7a475999..7632920a 100644
--- a/tests/Feature/Seo/LlmsTxtTest.php
+++ b/tests/Feature/Seo/LlmsTxtTest.php
@@ -87,12 +87,14 @@ function llmsTxtUrl(string $line): string
});
-it('lists guides apart from release posts, newest first', function (): void {
+it('lists guides apart from release posts and news, newest first', function (): void {
$sections = llmsTxtSections((string) $this->get('/llms.txt')->getContent());
$posts = app(BlogService::class)->all('en');
$guides = array_values(array_filter($posts, fn(Post $post): bool => $post->kind() === Post::KIND_GUIDE));
$releases = array_values(array_filter($posts, fn(Post $post): bool => $post->kind() === Post::KIND_RELEASE));
+ // News and release posts share Optional, newest first.
+ $others = array_values(array_filter($posts, fn(Post $post): bool => $post->kind() !== Post::KIND_GUIDE));
expect($guides)->not->toBeEmpty()
->and($releases)->not->toBeEmpty();
@@ -100,8 +102,8 @@ function llmsTxtUrl(string $line): string
expect(array_map('llmsTxtUrl', $sections['Guides']))
->toBe(array_map(fn(Post $post): string => "https://tablepro.app/blog/{$post->slug}.md", $guides));
- expect(array_slice(array_map('llmsTxtUrl', $sections['Optional']), 0, count($releases)))
- ->toBe(array_map(fn(Post $post): string => "https://tablepro.app/blog/{$post->slug}.md", $releases));
+ expect(array_slice(array_map('llmsTxtUrl', $sections['Optional']), 0, count($others)))
+ ->toBe(array_map(fn(Post $post): string => "https://tablepro.app/blog/{$post->slug}.md", $others));
});
it('names a page by its title, and by its card title where the title has a slot', function (): void {
diff --git a/tests/js/compare-model.test.ts b/tests/js/compare-model.test.ts
index 35908431..253fe236 100644
--- a/tests/js/compare-model.test.ts
+++ b/tests/js/compare-model.test.ts
@@ -145,7 +145,7 @@ test('builds TablePro’s price cell from pricing.json, never a bare "Free"', ()
assert.equal(cell.lines[0].text, hub.en.labels.tablepro.priceFree);
assert.equal(
cell.lines[1].text,
- `Starter: $${pricing.tiers.starter.prices.monthly} a month, $${pricing.tiers.starter.prices.yearly} a year or $${pricing.tiers.starter.prices.lifetime} once, for up to ${pricing.tiers.starter.activations} devices`,
+ `Pro: $${pricing.tiers.starter.prices.monthly} a month, $${pricing.tiers.starter.prices.yearly} a year or $${pricing.tiers.starter.prices.lifetime} once, for up to ${pricing.tiers.starter.activations} devices`,
);
assert.ok(pricing.tiers.starter.activations > 1, 'the sentence above is the plural form');
assert.match(cell.lines[2].text, new RegExp(`at least ${pricing.tiers.team.seats.min} seats$`));