diff --git a/docs/pr/credits-ui.png b/docs/pr/credits-ui.png new file mode 100644 index 0000000000..e886669c09 Binary files /dev/null and b/docs/pr/credits-ui.png differ diff --git a/messages/de.json b/messages/de.json index 036030671e..92bc8eb783 100644 --- a/messages/de.json +++ b/messages/de.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Kredite statt eines Plans verwenden", "credits-pagination-label": "Seite {current} / {total}", "credits-pricing-bandwidth-subtitle": "Pro GiB, das zusätzlich zu deinem Tarif ausgeliefert wird.", - "credits-pricing-bandwidth-tier-first": "Erste 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 pro GiB", - "credits-pricing-bandwidth-tier-next-13tb": "Nächste 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 pro GiB", - "credits-pricing-bandwidth-tier-next-1tb": "Nächste 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 pro GiB", - "credits-pricing-bandwidth-tier-next-38tb": "Nächste 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 pro GiB", - "credits-pricing-bandwidth-tier-next-4tb": "Nächste 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 pro GiB", - "credits-pricing-bandwidth-tier-next-64tb": "Nächste 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 pro GiB", - "credits-pricing-bandwidth-tier-next-6tb": "Nächste 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 pro GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Über 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 pro GiB", "credits-pricing-bandwidth-title": "Bandbreite (GiB)", "credits-pricing-build-subtitle": "Pro Build-Minute, die über das in deinem Tarif enthaltene hinausgeht.", - "credits-pricing-build-tier-first-100": "Erste 100 Minuten", - "credits-pricing-build-tier-first-100-price": "$0.50 pro Minute", - "credits-pricing-build-tier-next-400": "Nächste 400 Minuten", - "credits-pricing-build-tier-next-400-price": "$0.45 pro Minute", - "credits-pricing-build-tier-next-4000": "Nächste 4.000 Minuten", - "credits-pricing-build-tier-next-4000-price": "$0.35 pro Minute", - "credits-pricing-build-tier-next-500": "Nächste 500 Minuten", - "credits-pricing-build-tier-next-500-price": "$0.40 pro Minute", - "credits-pricing-build-tier-next-5000": "Nächste 5.000 Minuten", - "credits-pricing-build-tier-next-5000-price": "$0.30 pro Minute", - "credits-pricing-build-tier-over-10000": "Über 10.000 Minuten", - "credits-pricing-build-tier-over-10000-price": "$0.25 pro Minute", "credits-pricing-build-title": "Build-Zeit (Minuten)", "credits-pricing-description": "Credits decken die Nutzung über die Grenzen deines Tarifs hinaus ab. Nutze diese Stufen, um abzuschätzen, wie viele du kaufen solltest.", "credits-pricing-disclaimer": "Credits decken die Nutzung über die im Tarif enthaltenen Grenzen hinaus ab. Credits werden im Voraus bezahlt und sind 12 Monate gültig.", "credits-pricing-footnote": "* Speicher wird pro GiB und Stunde berechnet.", "credits-pricing-mau-subtitle": "Pro Gerät, das sich mindestens einmal im Monat meldet.", - "credits-pricing-mau-tier-first": "Erste 1 Mio.", - "credits-pricing-mau-tier-first-price": "$0.003 pro MAU", - "credits-pricing-mau-tier-next-10m": "Nächste 10 Mio.", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 pro MAU", - "credits-pricing-mau-tier-next-15m": "Nächste 15 Mio.", - "credits-pricing-mau-tier-next-15m-price": "$0.001 pro MAU", - "credits-pricing-mau-tier-next-2m": "Nächste 2 Mio.", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 pro MAU", - "credits-pricing-mau-tier-next-5m": "Nächste 5 Mio.", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 pro MAU", - "credits-pricing-mau-tier-next-60m": "Nächste 60 Mio.", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 pro MAU", - "credits-pricing-mau-tier-next-7m": "Nächste 7 Mio.", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 pro MAU", - "credits-pricing-mau-tier-over-100m": "Über 100 Mio.", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 pro MAU", "credits-pricing-mau-title": "Monatlich aktive Nutzer (MAU)", "credits-pricing-storage-subtitle": "Pro GiB, das Speicherplatz für deine Releases belegt.", - "credits-pricing-storage-tier-first": "Erste 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 pro GiB", - "credits-pricing-storage-tier-next-187gib": "Nächste 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 pro GiB", - "credits-pricing-storage-tier-next-19gib": "Nächste 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 pro GiB", - "credits-pricing-storage-tier-next-38gib": "Nächste 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 pro GiB", - "credits-pricing-storage-tier-next-390gib": "Nächste 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 pro GiB", - "credits-pricing-storage-tier-next-5gib": "Nächste 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 pro GiB", - "credits-pricing-storage-tier-next-640gib": "Nächste 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 pro GiB", - "credits-pricing-storage-tier-over-1tb": "Über 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 pro GiB", "credits-pricing-storage-title": "Speicher (GiB)", "credits-pricing-title": "Credit-Preise", "credits-top-up-quantity-help": "Du kannst die Menge während des Stripe-Checkouts direkt anpassen.", diff --git a/messages/en.json b/messages/en.json index 5deb1cebbe..e2d95e7b9a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -634,78 +634,26 @@ "created-channel-within-7-days": "Created Channel (within 7 days)", "demo-apps-created": "Demo Apps Created", "credits-pagination-label": "Page {current} / {total}", + "credits-plan-overage": "{included}, then {price}", + "credits-pricing-price": "{price} {unit}", "credits-pricing-bandwidth-subtitle": "Per GiB delivered beyond what is included with your plan.", - "credits-pricing-bandwidth-tier-first": "First 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 per GiB", - "credits-pricing-bandwidth-tier-next-13tb": "Next 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 per GiB", - "credits-pricing-bandwidth-tier-next-1tb": "Next 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 per GiB", - "credits-pricing-bandwidth-tier-next-38tb": "Next 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 per GiB", - "credits-pricing-bandwidth-tier-next-4tb": "Next 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 per GiB", - "credits-pricing-bandwidth-tier-next-64tb": "Next 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 per GiB", - "credits-pricing-bandwidth-tier-next-6tb": "Next 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 per GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Over 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 per GiB", "credits-pricing-bandwidth-title": "Bandwidth (GiB)", "credits-pricing-build-subtitle": "Per minute spent building beyond what is included with your plan.", - "credits-pricing-build-tier-first-100": "First 100 minutes", - "credits-pricing-build-tier-first-100-price": "$0.50 per minute", - "credits-pricing-build-tier-next-400": "Next 400 minutes", - "credits-pricing-build-tier-next-400-price": "$0.45 per minute", - "credits-pricing-build-tier-next-4000": "Next 4,000 minutes", - "credits-pricing-build-tier-next-4000-price": "$0.35 per minute", - "credits-pricing-build-tier-next-500": "Next 500 minutes", - "credits-pricing-build-tier-next-500-price": "$0.40 per minute", - "credits-pricing-build-tier-next-5000": "Next 5,000 minutes", - "credits-pricing-build-tier-next-5000-price": "$0.30 per minute", - "credits-pricing-build-tier-over-10000": "Over 10,000 minutes", - "credits-pricing-build-tier-over-10000-price": "$0.25 per minute", "credits-pricing-build-title": "Build time (minutes)", "credits-pricing-description": "Credits cover usage beyond your plan limits. Use these tiers to estimate how many to purchase.", "credits-pricing-disclaimer": "Credits cover usage beyond included plan limits. Credits are prepaid and remain valid for 12 months.", "credits-pricing-footnote": "* Storage is calculated per GiB per hour.", "credits-pricing-mau-subtitle": "Per device checking in at least once during the month.", - "credits-pricing-mau-tier-first": "First 1M", - "credits-pricing-mau-tier-first-price": "$0.003 per MAU", - "credits-pricing-mau-tier-next-10m": "Next 10M", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 per MAU", - "credits-pricing-mau-tier-next-15m": "Next 15M", - "credits-pricing-mau-tier-next-15m-price": "$0.001 per MAU", - "credits-pricing-mau-tier-next-2m": "Next 2M", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 per MAU", - "credits-pricing-mau-tier-next-5m": "Next 5M", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 per MAU", - "credits-pricing-mau-tier-next-60m": "Next 60M", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 per MAU", - "credits-pricing-mau-tier-next-7m": "Next 7M", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 per MAU", - "credits-pricing-mau-tier-over-100m": "Over 100M", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 per MAU", "credits-pricing-mau-title": "Monthly Active Users (MAU)", "credits-pricing-storage-subtitle": "Per GiB occupying storage for your releases.", - "credits-pricing-storage-tier-first": "First 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 per GiB", - "credits-pricing-storage-tier-next-187gib": "Next 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 per GiB", - "credits-pricing-storage-tier-next-19gib": "Next 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 per GiB", - "credits-pricing-storage-tier-next-38gib": "Next 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 per GiB", - "credits-pricing-storage-tier-next-390gib": "Next 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 per GiB", - "credits-pricing-storage-tier-next-5gib": "Next 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 per GiB", - "credits-pricing-storage-tier-next-640gib": "Next 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 per GiB", - "credits-pricing-storage-tier-over-1tb": "Over 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 per GiB", "credits-pricing-storage-title": "Storage (GiB)", + "credits-pricing-tier-first": "Up to {to}", + "credits-pricing-tier-range": "From {from} to {to}", + "credits-pricing-tier-over": "Over {from}", "credits-pricing-title": "Credit pricing", + "credits-pricing-unit-per-gib": "per GiB", + "credits-pricing-unit-per-mau": "per MAU", + "credits-pricing-unit-per-minute": "per minute", "credits-top-up-quantity-help": "You can adjust the quantity directly in Stripe during checkout.", "credits-top-up-quantity-invalid": "Enter a valid credit amount before continuing.", "credits-top-up-quantity-label": "Credits to purchase", diff --git a/messages/es.json b/messages/es.json index 24014e403e..5c76d9e40f 100644 --- a/messages/es.json +++ b/messages/es.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Utilizar créditos en lugar de un plan", "credits-pagination-label": "Página {current} / {total}", "credits-pricing-bandwidth-subtitle": "Por GiB entregado más allá de lo incluido en tu plan.", - "credits-pricing-bandwidth-tier-first": "Primer 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 por GiB", - "credits-pricing-bandwidth-tier-next-13tb": "Siguientes 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 por GiB", - "credits-pricing-bandwidth-tier-next-1tb": "Siguiente 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 por GiB", - "credits-pricing-bandwidth-tier-next-38tb": "Siguientes 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 por GiB", - "credits-pricing-bandwidth-tier-next-4tb": "Siguientes 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 por GiB", - "credits-pricing-bandwidth-tier-next-64tb": "Siguientes 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 por GiB", - "credits-pricing-bandwidth-tier-next-6tb": "Siguientes 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 por GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Más de 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 por GiB", "credits-pricing-bandwidth-title": "Ancho de banda (GiB)", "credits-pricing-build-subtitle": "Por minuto dedicado a compilar más allá de lo incluido en tu plan.", - "credits-pricing-build-tier-first-100": "Primeros 100 minutos", - "credits-pricing-build-tier-first-100-price": "$0.50 por minuto", - "credits-pricing-build-tier-next-400": "Siguientes 400 minutos", - "credits-pricing-build-tier-next-400-price": "$0.45 por minuto", - "credits-pricing-build-tier-next-4000": "Siguientes 4.000 minutos", - "credits-pricing-build-tier-next-4000-price": "$0.35 por minuto", - "credits-pricing-build-tier-next-500": "Siguientes 500 minutos", - "credits-pricing-build-tier-next-500-price": "$0.40 por minuto", - "credits-pricing-build-tier-next-5000": "Siguientes 5.000 minutos", - "credits-pricing-build-tier-next-5000-price": "$0.30 por minuto", - "credits-pricing-build-tier-over-10000": "Más de 10.000 minutos", - "credits-pricing-build-tier-over-10000-price": "$0.25 por minuto", "credits-pricing-build-title": "Tiempo de compilación (minutos)", "credits-pricing-description": "Los créditos cubren el uso que supera los límites de tu plan. Usa estos niveles para estimar cuántos comprar.", "credits-pricing-disclaimer": "Los créditos cubren el uso que excede los límites incluidos en el plan. Los créditos se prepagan y son válidos durante 12 meses.", "credits-pricing-footnote": "* El almacenamiento se calcula por GiB por hora.", "credits-pricing-mau-subtitle": "Por dispositivo que se conecta al menos una vez durante el mes.", - "credits-pricing-mau-tier-first": "Primeros 1 M", - "credits-pricing-mau-tier-first-price": "$0.003 por MAU", - "credits-pricing-mau-tier-next-10m": "Siguientes 10 M", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 por MAU", - "credits-pricing-mau-tier-next-15m": "Siguientes 15 M", - "credits-pricing-mau-tier-next-15m-price": "$0.001 por MAU", - "credits-pricing-mau-tier-next-2m": "Siguientes 2 M", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 por MAU", - "credits-pricing-mau-tier-next-5m": "Siguientes 5 M", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 por MAU", - "credits-pricing-mau-tier-next-60m": "Siguientes 60 M", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 por MAU", - "credits-pricing-mau-tier-next-7m": "Siguientes 7 M", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 por MAU", - "credits-pricing-mau-tier-over-100m": "Más de 100 M", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 por MAU", "credits-pricing-mau-title": "Usuarios activos mensuales (MAU)", "credits-pricing-storage-subtitle": "Por GiB que ocupa almacenamiento para tus publicaciones.", - "credits-pricing-storage-tier-first": "Primer 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 por GiB", - "credits-pricing-storage-tier-next-187gib": "Siguientes 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 por GiB", - "credits-pricing-storage-tier-next-19gib": "Siguientes 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 por GiB", - "credits-pricing-storage-tier-next-38gib": "Siguientes 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 por GiB", - "credits-pricing-storage-tier-next-390gib": "Siguientes 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 por GiB", - "credits-pricing-storage-tier-next-5gib": "Siguientes 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 por GiB", - "credits-pricing-storage-tier-next-640gib": "Siguientes 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 por GiB", - "credits-pricing-storage-tier-over-1tb": "Más de 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 por GiB", "credits-pricing-storage-title": "Almacenamiento (GiB)", "credits-pricing-title": "Precios de créditos", "credits-top-up-quantity-help": "Puedes ajustar la cantidad directamente en Stripe durante el pago.", diff --git a/messages/fr.json b/messages/fr.json index 5ad59c8ef4..0446c134ce 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Utiliser des crédits au lieu d'un plan", "credits-pagination-label": "Page {current} / {total}", "credits-pricing-bandwidth-subtitle": "Par GiB livré au-delà de ce qui est inclus dans votre offre.", - "credits-pricing-bandwidth-tier-first": "Premiers 1 To", - "credits-pricing-bandwidth-tier-first-price": "$0.12 par GiB", - "credits-pricing-bandwidth-tier-next-13tb": "Prochains 13 To", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 par GiB", - "credits-pricing-bandwidth-tier-next-1tb": "Prochains 1 To", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 par GiB", - "credits-pricing-bandwidth-tier-next-38tb": "Prochains 38 To", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 par GiB", - "credits-pricing-bandwidth-tier-next-4tb": "Prochains 4 To", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 par GiB", - "credits-pricing-bandwidth-tier-next-64tb": "Prochains 64 To", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 par GiB", - "credits-pricing-bandwidth-tier-next-6tb": "Prochains 6 To", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 par GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Au-delà de 128 To", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 par GiB", "credits-pricing-bandwidth-title": "Bande passante (GiB)", "credits-pricing-build-subtitle": "Par minute de build dépassant ce qui est inclus dans votre offre.", - "credits-pricing-build-tier-first-100": "Premières 100 minutes", - "credits-pricing-build-tier-first-100-price": "$0.50 par minute", - "credits-pricing-build-tier-next-400": "Prochaines 400 minutes", - "credits-pricing-build-tier-next-400-price": "$0.45 par minute", - "credits-pricing-build-tier-next-4000": "Prochaines 4 000 minutes", - "credits-pricing-build-tier-next-4000-price": "$0.35 par minute", - "credits-pricing-build-tier-next-500": "Prochaines 500 minutes", - "credits-pricing-build-tier-next-500-price": "$0.40 par minute", - "credits-pricing-build-tier-next-5000": "Prochaines 5 000 minutes", - "credits-pricing-build-tier-next-5000-price": "$0.30 par minute", - "credits-pricing-build-tier-over-10000": "Au-delà de 10 000 minutes", - "credits-pricing-build-tier-over-10000-price": "$0.25 par minute", "credits-pricing-build-title": "Temps de build (minutes)", "credits-pricing-description": "Les crédits couvrent l'utilisation au-delà des limites de votre offre. Utilisez ces paliers pour estimer combien en acheter.", "credits-pricing-disclaimer": "Les crédits couvrent l'utilisation au-delà des limites incluses dans l'offre. Les crédits sont prépayés et restent valables pendant 12 mois.", "credits-pricing-footnote": "* Le stockage est calculé par GiB et par heure.", "credits-pricing-mau-subtitle": "Par appareil se connectant au moins une fois durant le mois.", - "credits-pricing-mau-tier-first": "Premiers 1 M", - "credits-pricing-mau-tier-first-price": "$0.003 par MAU", - "credits-pricing-mau-tier-next-10m": "Prochains 10 M", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 par MAU", - "credits-pricing-mau-tier-next-15m": "Prochains 15 M", - "credits-pricing-mau-tier-next-15m-price": "$0.001 par MAU", - "credits-pricing-mau-tier-next-2m": "Prochains 2 M", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 par MAU", - "credits-pricing-mau-tier-next-5m": "Prochains 5 M", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 par MAU", - "credits-pricing-mau-tier-next-60m": "Prochains 60 M", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 par MAU", - "credits-pricing-mau-tier-next-7m": "Prochains 7 M", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 par MAU", - "credits-pricing-mau-tier-over-100m": "Au-delà de 100 M", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 par MAU", "credits-pricing-mau-title": "Utilisateurs actifs mensuels (MAU)", "credits-pricing-storage-subtitle": "Par GiB occupant l'espace de stockage de vos mises en production.", - "credits-pricing-storage-tier-first": "Premiers 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 par GiB", - "credits-pricing-storage-tier-next-187gib": "Prochains 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 par GiB", - "credits-pricing-storage-tier-next-19gib": "Prochains 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 par GiB", - "credits-pricing-storage-tier-next-38gib": "Prochains 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 par GiB", - "credits-pricing-storage-tier-next-390gib": "Prochains 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 par GiB", - "credits-pricing-storage-tier-next-5gib": "Prochains 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 par GiB", - "credits-pricing-storage-tier-next-640gib": "Prochains 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 par GiB", - "credits-pricing-storage-tier-over-1tb": "Au-delà de 1 To", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 par GiB", "credits-pricing-storage-title": "Stockage (GiB)", "credits-pricing-title": "Tarification des crédits", "credits-top-up-quantity-help": "Vous pourrez ajuster la quantité directement dans Stripe au moment du paiement.", diff --git a/messages/hi.json b/messages/hi.json index 76a1678243..aa7ec47eb5 100644 --- a/messages/hi.json +++ b/messages/hi.json @@ -590,75 +590,15 @@ "credits-only-info-title": "योजना के बजाय क्रेडिट का उपयोग करना", "credits-pagination-label": "पृष्ठ {current} / {total}", "credits-pricing-bandwidth-subtitle": "आपके प्लान में शामिल सीमा से अधिक डिलीवर किए गए प्रति GiB पर।", - "credits-pricing-bandwidth-tier-first": "पहला 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 प्रति GiB", - "credits-pricing-bandwidth-tier-next-13tb": "अगले 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 प्रति GiB", - "credits-pricing-bandwidth-tier-next-1tb": "अगला 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 प्रति GiB", - "credits-pricing-bandwidth-tier-next-38tb": "अगले 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 प्रति GiB", - "credits-pricing-bandwidth-tier-next-4tb": "अगले 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 प्रति GiB", - "credits-pricing-bandwidth-tier-next-64tb": "अगले 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 प्रति GiB", - "credits-pricing-bandwidth-tier-next-6tb": "अगले 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 प्रति GiB", - "credits-pricing-bandwidth-tier-over-128tb": "128 TB से अधिक", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 प्रति GiB", "credits-pricing-bandwidth-title": "बैंडविड्थ (GiB)", "credits-pricing-build-subtitle": "आपकी योजना में शामिल सीमा से परे किए गए हर बिल्ड मिनट पर।", - "credits-pricing-build-tier-first-100": "पहले 100 मिनट", - "credits-pricing-build-tier-first-100-price": "$0.50 प्रति मिनट", - "credits-pricing-build-tier-next-400": "अगले 400 मिनट", - "credits-pricing-build-tier-next-400-price": "$0.45 प्रति मिनट", - "credits-pricing-build-tier-next-4000": "अगले 4,000 मिनट", - "credits-pricing-build-tier-next-4000-price": "$0.35 प्रति मिनट", - "credits-pricing-build-tier-next-500": "अगले 500 मिनट", - "credits-pricing-build-tier-next-500-price": "$0.40 प्रति मिनट", - "credits-pricing-build-tier-next-5000": "अगले 5,000 मिनट", - "credits-pricing-build-tier-next-5000-price": "$0.30 प्रति मिनट", - "credits-pricing-build-tier-over-10000": "10,000 मिनट से अधिक", - "credits-pricing-build-tier-over-10000-price": "$0.25 प्रति मिनट", "credits-pricing-build-title": "बिल्ड समय (मिनट)", "credits-pricing-description": "क्रेडिट आपके प्लान की सीमा से अधिक उपयोग को कवर करते हैं। कितने क्रेडिट खरीदने हैं इसका अनुमान लगाने के लिए इन स्तरों का उपयोग करें।", "credits-pricing-disclaimer": "क्रेडिट आपके प्लान में शामिल सीमाओं से अधिक उपयोग को कवर करते हैं। क्रेडिट अग्रिम भुगतान किए जाते हैं और 12 महीनों तक मान्य रहते हैं।", "credits-pricing-footnote": "* स्टोरेज की गणना प्रति GiB प्रति घंटे के आधार पर होती है।", "credits-pricing-mau-subtitle": "प्रत्येक डिवाइस जो महीने में कम से कम एक बार चेक-इन करता है।", - "credits-pricing-mau-tier-first": "पहले 1 मिलियन", - "credits-pricing-mau-tier-first-price": "$0.003 प्रति MAU", - "credits-pricing-mau-tier-next-10m": "अगले 10 मिलियन", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 प्रति MAU", - "credits-pricing-mau-tier-next-15m": "अगले 15 मिलियन", - "credits-pricing-mau-tier-next-15m-price": "$0.001 प्रति MAU", - "credits-pricing-mau-tier-next-2m": "अगले 2 मिलियन", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 प्रति MAU", - "credits-pricing-mau-tier-next-5m": "अगले 5 मिलियन", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 प्रति MAU", - "credits-pricing-mau-tier-next-60m": "अगले 60 मिलियन", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 प्रति MAU", - "credits-pricing-mau-tier-next-7m": "अगले 7 मिलियन", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 प्रति MAU", - "credits-pricing-mau-tier-over-100m": "100 मिलियन से अधिक", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 प्रति MAU", "credits-pricing-mau-title": "मासिक सक्रिय उपयोगकर्ता (MAU)", "credits-pricing-storage-subtitle": "आपकी रिलीज़ के लिए स्टोरेज घेरने वाले प्रति GiB पर।", - "credits-pricing-storage-tier-first": "पहला 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 प्रति GiB", - "credits-pricing-storage-tier-next-187gib": "अगले 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 प्रति GiB", - "credits-pricing-storage-tier-next-19gib": "अगले 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 प्रति GiB", - "credits-pricing-storage-tier-next-38gib": "अगले 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 प्रति GiB", - "credits-pricing-storage-tier-next-390gib": "अगले 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 प्रति GiB", - "credits-pricing-storage-tier-next-5gib": "अगले 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 प्रति GiB", - "credits-pricing-storage-tier-next-640gib": "अगले 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 प्रति GiB", - "credits-pricing-storage-tier-over-1tb": "1 TB से अधिक", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 प्रति GiB", "credits-pricing-storage-title": "स्टोरेज (GiB)", "credits-pricing-title": "क्रेडिट मूल्य निर्धारण", "credits-top-up-quantity-help": "आप भुगतान करते समय Stripe में सीधे मात्रा समायोजित कर सकते हैं।", diff --git a/messages/id.json b/messages/id.json index 104682555c..24e25d03a1 100644 --- a/messages/id.json +++ b/messages/id.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Menggunakan kredit sebagai pengganti paket", "credits-pagination-label": "Halaman {current} / {total}", "credits-pricing-bandwidth-subtitle": "Per GiB yang dikirim melebihi batas paket Anda.", - "credits-pricing-bandwidth-tier-first": "1 TB pertama", - "credits-pricing-bandwidth-tier-first-price": "$0.12 per GiB", - "credits-pricing-bandwidth-tier-next-13tb": "13 TB berikutnya", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 per GiB", - "credits-pricing-bandwidth-tier-next-1tb": "1 TB berikutnya", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 per GiB", - "credits-pricing-bandwidth-tier-next-38tb": "38 TB berikutnya", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 per GiB", - "credits-pricing-bandwidth-tier-next-4tb": "4 TB berikutnya", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 per GiB", - "credits-pricing-bandwidth-tier-next-64tb": "64 TB berikutnya", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 per GiB", - "credits-pricing-bandwidth-tier-next-6tb": "6 TB berikutnya", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 per GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Di atas 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 per GiB", "credits-pricing-bandwidth-title": "Bandwidth (GiB)", "credits-pricing-build-subtitle": "Per menit waktu build di luar yang sudah termasuk dalam paket Anda.", - "credits-pricing-build-tier-first-100": "100 menit pertama", - "credits-pricing-build-tier-first-100-price": "$0.50 per menit", - "credits-pricing-build-tier-next-400": "400 menit berikutnya", - "credits-pricing-build-tier-next-400-price": "$0.45 per menit", - "credits-pricing-build-tier-next-4000": "4.000 menit berikutnya", - "credits-pricing-build-tier-next-4000-price": "$0.35 per menit", - "credits-pricing-build-tier-next-500": "500 menit berikutnya", - "credits-pricing-build-tier-next-500-price": "$0.40 per menit", - "credits-pricing-build-tier-next-5000": "5.000 menit berikutnya", - "credits-pricing-build-tier-next-5000-price": "$0.30 per menit", - "credits-pricing-build-tier-over-10000": "Di atas 10.000 menit", - "credits-pricing-build-tier-over-10000-price": "$0.25 per menit", "credits-pricing-build-title": "Waktu build (menit)", "credits-pricing-description": "Kredit menutup penggunaan yang melampaui batas paket Anda. Gunakan tingkatan ini untuk memperkirakan jumlah yang perlu dibeli.", "credits-pricing-disclaimer": "Kredit menutup penggunaan di luar batas paket yang disertakan. Kredit dibayar di muka dan berlaku selama 12 bulan.", "credits-pricing-footnote": "* Penyimpanan dihitung per GiB per jam.", "credits-pricing-mau-subtitle": "Per perangkat yang check-in setidaknya sekali dalam sebulan.", - "credits-pricing-mau-tier-first": "1 M pertama", - "credits-pricing-mau-tier-first-price": "$0.003 per MAU", - "credits-pricing-mau-tier-next-10m": "10 M berikutnya", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 per MAU", - "credits-pricing-mau-tier-next-15m": "15 M berikutnya", - "credits-pricing-mau-tier-next-15m-price": "$0.001 per MAU", - "credits-pricing-mau-tier-next-2m": "2 M berikutnya", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 per MAU", - "credits-pricing-mau-tier-next-5m": "5 M berikutnya", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 per MAU", - "credits-pricing-mau-tier-next-60m": "60 M berikutnya", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 per MAU", - "credits-pricing-mau-tier-next-7m": "7 M berikutnya", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 per MAU", - "credits-pricing-mau-tier-over-100m": "Di atas 100 M", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 per MAU", "credits-pricing-mau-title": "Pengguna aktif bulanan (MAU)", "credits-pricing-storage-subtitle": "Per GiB yang memakai penyimpanan untuk rilis Anda.", - "credits-pricing-storage-tier-first": "1 GiB pertama", - "credits-pricing-storage-tier-first-price": "$0.09 per GiB", - "credits-pricing-storage-tier-next-187gib": "187 GiB berikutnya", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 per GiB", - "credits-pricing-storage-tier-next-19gib": "19 GiB berikutnya", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 per GiB", - "credits-pricing-storage-tier-next-38gib": "38 GiB berikutnya", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 per GiB", - "credits-pricing-storage-tier-next-390gib": "390 GiB berikutnya", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 per GiB", - "credits-pricing-storage-tier-next-5gib": "5 GiB berikutnya", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 per GiB", - "credits-pricing-storage-tier-next-640gib": "640 GiB berikutnya", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 per GiB", - "credits-pricing-storage-tier-over-1tb": "Di atas 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 per GiB", "credits-pricing-storage-title": "Penyimpanan (GiB)", "credits-pricing-title": "Harga kredit", "credits-top-up-quantity-help": "Anda dapat menyesuaikan jumlah langsung di Stripe saat checkout.", diff --git a/messages/it.json b/messages/it.json index 12f365c860..4496a4edd5 100644 --- a/messages/it.json +++ b/messages/it.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Utilizzo di crediti invece di un piano", "credits-pagination-label": "Pagina {current} / {total}", "credits-pricing-bandwidth-subtitle": "Per GiB erogato oltre quanto incluso nel tuo piano.", - "credits-pricing-bandwidth-tier-first": "Primo 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 per GiB", - "credits-pricing-bandwidth-tier-next-13tb": "Successivi 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 per GiB", - "credits-pricing-bandwidth-tier-next-1tb": "Successivo 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 per GiB", - "credits-pricing-bandwidth-tier-next-38tb": "Successivi 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 per GiB", - "credits-pricing-bandwidth-tier-next-4tb": "Successivi 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 per GiB", - "credits-pricing-bandwidth-tier-next-64tb": "Successivi 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 per GiB", - "credits-pricing-bandwidth-tier-next-6tb": "Successivi 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 per GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Oltre 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 per GiB", "credits-pricing-bandwidth-title": "Banda (GiB)", "credits-pricing-build-subtitle": "Per ogni minuto di build oltre quanto incluso nel tuo piano.", - "credits-pricing-build-tier-first-100": "Primi 100 minuti", - "credits-pricing-build-tier-first-100-price": "$0.50 per minuto", - "credits-pricing-build-tier-next-400": "Prossimi 400 minuti", - "credits-pricing-build-tier-next-400-price": "$0.45 per minuto", - "credits-pricing-build-tier-next-4000": "Prossimi 4.000 minuti", - "credits-pricing-build-tier-next-4000-price": "$0.35 per minuto", - "credits-pricing-build-tier-next-500": "Prossimi 500 minuti", - "credits-pricing-build-tier-next-500-price": "$0.40 per minuto", - "credits-pricing-build-tier-next-5000": "Prossimi 5.000 minuti", - "credits-pricing-build-tier-next-5000-price": "$0.30 per minuto", - "credits-pricing-build-tier-over-10000": "Oltre 10.000 minuti", - "credits-pricing-build-tier-over-10000-price": "$0.25 per minuto", "credits-pricing-build-title": "Tempo di build (minuti)", "credits-pricing-description": "I crediti coprono l'utilizzo oltre i limiti del tuo piano. Usa questi livelli per stimare quanti acquistarne.", "credits-pricing-disclaimer": "I crediti coprono l'utilizzo oltre i limiti inclusi nel piano. I crediti sono prepagati e restano validi per 12 mesi.", "credits-pricing-footnote": "* L'archiviazione è calcolata per GiB per ora.", "credits-pricing-mau-subtitle": "Per dispositivo che si collega almeno una volta durante il mese.", - "credits-pricing-mau-tier-first": "Primi 1 M", - "credits-pricing-mau-tier-first-price": "$0.003 per MAU", - "credits-pricing-mau-tier-next-10m": "Successivi 10 M", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 per MAU", - "credits-pricing-mau-tier-next-15m": "Successivi 15 M", - "credits-pricing-mau-tier-next-15m-price": "$0.001 per MAU", - "credits-pricing-mau-tier-next-2m": "Successivi 2 M", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 per MAU", - "credits-pricing-mau-tier-next-5m": "Successivi 5 M", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 per MAU", - "credits-pricing-mau-tier-next-60m": "Successivi 60 M", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 per MAU", - "credits-pricing-mau-tier-next-7m": "Successivi 7 M", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 per MAU", - "credits-pricing-mau-tier-over-100m": "Oltre 100 M", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 per MAU", "credits-pricing-mau-title": "Utenti attivi mensili (MAU)", "credits-pricing-storage-subtitle": "Per GiB che occupa lo spazio di archiviazione delle tue release.", - "credits-pricing-storage-tier-first": "Primo 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 per GiB", - "credits-pricing-storage-tier-next-187gib": "Successivi 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 per GiB", - "credits-pricing-storage-tier-next-19gib": "Successivi 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 per GiB", - "credits-pricing-storage-tier-next-38gib": "Successivi 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 per GiB", - "credits-pricing-storage-tier-next-390gib": "Successivi 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 per GiB", - "credits-pricing-storage-tier-next-5gib": "Successivi 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 per GiB", - "credits-pricing-storage-tier-next-640gib": "Successivi 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 per GiB", - "credits-pricing-storage-tier-over-1tb": "Oltre 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 per GiB", "credits-pricing-storage-title": "Archiviazione (GiB)", "credits-pricing-title": "Prezzi dei crediti", "credits-top-up-quantity-help": "Puoi modificare la quantità direttamente su Stripe durante il checkout.", diff --git a/messages/ja.json b/messages/ja.json index 0e2fc79390..d794749546 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -590,75 +590,15 @@ "credits-only-info-title": "プランの代わりにクレジットを使う", "credits-pagination-label": "ページ {current} / {total}", "credits-pricing-bandwidth-subtitle": "プランに含まれる量を超えて配信されたGiBごと。", - "credits-pricing-bandwidth-tier-first": "最初の1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12/GiB", - "credits-pricing-bandwidth-tier-next-13tb": "次の13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055/GiB", - "credits-pricing-bandwidth-tier-next-1tb": "次の1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10/GiB", - "credits-pricing-bandwidth-tier-next-38tb": "次の38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04/GiB", - "credits-pricing-bandwidth-tier-next-4tb": "次の4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085/GiB", - "credits-pricing-bandwidth-tier-next-64tb": "次の64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03/GiB", - "credits-pricing-bandwidth-tier-next-6tb": "次の6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07/GiB", - "credits-pricing-bandwidth-tier-over-128tb": "128 TB超", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02/GiB", "credits-pricing-bandwidth-title": "帯域幅 (GiB)", "credits-pricing-build-subtitle": "プランに含まれる分を超えてビルドした時間の1分ごとに。", - "credits-pricing-build-tier-first-100": "最初の100分", - "credits-pricing-build-tier-first-100-price": "$0.50/分", - "credits-pricing-build-tier-next-400": "次の400分", - "credits-pricing-build-tier-next-400-price": "$0.45/分", - "credits-pricing-build-tier-next-4000": "次の4,000分", - "credits-pricing-build-tier-next-4000-price": "$0.35/分", - "credits-pricing-build-tier-next-500": "次の500分", - "credits-pricing-build-tier-next-500-price": "$0.40/分", - "credits-pricing-build-tier-next-5000": "次の5,000分", - "credits-pricing-build-tier-next-5000-price": "$0.30/分", - "credits-pricing-build-tier-over-10000": "10,000分以上", - "credits-pricing-build-tier-over-10000-price": "$0.25/分", "credits-pricing-build-title": "ビルド時間 (分)", "credits-pricing-description": "クレジットはプラン上限を超えた利用分をカバーします。必要な購入数の目安として以下の階層をご利用ください。", "credits-pricing-disclaimer": "クレジットはプランに含まれる上限を超えた利用を補います。クレジットは前払いで、12か月間有効です。", "credits-pricing-footnote": "* ストレージは GiB×時間で計算されます。", "credits-pricing-mau-subtitle": "月内に少なくとも1回チェックインする端末あたり。", - "credits-pricing-mau-tier-first": "最初の100万", - "credits-pricing-mau-tier-first-price": "$0.003/MAU", - "credits-pricing-mau-tier-next-10m": "次の1,000万", - "credits-pricing-mau-tier-next-10m-price": "$0.0011/MAU", - "credits-pricing-mau-tier-next-15m": "次の1,500万", - "credits-pricing-mau-tier-next-15m-price": "$0.001/MAU", - "credits-pricing-mau-tier-next-2m": "次の200万", - "credits-pricing-mau-tier-next-2m-price": "$0.0022/MAU", - "credits-pricing-mau-tier-next-5m": "次の500万", - "credits-pricing-mau-tier-next-5m-price": "$0.0014/MAU", - "credits-pricing-mau-tier-next-60m": "次の6,000万", - "credits-pricing-mau-tier-next-60m-price": "$0.0009/MAU", - "credits-pricing-mau-tier-next-7m": "次の700万", - "credits-pricing-mau-tier-next-7m-price": "$0.0016/MAU", - "credits-pricing-mau-tier-over-100m": "1億超", - "credits-pricing-mau-tier-over-100m-price": "$0.0007/MAU", "credits-pricing-mau-title": "月間アクティブユーザー(MAU)", "credits-pricing-storage-subtitle": "リリースを保存するために使用されるGiBごと。", - "credits-pricing-storage-tier-first": "最初の1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09/GiB", - "credits-pricing-storage-tier-next-187gib": "次の187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04/GiB", - "credits-pricing-storage-tier-next-19gib": "次の19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065/GiB", - "credits-pricing-storage-tier-next-38gib": "次の38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05/GiB", - "credits-pricing-storage-tier-next-390gib": "次の390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03/GiB", - "credits-pricing-storage-tier-next-5gib": "次の5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08/GiB", - "credits-pricing-storage-tier-next-640gib": "次の640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025/GiB", - "credits-pricing-storage-tier-over-1tb": "1 TB超", - "credits-pricing-storage-tier-over-1tb-price": "$0.021/GiB", "credits-pricing-storage-title": "ストレージ (GiB)", "credits-pricing-title": "クレジット料金", "credits-top-up-quantity-help": "決済時にStripe上で数量を直接調整できます。", diff --git a/messages/ko.json b/messages/ko.json index 9866d8661f..45ea99f2e2 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -590,75 +590,15 @@ "credits-only-info-title": "요금제 대신 크레딧 사용", "credits-pagination-label": "페이지 {current} / {total}", "credits-pricing-bandwidth-subtitle": "요금제 포함량을 초과해 전송된 GiB 기준입니다.", - "credits-pricing-bandwidth-tier-first": "처음 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 (GiB당)", - "credits-pricing-bandwidth-tier-next-13tb": "다음 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 (GiB당)", - "credits-pricing-bandwidth-tier-next-1tb": "다음 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 (GiB당)", - "credits-pricing-bandwidth-tier-next-38tb": "다음 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 (GiB당)", - "credits-pricing-bandwidth-tier-next-4tb": "다음 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 (GiB당)", - "credits-pricing-bandwidth-tier-next-64tb": "다음 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 (GiB당)", - "credits-pricing-bandwidth-tier-next-6tb": "다음 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 (GiB당)", - "credits-pricing-bandwidth-tier-over-128tb": "128 TB 초과", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 (GiB당)", "credits-pricing-bandwidth-title": "대역폭 (GiB)", "credits-pricing-build-subtitle": "요금제에 포함된 분량을 넘는 빌드 시간의 분당 요금입니다.", - "credits-pricing-build-tier-first-100": "처음 100분", - "credits-pricing-build-tier-first-100-price": "$0.50 (분당)", - "credits-pricing-build-tier-next-400": "다음 400분", - "credits-pricing-build-tier-next-400-price": "$0.45 (분당)", - "credits-pricing-build-tier-next-4000": "다음 4,000분", - "credits-pricing-build-tier-next-4000-price": "$0.35 (분당)", - "credits-pricing-build-tier-next-500": "다음 500분", - "credits-pricing-build-tier-next-500-price": "$0.40 (분당)", - "credits-pricing-build-tier-next-5000": "다음 5,000분", - "credits-pricing-build-tier-next-5000-price": "$0.30 (분당)", - "credits-pricing-build-tier-over-10000": "10,000분 초과", - "credits-pricing-build-tier-over-10000-price": "$0.25 (분당)", "credits-pricing-build-title": "빌드 시간 (분)", "credits-pricing-description": "크레딧은 요금제 한도를 초과한 사용량을 보완합니다. 아래 구간을 참고해 구매할 수량을 가늠하세요.", "credits-pricing-disclaimer": "크레딧은 요금제에 포함된 한도를 초과한 사용량을 보완합니다. 크레딧은 선불이며 12개월 동안 유효합니다.", "credits-pricing-footnote": "* 스토리지는 GiB·시간 기준으로 계산됩니다.", "credits-pricing-mau-subtitle": "월 동안 최소 한 번 체크인한 기기 기준입니다.", - "credits-pricing-mau-tier-first": "처음 100만", - "credits-pricing-mau-tier-first-price": "$0.003 (MAU당)", - "credits-pricing-mau-tier-next-10m": "다음 1,000만", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 (MAU당)", - "credits-pricing-mau-tier-next-15m": "다음 1,500만", - "credits-pricing-mau-tier-next-15m-price": "$0.001 (MAU당)", - "credits-pricing-mau-tier-next-2m": "다음 200만", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 (MAU당)", - "credits-pricing-mau-tier-next-5m": "다음 500만", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 (MAU당)", - "credits-pricing-mau-tier-next-60m": "다음 6,000만", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 (MAU당)", - "credits-pricing-mau-tier-next-7m": "다음 700만", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 (MAU당)", - "credits-pricing-mau-tier-over-100m": "1억 초과", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 (MAU당)", "credits-pricing-mau-title": "월간 활성 사용자(MAU)", "credits-pricing-storage-subtitle": "배포본이 차지하는 저장소 GiB 기준입니다.", - "credits-pricing-storage-tier-first": "처음 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 (GiB당)", - "credits-pricing-storage-tier-next-187gib": "다음 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 (GiB당)", - "credits-pricing-storage-tier-next-19gib": "다음 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 (GiB당)", - "credits-pricing-storage-tier-next-38gib": "다음 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 (GiB당)", - "credits-pricing-storage-tier-next-390gib": "다음 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 (GiB당)", - "credits-pricing-storage-tier-next-5gib": "다음 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 (GiB당)", - "credits-pricing-storage-tier-next-640gib": "다음 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 (GiB당)", - "credits-pricing-storage-tier-over-1tb": "1 TB 초과", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 (GiB당)", "credits-pricing-storage-title": "스토리지 (GiB)", "credits-pricing-title": "크레딧 요금", "credits-top-up-quantity-help": "결제 중 Stripe에서 수량을 바로 조정할 수 있습니다.", diff --git a/messages/pl.json b/messages/pl.json index 8c36159879..ebc394c138 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Korzystanie z kredytów zamiast planu", "credits-pagination-label": "Strona {current} / {total}", "credits-pricing-bandwidth-subtitle": "Za GiB dostarczony poza limitem planu.", - "credits-pricing-bandwidth-tier-first": "Pierwszy 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 za GiB", - "credits-pricing-bandwidth-tier-next-13tb": "Kolejne 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 za GiB", - "credits-pricing-bandwidth-tier-next-1tb": "Kolejny 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 za GiB", - "credits-pricing-bandwidth-tier-next-38tb": "Kolejne 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 za GiB", - "credits-pricing-bandwidth-tier-next-4tb": "Kolejne 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 za GiB", - "credits-pricing-bandwidth-tier-next-64tb": "Kolejne 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 za GiB", - "credits-pricing-bandwidth-tier-next-6tb": "Kolejne 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 za GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Powyżej 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 za GiB", "credits-pricing-bandwidth-title": "Przepustowość (GiB)", "credits-pricing-build-subtitle": "Za każdą minutę budowania poza tym, co obejmuje Twój plan.", - "credits-pricing-build-tier-first-100": "Pierwsze 100 minut", - "credits-pricing-build-tier-first-100-price": "$0.50 za minutę", - "credits-pricing-build-tier-next-400": "Kolejne 400 minut", - "credits-pricing-build-tier-next-400-price": "$0.45 za minutę", - "credits-pricing-build-tier-next-4000": "Kolejne 4 000 minut", - "credits-pricing-build-tier-next-4000-price": "$0.35 za minutę", - "credits-pricing-build-tier-next-500": "Kolejne 500 minut", - "credits-pricing-build-tier-next-500-price": "$0.40 za minutę", - "credits-pricing-build-tier-next-5000": "Kolejne 5 000 minut", - "credits-pricing-build-tier-next-5000-price": "$0.30 za minutę", - "credits-pricing-build-tier-over-10000": "Powyżej 10 000 minut", - "credits-pricing-build-tier-over-10000-price": "$0.25 za minutę", "credits-pricing-build-title": "Czas budowania (minuty)", "credits-pricing-description": "Kredyty pokrywają zużycie przekraczające limity planu. Skorzystaj z poniższych progów, aby oszacować, ile należy kupić.", "credits-pricing-disclaimer": "Kredyty pokrywają zużycie poza limitami zawartymi w planie. Kredyty są opłacane z góry i ważne przez 12 miesięcy.", "credits-pricing-footnote": "* Przechowywanie liczone jest w GiB na godzinę.", "credits-pricing-mau-subtitle": "Na urządzenie, które zgłasza się co najmniej raz w miesiącu.", - "credits-pricing-mau-tier-first": "Pierwsze 1 mln", - "credits-pricing-mau-tier-first-price": "$0.003 za MAU", - "credits-pricing-mau-tier-next-10m": "Kolejne 10 mln", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 za MAU", - "credits-pricing-mau-tier-next-15m": "Kolejne 15 mln", - "credits-pricing-mau-tier-next-15m-price": "$0.001 za MAU", - "credits-pricing-mau-tier-next-2m": "Kolejne 2 mln", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 za MAU", - "credits-pricing-mau-tier-next-5m": "Kolejne 5 mln", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 za MAU", - "credits-pricing-mau-tier-next-60m": "Kolejne 60 mln", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 za MAU", - "credits-pricing-mau-tier-next-7m": "Kolejne 7 mln", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 za MAU", - "credits-pricing-mau-tier-over-100m": "Powyżej 100 mln", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 za MAU", "credits-pricing-mau-title": "Miesięcznie aktywni użytkownicy (MAU)", "credits-pricing-storage-subtitle": "Za GiB zajmujący miejsce w magazynie wydań.", - "credits-pricing-storage-tier-first": "Pierwszy 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 za GiB", - "credits-pricing-storage-tier-next-187gib": "Kolejne 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 za GiB", - "credits-pricing-storage-tier-next-19gib": "Kolejne 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 za GiB", - "credits-pricing-storage-tier-next-38gib": "Kolejne 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 za GiB", - "credits-pricing-storage-tier-next-390gib": "Kolejne 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 za GiB", - "credits-pricing-storage-tier-next-5gib": "Kolejne 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 za GiB", - "credits-pricing-storage-tier-next-640gib": "Kolejne 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 za GiB", - "credits-pricing-storage-tier-over-1tb": "Powyżej 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 za GiB", "credits-pricing-storage-title": "Przechowywanie (GiB)", "credits-pricing-title": "Cennik kredytów", "credits-top-up-quantity-help": "Możesz dostosować ilość bezpośrednio w Stripe podczas płatności.", diff --git a/messages/pt-br.json b/messages/pt-br.json index 586c69a7b5..89408c0fe9 100644 --- a/messages/pt-br.json +++ b/messages/pt-br.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Usar créditos em vez de um plano", "credits-pagination-label": "Página {current} / {total}", "credits-pricing-bandwidth-subtitle": "Por GiB entregue além do que está incluído no seu plano.", - "credits-pricing-bandwidth-tier-first": "Primeiro 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 por GiB", - "credits-pricing-bandwidth-tier-next-13tb": "Próximos 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 por GiB", - "credits-pricing-bandwidth-tier-next-1tb": "Próximo 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 por GiB", - "credits-pricing-bandwidth-tier-next-38tb": "Próximos 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 por GiB", - "credits-pricing-bandwidth-tier-next-4tb": "Próximos 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 por GiB", - "credits-pricing-bandwidth-tier-next-64tb": "Próximos 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 por GiB", - "credits-pricing-bandwidth-tier-next-6tb": "Próximos 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 por GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Acima de 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 por GiB", "credits-pricing-bandwidth-title": "Largura de banda (GiB)", "credits-pricing-build-subtitle": "Por minuto gasto em build além do que está incluído no seu plano.", - "credits-pricing-build-tier-first-100": "Primeiros 100 minutos", - "credits-pricing-build-tier-first-100-price": "$0.50 por minuto", - "credits-pricing-build-tier-next-400": "Próximos 400 minutos", - "credits-pricing-build-tier-next-400-price": "$0.45 por minuto", - "credits-pricing-build-tier-next-4000": "Próximos 4.000 minutos", - "credits-pricing-build-tier-next-4000-price": "$0.35 por minuto", - "credits-pricing-build-tier-next-500": "Próximos 500 minutos", - "credits-pricing-build-tier-next-500-price": "$0.40 por minuto", - "credits-pricing-build-tier-next-5000": "Próximos 5.000 minutos", - "credits-pricing-build-tier-next-5000-price": "$0.30 por minuto", - "credits-pricing-build-tier-over-10000": "Acima de 10.000 minutos", - "credits-pricing-build-tier-over-10000-price": "$0.25 por minuto", "credits-pricing-build-title": "Tempo de build (minutos)", "credits-pricing-description": "Os créditos cobrem o uso além dos limites do seu plano. Use estes níveis para estimar quantos comprar.", "credits-pricing-disclaimer": "Os créditos cobrem o uso além dos limites incluídos no plano. Os créditos são pré-pagos e permanecem válidos por 12 meses.", "credits-pricing-footnote": "* O armazenamento é calculado por GiB por hora.", "credits-pricing-mau-subtitle": "Por dispositivo que se conecta pelo menos uma vez durante o mês.", - "credits-pricing-mau-tier-first": "Primeiros 1 M", - "credits-pricing-mau-tier-first-price": "$0.003 por MAU", - "credits-pricing-mau-tier-next-10m": "Próximos 10 M", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 por MAU", - "credits-pricing-mau-tier-next-15m": "Próximos 15 M", - "credits-pricing-mau-tier-next-15m-price": "$0.001 por MAU", - "credits-pricing-mau-tier-next-2m": "Próximos 2 M", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 por MAU", - "credits-pricing-mau-tier-next-5m": "Próximos 5 M", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 por MAU", - "credits-pricing-mau-tier-next-60m": "Próximos 60 M", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 por MAU", - "credits-pricing-mau-tier-next-7m": "Próximos 7 M", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 por MAU", - "credits-pricing-mau-tier-over-100m": "Acima de 100 M", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 por MAU", "credits-pricing-mau-title": "Usuários ativos mensais (MAU)", "credits-pricing-storage-subtitle": "Por GiB ocupando armazenamento para suas publicações.", - "credits-pricing-storage-tier-first": "Primeiro 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 por GiB", - "credits-pricing-storage-tier-next-187gib": "Próximos 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 por GiB", - "credits-pricing-storage-tier-next-19gib": "Próximos 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 por GiB", - "credits-pricing-storage-tier-next-38gib": "Próximos 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 por GiB", - "credits-pricing-storage-tier-next-390gib": "Próximos 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 por GiB", - "credits-pricing-storage-tier-next-5gib": "Próximos 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 por GiB", - "credits-pricing-storage-tier-next-640gib": "Próximos 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 por GiB", - "credits-pricing-storage-tier-over-1tb": "Acima de 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 por GiB", "credits-pricing-storage-title": "Armazenamento (GiB)", "credits-pricing-title": "Preços de créditos", "credits-top-up-quantity-help": "Você pode ajustar a quantidade diretamente no Stripe durante o checkout.", diff --git a/messages/ru.json b/messages/ru.json index d4bea8b0f1..3e6fbfa566 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Использование кредитов вместо плана", "credits-pagination-label": "Страница {current} / {total}", "credits-pricing-bandwidth-subtitle": "За GiB, переданный сверх объема, включенного в тариф.", - "credits-pricing-bandwidth-tier-first": "Первые 1 ТБ", - "credits-pricing-bandwidth-tier-first-price": "$0.12 за GiB", - "credits-pricing-bandwidth-tier-next-13tb": "Следующие 13 ТБ", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 за GiB", - "credits-pricing-bandwidth-tier-next-1tb": "Следующий 1 ТБ", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 за GiB", - "credits-pricing-bandwidth-tier-next-38tb": "Следующие 38 ТБ", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 за GiB", - "credits-pricing-bandwidth-tier-next-4tb": "Следующие 4 ТБ", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 за GiB", - "credits-pricing-bandwidth-tier-next-64tb": "Следующие 64 ТБ", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 за GiB", - "credits-pricing-bandwidth-tier-next-6tb": "Следующие 6 ТБ", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 за GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Свыше 128 ТБ", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 за GiB", "credits-pricing-bandwidth-title": "Пропускная способность (GiB)", "credits-pricing-build-subtitle": "За каждую минуту сборки сверх того, что включено в ваш тариф.", - "credits-pricing-build-tier-first-100": "Первые 100 минут", - "credits-pricing-build-tier-first-100-price": "$0.50 за минуту", - "credits-pricing-build-tier-next-400": "Следующие 400 минут", - "credits-pricing-build-tier-next-400-price": "$0.45 за минуту", - "credits-pricing-build-tier-next-4000": "Следующие 4 000 минут", - "credits-pricing-build-tier-next-4000-price": "$0.35 за минуту", - "credits-pricing-build-tier-next-500": "Следующие 500 минут", - "credits-pricing-build-tier-next-500-price": "$0.40 за минуту", - "credits-pricing-build-tier-next-5000": "Следующие 5 000 минут", - "credits-pricing-build-tier-next-5000-price": "$0.30 за минуту", - "credits-pricing-build-tier-over-10000": "Свыше 10 000 минут", - "credits-pricing-build-tier-over-10000-price": "$0.25 за минуту", "credits-pricing-build-title": "Время сборки (минуты)", "credits-pricing-description": "Кредиты покрывают использование сверх лимитов вашего тарифа. Используйте эти уровни, чтобы оценить, сколько необходимо приобрести.", "credits-pricing-disclaimer": "Кредиты покрывают использование сверх лимитов, включенных в тариф. Кредиты оплачиваются заранее и действуют 12 месяцев.", "credits-pricing-footnote": "* Хранилище рассчитывается по GiB за час.", "credits-pricing-mau-subtitle": "За устройство, которое подключается хотя бы раз в месяц.", - "credits-pricing-mau-tier-first": "Первые 1 млн", - "credits-pricing-mau-tier-first-price": "$0.003 за MAU", - "credits-pricing-mau-tier-next-10m": "Следующие 10 млн", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 за MAU", - "credits-pricing-mau-tier-next-15m": "Следующие 15 млн", - "credits-pricing-mau-tier-next-15m-price": "$0.001 за MAU", - "credits-pricing-mau-tier-next-2m": "Следующие 2 млн", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 за MAU", - "credits-pricing-mau-tier-next-5m": "Следующие 5 млн", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 за MAU", - "credits-pricing-mau-tier-next-60m": "Следующие 60 млн", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 за MAU", - "credits-pricing-mau-tier-next-7m": "Следующие 7 млн", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 за MAU", - "credits-pricing-mau-tier-over-100m": "Свыше 100 млн", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 за MAU", "credits-pricing-mau-title": "Ежемесячно активные пользователи (MAU)", "credits-pricing-storage-subtitle": "За GiB, занимающий место в хранилище ваших релизов.", - "credits-pricing-storage-tier-first": "Первые 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 за GiB", - "credits-pricing-storage-tier-next-187gib": "Следующие 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 за GiB", - "credits-pricing-storage-tier-next-19gib": "Следующие 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 за GiB", - "credits-pricing-storage-tier-next-38gib": "Следующие 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 за GiB", - "credits-pricing-storage-tier-next-390gib": "Следующие 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 за GiB", - "credits-pricing-storage-tier-next-5gib": "Следующие 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 за GiB", - "credits-pricing-storage-tier-next-640gib": "Следующие 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 за GiB", - "credits-pricing-storage-tier-over-1tb": "Свыше 1 ТБ", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 за GiB", "credits-pricing-storage-title": "Хранилище (GiB)", "credits-pricing-title": "Цены на кредиты", "credits-top-up-quantity-help": "Вы можете изменить количество прямо в Stripe во время оплаты.", diff --git a/messages/tr.json b/messages/tr.json index 695566dfb1..b25251b271 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Plan yerine kredi kullanma", "credits-pagination-label": "Sayfa {current} / {total}", "credits-pricing-bandwidth-subtitle": "Planınıza dahil olanın ötesinde teslim edilen GiB başına.", - "credits-pricing-bandwidth-tier-first": "İlk 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 GiB başına", - "credits-pricing-bandwidth-tier-next-13tb": "Sonraki 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 GiB başına", - "credits-pricing-bandwidth-tier-next-1tb": "Sonraki 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 GiB başına", - "credits-pricing-bandwidth-tier-next-38tb": "Sonraki 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 GiB başına", - "credits-pricing-bandwidth-tier-next-4tb": "Sonraki 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 GiB başına", - "credits-pricing-bandwidth-tier-next-64tb": "Sonraki 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 GiB başına", - "credits-pricing-bandwidth-tier-next-6tb": "Sonraki 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 GiB başına", - "credits-pricing-bandwidth-tier-over-128tb": "128 TB üzeri", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 GiB başına", "credits-pricing-bandwidth-title": "Bant genişliği (GiB)", "credits-pricing-build-subtitle": "Planınıza dahil olanın ötesinde yapılan build süresi için dakika başına.", - "credits-pricing-build-tier-first-100": "İlk 100 dakika", - "credits-pricing-build-tier-first-100-price": "$0.50 dakika başına", - "credits-pricing-build-tier-next-400": "Sonraki 400 dakika", - "credits-pricing-build-tier-next-400-price": "$0.45 dakika başına", - "credits-pricing-build-tier-next-4000": "Sonraki 4.000 dakika", - "credits-pricing-build-tier-next-4000-price": "$0.35 dakika başına", - "credits-pricing-build-tier-next-500": "Sonraki 500 dakika", - "credits-pricing-build-tier-next-500-price": "$0.40 dakika başına", - "credits-pricing-build-tier-next-5000": "Sonraki 5.000 dakika", - "credits-pricing-build-tier-next-5000-price": "$0.30 dakika başına", - "credits-pricing-build-tier-over-10000": "10.000 dakikanın üzerinde", - "credits-pricing-build-tier-over-10000-price": "$0.25 dakika başına", "credits-pricing-build-title": "Build süresi (dakika)", "credits-pricing-description": "Krediler, plan limitinizi aşan kullanımı karşılar. Kaç adet almanız gerektiğini tahmin etmek için bu kademeleri kullanın.", "credits-pricing-disclaimer": "Krediler, plana dahil edilen limitlerin üzerindeki kullanımı karşılar. Krediler peşin ödenir ve 12 ay boyunca geçerlidir.", "credits-pricing-footnote": "* Depolama, GiB başına saat olarak hesaplanır.", "credits-pricing-mau-subtitle": "Ay içinde en az bir kez oturum açan cihaz başına.", - "credits-pricing-mau-tier-first": "İlk 1 M", - "credits-pricing-mau-tier-first-price": "$0.003 MAU başına", - "credits-pricing-mau-tier-next-10m": "Sonraki 10 M", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 MAU başına", - "credits-pricing-mau-tier-next-15m": "Sonraki 15 M", - "credits-pricing-mau-tier-next-15m-price": "$0.001 MAU başına", - "credits-pricing-mau-tier-next-2m": "Sonraki 2 M", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 MAU başına", - "credits-pricing-mau-tier-next-5m": "Sonraki 5 M", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 MAU başına", - "credits-pricing-mau-tier-next-60m": "Sonraki 60 M", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 MAU başına", - "credits-pricing-mau-tier-next-7m": "Sonraki 7 M", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 MAU başına", - "credits-pricing-mau-tier-over-100m": "100 M üzeri", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 MAU başına", "credits-pricing-mau-title": "Aylık aktif kullanıcılar (MAU)", "credits-pricing-storage-subtitle": "Yayınlarınız için depolamayı kullanan GiB başına.", - "credits-pricing-storage-tier-first": "İlk 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 GiB başına", - "credits-pricing-storage-tier-next-187gib": "Sonraki 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 GiB başına", - "credits-pricing-storage-tier-next-19gib": "Sonraki 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 GiB başına", - "credits-pricing-storage-tier-next-38gib": "Sonraki 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 GiB başına", - "credits-pricing-storage-tier-next-390gib": "Sonraki 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 GiB başına", - "credits-pricing-storage-tier-next-5gib": "Sonraki 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 GiB başına", - "credits-pricing-storage-tier-next-640gib": "Sonraki 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 GiB başına", - "credits-pricing-storage-tier-over-1tb": "1 TB üzeri", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 GiB başına", "credits-pricing-storage-title": "Depolama (GiB)", "credits-pricing-title": "Kredi fiyatlandırması", "credits-top-up-quantity-help": "Ödeme sırasında Stripe üzerinden miktarı doğrudan ayarlayabilirsiniz.", diff --git a/messages/vi.json b/messages/vi.json index bca617e6ff..3e19f514e9 100644 --- a/messages/vi.json +++ b/messages/vi.json @@ -590,75 +590,15 @@ "credits-only-info-title": "Sử dụng tín dụng thay vì gói cước", "credits-pagination-label": "Trang {current} / {total}", "credits-pricing-bandwidth-subtitle": "Cho mỗi GiB phân phối vượt ngoài gói của bạn.", - "credits-pricing-bandwidth-tier-first": "1 TB đầu tiên", - "credits-pricing-bandwidth-tier-first-price": "$0.12 mỗi GiB", - "credits-pricing-bandwidth-tier-next-13tb": "13 TB tiếp theo", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 mỗi GiB", - "credits-pricing-bandwidth-tier-next-1tb": "1 TB tiếp theo", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 mỗi GiB", - "credits-pricing-bandwidth-tier-next-38tb": "38 TB tiếp theo", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 mỗi GiB", - "credits-pricing-bandwidth-tier-next-4tb": "4 TB tiếp theo", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 mỗi GiB", - "credits-pricing-bandwidth-tier-next-64tb": "64 TB tiếp theo", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 mỗi GiB", - "credits-pricing-bandwidth-tier-next-6tb": "6 TB tiếp theo", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 mỗi GiB", - "credits-pricing-bandwidth-tier-over-128tb": "Trên 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 mỗi GiB", "credits-pricing-bandwidth-title": "Băng thông (GiB)", "credits-pricing-build-subtitle": "Tính cho mỗi phút build vượt quá những gì gói của bạn bao gồm.", - "credits-pricing-build-tier-first-100": "100 phút đầu tiên", - "credits-pricing-build-tier-first-100-price": "$0.50 mỗi phút", - "credits-pricing-build-tier-next-400": "400 phút tiếp theo", - "credits-pricing-build-tier-next-400-price": "$0.45 mỗi phút", - "credits-pricing-build-tier-next-4000": "4.000 phút tiếp theo", - "credits-pricing-build-tier-next-4000-price": "$0.35 mỗi phút", - "credits-pricing-build-tier-next-500": "500 phút tiếp theo", - "credits-pricing-build-tier-next-500-price": "$0.40 mỗi phút", - "credits-pricing-build-tier-next-5000": "5.000 phút tiếp theo", - "credits-pricing-build-tier-next-5000-price": "$0.30 mỗi phút", - "credits-pricing-build-tier-over-10000": "Trên 10.000 phút", - "credits-pricing-build-tier-over-10000-price": "$0.25 mỗi phút", "credits-pricing-build-title": "Thời gian build (phút)", "credits-pricing-description": "Tín dụng dùng để chi trả phần sử dụng vượt giới hạn gói. Hãy dùng các bậc dưới đây để ước lượng số lượng cần mua.", "credits-pricing-disclaimer": "Tín dụng chi trả cho phần sử dụng vượt giới hạn gói. Tín dụng được trả trước và có hiệu lực trong 12 tháng.", "credits-pricing-footnote": "* Lưu trữ được tính theo GiB mỗi giờ.", "credits-pricing-mau-subtitle": "Tính cho mỗi thiết bị đăng ký ít nhất một lần trong tháng.", - "credits-pricing-mau-tier-first": "1 triệu đầu tiên", - "credits-pricing-mau-tier-first-price": "$0.003 mỗi MAU", - "credits-pricing-mau-tier-next-10m": "10 triệu tiếp theo", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 mỗi MAU", - "credits-pricing-mau-tier-next-15m": "15 triệu tiếp theo", - "credits-pricing-mau-tier-next-15m-price": "$0.001 mỗi MAU", - "credits-pricing-mau-tier-next-2m": "2 triệu tiếp theo", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 mỗi MAU", - "credits-pricing-mau-tier-next-5m": "5 triệu tiếp theo", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 mỗi MAU", - "credits-pricing-mau-tier-next-60m": "60 triệu tiếp theo", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 mỗi MAU", - "credits-pricing-mau-tier-next-7m": "7 triệu tiếp theo", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 mỗi MAU", - "credits-pricing-mau-tier-over-100m": "Trên 100 triệu", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 mỗi MAU", "credits-pricing-mau-title": "Người dùng hoạt động hàng tháng (MAU)", "credits-pricing-storage-subtitle": "Cho mỗi GiB chiếm dung lượng lưu trữ cho các bản phát hành của bạn.", - "credits-pricing-storage-tier-first": "1 GiB đầu tiên", - "credits-pricing-storage-tier-first-price": "$0.09 mỗi GiB", - "credits-pricing-storage-tier-next-187gib": "187 GiB tiếp theo", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 mỗi GiB", - "credits-pricing-storage-tier-next-19gib": "19 GiB tiếp theo", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 mỗi GiB", - "credits-pricing-storage-tier-next-38gib": "38 GiB tiếp theo", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 mỗi GiB", - "credits-pricing-storage-tier-next-390gib": "390 GiB tiếp theo", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 mỗi GiB", - "credits-pricing-storage-tier-next-5gib": "5 GiB tiếp theo", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 mỗi GiB", - "credits-pricing-storage-tier-next-640gib": "640 GiB tiếp theo", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 mỗi GiB", - "credits-pricing-storage-tier-over-1tb": "Trên 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 mỗi GiB", "credits-pricing-storage-title": "Lưu trữ (GiB)", "credits-pricing-title": "Bảng giá tín dụng", "credits-top-up-quantity-help": "Bạn có thể điều chỉnh số lượng trực tiếp trên Stripe khi thanh toán.", diff --git a/messages/zh-cn.json b/messages/zh-cn.json index 5ff46f364e..94f46a2f99 100644 --- a/messages/zh-cn.json +++ b/messages/zh-cn.json @@ -590,75 +590,15 @@ "credits-only-info-title": "用积分代替计划", "credits-pagination-label": "第 {current} / {total} 页", "credits-pricing-bandwidth-subtitle": "对超出套餐包含范围的每 GiB 计费。", - "credits-pricing-bandwidth-tier-first": "前 1 TB", - "credits-pricing-bandwidth-tier-first-price": "$0.12 每 GiB", - "credits-pricing-bandwidth-tier-next-13tb": "接下来的 13 TB", - "credits-pricing-bandwidth-tier-next-13tb-price": "$0.055 每 GiB", - "credits-pricing-bandwidth-tier-next-1tb": "接下来的 1 TB", - "credits-pricing-bandwidth-tier-next-1tb-price": "$0.10 每 GiB", - "credits-pricing-bandwidth-tier-next-38tb": "接下来的 38 TB", - "credits-pricing-bandwidth-tier-next-38tb-price": "$0.04 每 GiB", - "credits-pricing-bandwidth-tier-next-4tb": "接下来的 4 TB", - "credits-pricing-bandwidth-tier-next-4tb-price": "$0.085 每 GiB", - "credits-pricing-bandwidth-tier-next-64tb": "接下来的 64 TB", - "credits-pricing-bandwidth-tier-next-64tb-price": "$0.03 每 GiB", - "credits-pricing-bandwidth-tier-next-6tb": "接下来的 6 TB", - "credits-pricing-bandwidth-tier-next-6tb-price": "$0.07 每 GiB", - "credits-pricing-bandwidth-tier-over-128tb": "超过 128 TB", - "credits-pricing-bandwidth-tier-over-128tb-price": "$0.02 每 GiB", "credits-pricing-bandwidth-title": "带宽 (GiB)", "credits-pricing-build-subtitle": "超出套餐包含部分的构建时间按分钟计费。", - "credits-pricing-build-tier-first-100": "前 100 分钟", - "credits-pricing-build-tier-first-100-price": "$0.50 每分钟", - "credits-pricing-build-tier-next-400": "接下来的 400 分钟", - "credits-pricing-build-tier-next-400-price": "$0.45 每分钟", - "credits-pricing-build-tier-next-4000": "接下来的 4,000 分钟", - "credits-pricing-build-tier-next-4000-price": "$0.35 每分钟", - "credits-pricing-build-tier-next-500": "接下来的 500 分钟", - "credits-pricing-build-tier-next-500-price": "$0.40 每分钟", - "credits-pricing-build-tier-next-5000": "接下来的 5,000 分钟", - "credits-pricing-build-tier-next-5000-price": "$0.30 每分钟", - "credits-pricing-build-tier-over-10000": "超过 10,000 分钟", - "credits-pricing-build-tier-over-10000-price": "$0.25 每分钟", "credits-pricing-build-title": "构建时间(分钟)", "credits-pricing-description": "積分可覆盖超出套餐限制的用量。使用以下分级来估算需要购买的数量。", "credits-pricing-disclaimer": "積分用于覆盖超出套餐包含限制的用量。積分需预付并在 12 个月内有效。", "credits-pricing-footnote": "* 存储按每 GiB 每小时计算。", "credits-pricing-mau-subtitle": "每台设备在当月至少连接一次。", - "credits-pricing-mau-tier-first": "前 100 万", - "credits-pricing-mau-tier-first-price": "$0.003 每 MAU", - "credits-pricing-mau-tier-next-10m": "接下来的 1000 万", - "credits-pricing-mau-tier-next-10m-price": "$0.0011 每 MAU", - "credits-pricing-mau-tier-next-15m": "接下来的 1500 万", - "credits-pricing-mau-tier-next-15m-price": "$0.001 每 MAU", - "credits-pricing-mau-tier-next-2m": "接下来的 200 万", - "credits-pricing-mau-tier-next-2m-price": "$0.0022 每 MAU", - "credits-pricing-mau-tier-next-5m": "接下来的 500 万", - "credits-pricing-mau-tier-next-5m-price": "$0.0014 每 MAU", - "credits-pricing-mau-tier-next-60m": "接下来的 6000 万", - "credits-pricing-mau-tier-next-60m-price": "$0.0009 每 MAU", - "credits-pricing-mau-tier-next-7m": "接下来的 700 万", - "credits-pricing-mau-tier-next-7m-price": "$0.0016 每 MAU", - "credits-pricing-mau-tier-over-100m": "超过 1 亿", - "credits-pricing-mau-tier-over-100m-price": "$0.0007 每 MAU", "credits-pricing-mau-title": "月活跃用户(MAU)", "credits-pricing-storage-subtitle": "对你的发布所占用的每 GiB 存储计费。", - "credits-pricing-storage-tier-first": "前 1 GiB", - "credits-pricing-storage-tier-first-price": "$0.09 每 GiB", - "credits-pricing-storage-tier-next-187gib": "接下来的 187 GiB", - "credits-pricing-storage-tier-next-187gib-price": "$0.04 每 GiB", - "credits-pricing-storage-tier-next-19gib": "接下来的 19 GiB", - "credits-pricing-storage-tier-next-19gib-price": "$0.065 每 GiB", - "credits-pricing-storage-tier-next-38gib": "接下来的 38 GiB", - "credits-pricing-storage-tier-next-38gib-price": "$0.05 每 GiB", - "credits-pricing-storage-tier-next-390gib": "接下来的 390 GiB", - "credits-pricing-storage-tier-next-390gib-price": "$0.03 每 GiB", - "credits-pricing-storage-tier-next-5gib": "接下来的 5 GiB", - "credits-pricing-storage-tier-next-5gib-price": "$0.08 每 GiB", - "credits-pricing-storage-tier-next-640gib": "接下来的 640 GiB", - "credits-pricing-storage-tier-next-640gib-price": "$0.025 每 GiB", - "credits-pricing-storage-tier-over-1tb": "超过 1 TB", - "credits-pricing-storage-tier-over-1tb-price": "$0.021 每 GiB", "credits-pricing-storage-title": "存储 (GiB)", "credits-pricing-title": "积分定价", "credits-top-up-quantity-help": "结账时可在 Stripe 中直接调整数量。", diff --git a/src/modules/i18n.ts b/src/modules/i18n.ts index 46fae89757..8b6b25a44d 100644 --- a/src/modules/i18n.ts +++ b/src/modules/i18n.ts @@ -2,13 +2,15 @@ import type { Locale } from 'vue-i18n' import type { UserModule } from '~/types' import { createI18n } from 'vue-i18n' +const FALLBACK_LOCALE = 'en' as const + // Import i18n resources // https://vitejs.dev/guide/features.html#glob-import // // Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite export const i18n = createI18n({ legacy: false, - fallbackLocale: 'en', + fallbackLocale: FALLBACK_LOCALE, locale: '', messages: {}, }) @@ -39,6 +41,15 @@ export const languages = { const loadedLanguages: string[] = [] +async function ensureLanguageLoaded(lang: Locale) { + if (loadedLanguages.includes(lang)) + return + + const messages = await localesMap[lang]() + i18n.global.setLocaleMessage(lang, messages.default) + loadedLanguages.push(lang) +} + function setI18nLanguage(lang: Locale) { i18n.global.locale.value = lang as any localStorage.setItem('lang', lang) @@ -48,8 +59,11 @@ function setI18nLanguage(lang: Locale) { } export async function loadLanguageAsync(lang: string): Promise { + if (lang !== FALLBACK_LOCALE) + await ensureLanguageLoaded(FALLBACK_LOCALE) + // If the same language - if (i18n.global.locale.value === lang) + if (i18n.global.locale.value === lang && loadedLanguages.includes(lang)) return setI18nLanguage(lang) // If the language was already loaded @@ -57,9 +71,7 @@ export async function loadLanguageAsync(lang: string): Promise { return setI18nLanguage(lang) // If the language hasn't been loaded yet - const messages = await localesMap[lang]() - i18n.global.setLocaleMessage(lang, messages.default) - loadedLanguages.push(lang) + await ensureLanguageLoaded(lang as Locale) return setI18nLanguage(lang) } diff --git a/src/pages/settings/organization/Credits.vue b/src/pages/settings/organization/Credits.vue index 8709a1310b..2b6ed53d0b 100644 --- a/src/pages/settings/organization/Credits.vue +++ b/src/pages/settings/organization/Credits.vue @@ -1,4 +1,5 @@ diff --git a/src/pages/settings/organization/Plans.vue b/src/pages/settings/organization/Plans.vue index 3db3b52266..7046057d64 100644 --- a/src/pages/settings/organization/Plans.vue +++ b/src/pages/settings/organization/Plans.vue @@ -8,6 +8,7 @@ import { useRoute, useRouter } from 'vue-router' import { toast } from 'vue-sonner' import AdminOnlyModal from '~/components/AdminOnlyModal.vue' import CreditsCta from '~/components/CreditsCta.vue' +import { formatCreditPricingPrice, formatIncludedThenPrice } from '~/services/creditPricing' import { checkPermissions } from '~/services/permissions' import { openCheckout } from '~/services/stripe' import { getCreditUnitPricing, getCurrentPlanNameOrg, useSupabase } from '~/services/supabase' @@ -62,24 +63,32 @@ function planFeatures(plan: Database['public']['Tables']['plans']['Row']) { } } - const features = [ - `${plan.mau.toLocaleString()} ${t('mau')}`, - `${plan.storage.toLocaleString()} ${t('plan-storage')}`, - `${plan.bandwidth.toLocaleString()} ${t('plan-bandwidth')}`, - buildTimeDisplay, // Will be empty string if 0, filtered out below - ] + const mauFeature = creditUnitPrices.value.mau !== undefined + ? `${plan.mau.toLocaleString()} ${t('mau')} · ${formatIncludedThenPrice('mau', creditUnitPrices.value.mau, t)}` + : `${plan.mau.toLocaleString()} ${t('mau')}` - if (creditUnitPrices.value.mau) - features[0] += ` included, then $${creditUnitPrices.value.mau}/user` + const storageFeature = creditUnitPrices.value.storage !== undefined + ? `${plan.storage.toLocaleString()} ${t('plan-storage')} · ${formatIncludedThenPrice('storage', creditUnitPrices.value.storage, t)}` + : `${plan.storage.toLocaleString()} ${t('plan-storage')}` - if (creditUnitPrices.value.storage) - features[1] += ` included, then $${creditUnitPrices.value.storage} per GB` + const bandwidthFeature = creditUnitPrices.value.bandwidth !== undefined + ? `${plan.bandwidth.toLocaleString()} ${t('plan-bandwidth')} · ${formatIncludedThenPrice('bandwidth', creditUnitPrices.value.bandwidth, t)}` + : `${plan.bandwidth.toLocaleString()} ${t('plan-bandwidth')}` - if (creditUnitPrices.value.bandwidth) - features[2] += ` included, then $${creditUnitPrices.value.bandwidth} per GB` + const buildTimeFeature = buildTimeDisplay + ? creditUnitPrices.value.build_time !== undefined + ? `${buildTimeDisplay} · ${formatIncludedThenPrice('build_time', creditUnitPrices.value.build_time, t)}` + : buildTimeDisplay + : creditUnitPrices.value.build_time !== undefined + ? `${t('build-time')} · ${formatCreditPricingPrice('build_time', creditUnitPrices.value.build_time, t)}` + : '' - if (creditUnitPrices.value.build_time) - features[3] += ` included, then $${creditUnitPrices.value.build_time} per minute` + const features = [ + mauFeature, + storageFeature, + bandwidthFeature, + buildTimeFeature, + ] const planName = plan.name?.toLowerCase() ?? '' if (planName === 'solo') { diff --git a/src/pages/settings/organization/Usage.vue b/src/pages/settings/organization/Usage.vue index 950db7ed57..814790de4d 100644 --- a/src/pages/settings/organization/Usage.vue +++ b/src/pages/settings/organization/Usage.vue @@ -10,7 +10,7 @@ import { toast } from 'vue-sonner' import CreditsCta from '~/components/CreditsCta.vue' import Spinner from '~/components/Spinner.vue' import { bytesToGb } from '~/services/conversion' -import { getCreditUnitPricing, getCurrentPlanNameOrg, getPlans, getPlanUsagePercent, getTotalStorage, getUsageCreditDeductions } from '~/services/supabase' +import { calculateCreditCost, getCurrentPlanNameOrg, getPlans, getPlanUsagePercent, getTotalStorage, getUsageCreditDeductions } from '~/services/supabase' import { sendEvent } from '~/services/tracking' import { useDialogV2Store } from '~/stores/dialogv2' import { useMainStore } from '~/stores/main' @@ -18,7 +18,6 @@ import { useMainStore } from '~/stores/main' const { t } = useI18n() const plans = ref([]) -const creditUnitPrices = ref>>({}) const isLoading = ref(false) const initialLoad = ref(true) @@ -73,20 +72,6 @@ async function getUsage(orgId: string) { } detailPlanUsage = roundUsagePercents(detailPlanUsage) - const enterprise_base = { - mau: currentPlan?.mau ?? 0, - storage: currentPlan?.storage ?? 0, - bandwidth: currentPlan?.bandwidth ?? 0, - build_time: currentPlan?.build_time_unit ?? 0, - } - - const enterprise_units = { - mau: creditUnitPrices.value.mau ?? 0, - storage: creditUnitPrices.value.storage ?? 0, - bandwidth: creditUnitPrices.value.bandwidth ?? 0, - build_time: creditUnitPrices.value?.build_time ?? 0, - } - const creditDeductions = await getUsageCreditDeductions(orgId) const nowEndOfDay = dayjs().endOf('day') @@ -109,9 +94,9 @@ async function getUsage(orgId: string) { const relevantUsage = usageInCycle.length > 0 ? usageInCycle : usage - const totalCreditDeductions = creditDeductions.reduce((acc, entry) => { + const creditDeductionsInCycle = creditDeductions.filter((entry) => { if (entry.amount === null) - return acc + return false const entryStart = entry.billing_cycle_start ? dayjs(entry.billing_cycle_start).startOf('day') @@ -126,13 +111,14 @@ async function getUsage(orgId: string) { : null if (billingStart && entryEnd && entryEnd.isBefore(billingStart)) - return acc + return false if (billingEnd && entryStart && entryStart.isAfter(billingEnd)) - return acc + return false - return acc + Math.abs(entry.amount) - }, 0) + return true + }) + const totalCreditDeductions = creditDeductionsInCycle.reduce((acc, entry) => acc + Math.abs(entry.amount ?? 0), 0) const totalMau = relevantUsage.reduce((acc, entry) => acc + (entry.mau ?? 0), 0) const totalBandwidthBytes = relevantUsage.reduce((acc, entry) => acc + (entry.bandwidth ?? 0), 0) @@ -151,31 +137,30 @@ async function getUsage(orgId: string) { }) const basePrice = currentPlan?.price_m ?? 0 - - const calculatePrice = (total: number, base: number, unit: number) => { - if (unit <= 0) - return 0 - return total <= base ? 0 : (total - base) * unit + let estimatedUsagePrice: number | null = null + + if (currentPlan) { + try { + const overageCost = await calculateCreditCost({ + org_id: orgId, + mau: Math.max(totalMau - currentPlan.mau, 0), + bandwidth: Math.max(totalBandwidthBytes - Math.round(currentPlan.bandwidth * 1073741824), 0), + storage: Math.max(totalStorageBytes - Math.round(currentPlan.storage * 1073741824), 0), + build_time: Math.max(totalBuildTime - currentPlan.build_time_unit, 0), + }) + estimatedUsagePrice = roundNumber(overageCost.total_cost) + } + catch (err) { + console.error('Error estimating credit overage cost:', err) + } } - const estimatedUsagePrice = computed(() => { - const mauPrice = calculatePrice(totalMau, enterprise_base.mau, enterprise_units.mau) - const storagePrice = calculatePrice(totalStorage, enterprise_base.storage, enterprise_units.storage) - const bandwidthPrice = calculatePrice(totalBandwidth, enterprise_base.bandwidth, enterprise_units.bandwidth) - const buildTimePrice = calculatePrice(totalBuildTime, enterprise_base.build_time, enterprise_units.build_time) - const sum = mauPrice + storagePrice + bandwidthPrice + buildTimePrice - return roundNumber(sum) - }) - - const totalUsagePrice = computed(() => { - if (creditDeductions.length > 0) - return roundNumber(totalCreditDeductions) - return estimatedUsagePrice.value - }) - - const totalPrice = computed(() => { - return roundNumber(basePrice + totalUsagePrice.value) - }) + const totalUsagePrice = creditDeductionsInCycle.length > 0 + ? roundNumber(totalCreditDeductions) + : estimatedUsagePrice + const totalPrice = totalUsagePrice !== null && currentPlan + ? roundNumber(basePrice + totalUsagePrice) + : null return { currentPlan, @@ -185,7 +170,6 @@ async function getUsage(orgId: string) { totalBandwidth, totalStorage, totalBuildTime, - enterprise_units, detailPlanUsage, cycle: { subscription_anchor_start: dayjs(organizationStore.currentOrganization?.subscription_start).format('YYYY/MM/D'), @@ -205,6 +189,20 @@ function roundNumber(number: number) { return Math.round(number * 100) / 100 } +function formatCurrency(value?: number | null) { + if (typeof value !== 'number' || !Number.isFinite(value)) + return t('unknown') + + return `$${value.toLocaleString()}` +} + +function formatMonthlyPrice(value?: number | null) { + if (typeof value !== 'number' || !Number.isFinite(value)) + return t('unknown') + + return `$${value}/${t('mo')}` +} + function percent(usage: number, limit: number) { if (!Number.isFinite(usage) || !Number.isFinite(limit) || limit <= 0) return 0 @@ -308,16 +306,11 @@ async function loadData() { isLoading.value = true if (initialLoad.value) { - const [pls, pricing] = await Promise.all([ + const [pls] = await Promise.all([ getPlans(), - getCreditUnitPricing(gid || undefined), ]) plans.value.length = 0 plans.value.push(...pls) - creditUnitPrices.value = pricing - } - else if (!Object.keys(creditUnitPrices.value).length) { - creditUnitPrices.value = await getCreditUnitPricing(gid || undefined) } const usageDetails = await getUsage(gid) @@ -392,7 +385,7 @@ function nextRunDate() { {{ t('base') }}
- ${{ currentPlan?.price_m }}/{{ t('mo') }} + {{ formatMonthlyPrice(currentPlan?.price_m) }}
@@ -400,7 +393,7 @@ function nextRunDate() { {{ t('credits-used-in-period') }}
- ${{ planUsage?.totalUsagePrice.toLocaleString() }} + {{ formatCurrency(planUsage?.totalUsagePrice) }}
@@ -409,7 +402,7 @@ function nextRunDate() { {{ t('total') }}
- ${{ planUsage?.totalPrice.toLocaleString() }} + {{ formatCurrency(planUsage?.totalPrice) }}
diff --git a/src/services/creditPricing.ts b/src/services/creditPricing.ts new file mode 100644 index 0000000000..a53f2ff70c --- /dev/null +++ b/src/services/creditPricing.ts @@ -0,0 +1,136 @@ +import type { Database } from '~/types/supabase.types' + +export type CreditMetricType = Database['public']['Enums']['credit_metric_type'] + +export interface CreditPricingStep { + type: CreditMetricType + step_min: number + step_max: number + price_per_unit: number + unit_factor: number + org_id?: string | null +} + +type Translate = (key: string, values?: Record) => string + +export const creditPricingMetricOrder: CreditMetricType[] = ['mau', 'bandwidth', 'storage', 'build_time'] + +const creditPricingUnitLabelKeys: Record = { + mau: 'credits-pricing-unit-per-mau', + bandwidth: 'credits-pricing-unit-per-gib', + storage: 'credits-pricing-unit-per-gib', + build_time: 'credits-pricing-unit-per-minute', +} + +function getMetricOrder(metric: CreditMetricType) { + const index = creditPricingMetricOrder.indexOf(metric) + return index === -1 ? Number.MAX_SAFE_INTEGER : index +} + +function isOpenEndedTier(step: Pick) { + return !Number.isFinite(step.step_max) || step.step_max >= Number.MAX_SAFE_INTEGER +} + +function toBilledUnits(step: Pick, rawValue: number) { + const factor = step.unit_factor || 1 + return Math.ceil(rawValue / factor) +} + +function formatCreditTierAmount(metric: CreditMetricType, billedUnits: number, t: Translate, locale?: string) { + const formatter = new Intl.NumberFormat(locale, { + maximumFractionDigits: 0, + notation: metric === 'mau' ? 'compact' : 'standard', + compactDisplay: 'short', + }) + + if (metric === 'mau') + return formatter.format(billedUnits) + + if ((metric === 'bandwidth' || metric === 'storage') && billedUnits >= 1024 && billedUnits % 1024 === 0) + return `${formatter.format(billedUnits / 1024)} TB` + + if (metric === 'bandwidth' || metric === 'storage') + return `${formatter.format(billedUnits)} GiB` + + if (metric === 'build_time') + return t('minutes-short', { minutes: formatter.format(billedUnits) }) + + return formatter.format(billedUnits) +} + +export function sortCreditPricingSteps(steps: CreditPricingStep[]) { + return [...steps].sort((left, right) => { + const metricOrderDiff = getMetricOrder(left.type) - getMetricOrder(right.type) + if (metricOrderDiff !== 0) + return metricOrderDiff + + if (left.step_min !== right.step_min) + return left.step_min - right.step_min + + return left.step_max - right.step_max + }) +} + +export function getFirstTierCreditUnitPricing(steps: CreditPricingStep[]) { + return sortCreditPricingSteps(steps).reduce>>((pricing, step) => { + if (pricing[step.type] === undefined) + pricing[step.type] = step.price_per_unit + + return pricing + }, {}) +} + +export function formatCreditPriceValue(pricePerUnit: number, locale?: string) { + return new Intl.NumberFormat(locale, { + style: 'currency', + currency: 'USD', + minimumFractionDigits: 2, + maximumFractionDigits: 4, + }).format(pricePerUnit) +} + +export function formatCreditPricingPrice( + metric: CreditMetricType, + pricePerUnit: number, + t: Translate, + locale?: string, +) { + return t('credits-pricing-price', { + price: formatCreditPriceValue(pricePerUnit, locale), + unit: t(creditPricingUnitLabelKeys[metric]), + }) +} + +export function formatCreditPricingTierLabel( + step: Pick, + t: Translate, + locale?: string, +) { + const minUnits = toBilledUnits(step, step.step_min) + const maxUnits = toBilledUnits(step, step.step_max) + const openEnded = isOpenEndedTier(step) + + if (step.step_min === 0) { + return t('credits-pricing-tier-first', { + to: formatCreditTierAmount(step.type, maxUnits, t, locale), + }) + } + + if (openEnded) { + return t('credits-pricing-tier-over', { + from: formatCreditTierAmount(step.type, minUnits, t, locale), + }) + } + + return t('credits-pricing-tier-range', { + from: formatCreditTierAmount(step.type, minUnits, t, locale), + to: formatCreditTierAmount(step.type, maxUnits, t, locale), + }) +} + +export function formatIncludedThenPrice(metric: CreditMetricType, pricePerUnit: number, t: Translate, locale?: string) { + return t('credits-plan-overage', { + included: t('included-in-plan'), + price: formatCreditPricingPrice(metric, pricePerUnit, t, locale), + }) +} diff --git a/src/services/supabase.ts b/src/services/supabase.ts index d6151f33f1..1bc573a408 100644 --- a/src/services/supabase.ts +++ b/src/services/supabase.ts @@ -1,10 +1,12 @@ import type { SupabaseClient } from '@supabase/supabase-js' import type { RouteLocationNormalizedLoaded } from 'vue-router' +import type { CreditMetricType, CreditPricingStep } from './creditPricing' import type { Database } from '~/types/supabase.types' import { format, parse } from '@std/semver' import { createClient } from '@supabase/supabase-js' import subset from 'semver/ranges/subset' import { ref } from 'vue' +import { getFirstTierCreditUnitPricing, sortCreditPricingSteps } from './creditPricing' let supaClient: SupabaseClient = null as any @@ -385,38 +387,76 @@ export async function getPlans(): Promise> +export type CreditUnitPricing = Partial> export type UsageCreditLedgerRow = Database['public']['Views']['usage_credit_ledger']['Row'] -export async function getCreditUnitPricing(orgId?: string): Promise { - try { - const { data, error } = await useSupabase() - .from('capgo_credits_steps') - .select('type, price_per_unit, step_min, org_id') - .eq('step_min', 0) - .order('step_min', { ascending: true }) - - if (error || !data) - throw new Error(error?.message ?? 'Failed to fetch credit pricing') +export interface CreditTierUsage { + tier_id: number + step_min: number + step_max: number + unit_factor: number + units_used: number + price_per_unit: number + cost: number +} - const sortedSteps = [...data].sort((a, b) => { - const aOrgPriority = a.org_id && orgId && a.org_id === orgId ? 0 : 1 - const bOrgPriority = b.org_id && orgId && b.org_id === orgId ? 0 : 1 +export interface CreditMetricBreakdown { + cost: number + tiers: CreditTierUsage[] +} - if (aOrgPriority !== bOrgPriority) - return aOrgPriority - bOrgPriority +export interface CreditCostCalculationRequest { + mau: number + bandwidth: number + storage: number + build_time?: number + org_id?: string +} + +export interface CreditCostCalculationResponse { + total_cost: number + breakdown: Record + usage: { + mau: number + bandwidth: number + storage: number + build_time: number + } +} - return (a.step_min ?? 0) - (b.step_min ?? 0) +export async function getCreditPricingSteps(orgId?: string): Promise { + try { + const supabase = useSupabase() + const { data: currentSession } = await supabase.auth.getSession() + const endpoint = new URL(`${defaultApiHost}/private/credits`) + + if (orgId) + endpoint.searchParams.set('org_id', orgId) + + const response = await fetch(endpoint.toString(), { + headers: currentSession.session?.access_token + ? { + Authorization: `Bearer ${currentSession.session.access_token}`, + } + : undefined, }) - return sortedSteps.reduce((pricing, step) => { - const metric = step.type as Database['public']['Enums']['credit_metric_type'] + if (!response.ok) + throw new Error(`Failed to fetch credit pricing: HTTP ${response.status}`) - if (pricing[metric] === undefined) - pricing[metric] = step.price_per_unit + const data = await response.json() as CreditPricingStep[] + return sortCreditPricingSteps(data ?? []) + } + catch (err) { + console.error('getCreditPricingSteps error', err) + return [] + } +} - return pricing - }, {}) +export async function getCreditUnitPricing(orgId?: string): Promise { + try { + const steps = await getCreditPricingSteps(orgId) + return getFirstTierCreditUnitPricing(steps) } catch (err) { console.error('getCreditUnitPricing error', err) @@ -447,6 +487,20 @@ export async function getUsageCreditDeductions(orgId: string): Promise { + const response = await useSupabase().functions.invoke('private/credits', { + body: { + ...request, + build_time: request.build_time ?? 0, + }, + }) + + if (response.error) + throw new Error(response.error.message) + + return response.data as CreditCostCalculationResponse +} + interface PlanUsage { total_percent: number mau_percent: number diff --git a/supabase/functions/_backend/private/credits.ts b/supabase/functions/_backend/private/credits.ts index c835451459..540bfbc173 100644 --- a/supabase/functions/_backend/private/credits.ts +++ b/supabase/functions/_backend/private/credits.ts @@ -1,9 +1,9 @@ import type { Context } from 'hono' import type Stripe from 'stripe' -import type { MiddlewareKeyVariables } from '../utils/hono.ts' +import type { AuthInfo, MiddlewareKeyVariables } from '../utils/hono.ts' import { Hono } from 'hono/tiny' import { getFallbackCreditProductId } from '../utils/credits.ts' -import { middlewareAuth, parseBody, simpleError, useCors } from '../utils/hono.ts' +import { getClaimsFromJWT, middlewareAuth, parseBody, simpleError, useCors } from '../utils/hono.ts' import { cloudlog, cloudlogErr } from '../utils/logging.ts' import { checkPermission } from '../utils/rbac.ts' import { createOneTimeCheckout, getCreditCheckoutDetails, getStripe, isStripeEmulatorEnabled } from '../utils/stripe.ts' @@ -26,6 +26,8 @@ interface CostCalculationRequest { mau: number bandwidth: number // in bytes storage: number // in bytes + build_time?: number // in seconds + org_id?: string } interface TierUsage { @@ -33,7 +35,7 @@ interface TierUsage { step_min: number step_max: number unit_factor: number - units_used: number // billing units (GB for bandwidth/storage, count for MAU) + units_used: number // billing units (GiB/minutes/count) price_per_unit: number // Price per billing unit cost: number } @@ -49,11 +51,13 @@ interface CostCalculationResponse { mau: MetricBreakdown bandwidth: MetricBreakdown storage: MetricBreakdown + build_time: MetricBreakdown } usage: { mau: number bandwidth: number storage: number + build_time: number } } @@ -72,6 +76,150 @@ const MAX_TOP_UP_QUANTITY = 100000 type AppContext = Context +function sortCreditSteps(steps: CreditStep[]): CreditStep[] { + return [...steps].sort((a, b) => { + if (a.type !== b.type) + return a.type.localeCompare(b.type) + + if (a.step_min !== b.step_min) + return a.step_min - b.step_min + + return a.step_max - b.step_max + }) +} + +function subtractScopedRange(baseStep: CreditStep, scopedStep: CreditStep): CreditStep[] { + const overlapStart = Math.max(baseStep.step_min, scopedStep.step_min) + const overlapEnd = Math.min(baseStep.step_max, scopedStep.step_max) + + if (overlapStart >= overlapEnd) + return [baseStep] + + const remainingSteps: CreditStep[] = [] + + if (baseStep.step_min < overlapStart) { + remainingSteps.push({ + ...baseStep, + step_min: baseStep.step_min, + step_max: overlapStart, + }) + } + + if (overlapEnd < baseStep.step_max) { + remainingSteps.push({ + ...baseStep, + step_min: overlapEnd, + step_max: baseStep.step_max, + }) + } + + return remainingSteps +} + +function preferScopedCreditSteps(steps: CreditStep[], orgId?: string): CreditStep[] { + if (!orgId) + return sortCreditSteps(steps) + + const stepGroups = new Map() + + for (const step of steps) { + const currentGroup = stepGroups.get(step.type) ?? { global: [], scoped: [] } + + if (step.org_id === orgId) + currentGroup.scoped.push(step) + else + currentGroup.global.push(step) + + stepGroups.set(step.type, currentGroup) + } + + const normalizedSteps: CreditStep[] = [] + + for (const [, group] of stepGroups.entries()) { + const scopedSteps = sortCreditSteps(group.scoped) + if (scopedSteps.length === 0) { + normalizedSteps.push(...sortCreditSteps(group.global)) + continue + } + + let remainingGlobalSteps = sortCreditSteps(group.global) + for (const scopedStep of scopedSteps) + remainingGlobalSteps = remainingGlobalSteps.flatMap(globalStep => subtractScopedRange(globalStep, scopedStep)) + + normalizedSteps.push(...sortCreditSteps([...remainingGlobalSteps, ...scopedSteps])) + } + + return sortCreditSteps(normalizedSteps) +} + +async function requireOrgScopedPricingAccess(c: AppContext, orgId: string, authorization: string) { + c.set('authorization', authorization) + + const claims = await getClaimsFromJWT(c, authorization) + if (!claims?.sub) { + throw simpleError('not_authorized', 'Not authorized') + } + + c.set('auth', { + userId: claims.sub, + authType: 'jwt', + apikey: null, + jwt: authorization, + } satisfies AuthInfo) + + if (!await checkPermission(c, 'org.read', { orgId })) { + throw simpleError('not_authorized', 'Not authorized') + } +} + +async function getScopedCreditSteps(c: AppContext, orgId?: string): Promise { + const authorization = c.req.header('authorization') + ?? c.req.header('Authorization') + ?? c.get('authorization') + + let pricingClient: ReturnType | ReturnType | undefined + if (orgId) { + if (!authorization) { + throw simpleError('not_authorized', 'Not authorized') + } + + await requireOrgScopedPricingAccess(c, orgId, authorization) + pricingClient = supabaseClient(c, authorization) + } + else { + pricingClient = supabaseAdmin(c) + } + + if (!pricingClient) + throw simpleError('not_authorized', 'Not authorized') + + const scopedPricingClient = pricingClient + + const [globalCreditsResult, orgCreditsResult] = await Promise.all([ + scopedPricingClient + .from('capgo_credits_steps') + .select() + .is('org_id', null), + orgId + ? scopedPricingClient + .from('capgo_credits_steps') + .select() + .eq('org_id', orgId) + : Promise.resolve({ data: [] as CreditStep[], error: null }), + ]) + + const { data: globalCredits, error: globalCreditsError } = globalCreditsResult + const { data: orgCredits, error: orgCreditsError } = orgCreditsResult + + if (globalCreditsError || orgCreditsError) + throw simpleError('failed_to_fetch_pricing_data', 'Failed to fetch pricing data') + + return preferScopedCreditSteps([ + ...((globalCredits ?? []) as CreditStep[]), + ...((orgCredits ?? []) as CreditStep[]), + ], orgId) +} + async function getCreditTopUpProductId(c: AppContext, customerId: string, token: string): Promise<{ productId: string }> { const supabase = supabaseClient(c, token) const { data: stripeInfo, error: stripeInfoError } = await supabase @@ -269,39 +417,24 @@ export const app = new Hono() app.use('*', useCors) app.get('/', async (c) => { - try { - const { data: credits } = await supabaseAdmin(c) - .from('capgo_credits_steps') - .select() - .order('price_per_unit') - return c.json(credits ?? []) - } - catch (e) { - throw simpleError('failed_to_fetch_pricing_data', 'Failed to fetch pricing data', {}, e) - } + const orgId = c.req.query('org_id') ?? undefined + const credits = await getScopedCreditSteps(c as AppContext, orgId) + return c.json(credits) }) app.post('/', async (c) => { const body = await parseBody(c) - const { mau, bandwidth, storage } = body + const buildTime = Number(body.build_time ?? 0) + const { mau, bandwidth, org_id: orgId, storage } = body // Validate inputs if (mau === undefined || bandwidth === undefined || storage === undefined) { throw simpleError('missing_required_fields', 'Missing required fields: mau, bandwidth, storage') } + if (!Number.isFinite(buildTime) || buildTime < 0) + throw simpleError('invalid_build_time', 'build_time must be a non-negative number') - // Get pricing steps from database - const { data: credits, error } = await supabaseAdmin(c) - .from('capgo_credits_steps') - .select() - .order('type, step_min') - - if (error || !credits) { - throw simpleError('failed_to_fetch_pricing_data', 'Failed to fetch pricing data') - } - - // Type assertion for credits - const typedCredits = credits as CreditStep[] + const typedCredits = await getScopedCreditSteps(c as AppContext, orgId) // Calculate cost for each metric type with tier breakdown const calculateMetricCost = (value: number, type: string): MetricBreakdown => { @@ -376,8 +509,9 @@ app.post('/', async (c) => { const mauResult = calculateMetricCost(mau, 'mau') const bandwidthResult = calculateMetricCost(bandwidth, 'bandwidth') const storageResult = calculateMetricCost(storage, 'storage') + const buildTimeResult = calculateMetricCost(buildTime, 'build_time') - const totalCost = mauResult.cost + bandwidthResult.cost + storageResult.cost + const totalCost = mauResult.cost + bandwidthResult.cost + storageResult.cost + buildTimeResult.cost const response: CostCalculationResponse = { total_cost: totalCost, @@ -385,11 +519,13 @@ app.post('/', async (c) => { mau: mauResult, bandwidth: bandwidthResult, storage: storageResult, + build_time: buildTimeResult, }, usage: { mau, bandwidth, storage, + build_time: buildTime, }, } diff --git a/supabase/functions/_backend/public/webhooks/index.ts b/supabase/functions/_backend/public/webhooks/index.ts index 1db397f09d..19bb3f574a 100644 --- a/supabase/functions/_backend/public/webhooks/index.ts +++ b/supabase/functions/_backend/public/webhooks/index.ts @@ -26,6 +26,24 @@ function assertOrgWebhookScope( } } +async function assertWebhookOrgPolicy( + c: Context, + orgId: string, + apikey: Database['public']['Tables']['apikeys']['Row'], +): Promise { + const supabase = supabaseApikey(c, c.get('capgkey') as string) + const orgCheck = await apikeyHasOrgRightWithPolicy(c, apikey, orgId, supabase) + if (orgCheck.valid) { + return + } + + if (orgCheck.error === 'org_requires_expiring_key') { + throw quickError(401, 'org_requires_expiring_key', 'This organization requires API keys with an expiration date. Please use a different key or update this key with an expiration date.') + } + + throw simpleError('invalid_org_id', 'You can\'t access this organization', { org_id: orgId }) +} + /** * Shared permission check for webhook endpoints (API key auth) * Validates admin access to organization @@ -82,6 +100,8 @@ export async function checkWebhookPermissionV2( // If using API key, also check the key has org access if (auth.authType === 'apikey' && auth.apikey) { assertOrgWebhookScope(orgId, auth.apikey) + const policyKey = c.get('apikey') as Database['public']['Tables']['apikeys']['Row'] | undefined + await assertWebhookOrgPolicy(c, orgId, policyKey ?? auth.apikey) } } diff --git a/supabase/migrations/20260408134842_adjust_build_time_credit_pricing.sql b/supabase/migrations/20260408134842_adjust_build_time_credit_pricing.sql new file mode 100644 index 0000000000..131845a61a --- /dev/null +++ b/supabase/migrations/20260408134842_adjust_build_time_credit_pricing.sql @@ -0,0 +1,49 @@ +-- Move build minutes onto the same shared usage-credit ladder used for the other +-- overage metrics while lowering the effective build-minute pricing. +-- Keep the existing ranges and update rows in place so historical +-- usage_overage_events.credit_step_id links remain attached to their original +-- pricing tiers. + +WITH desired_steps (step_min, step_max, price_per_unit, unit_factor) AS ( + VALUES + (0::bigint, 6000::bigint, 0.16::double precision, 60::bigint), + (6000::bigint, 30000::bigint, 0.14::double precision, 60::bigint), + (30000::bigint, 60000::bigint, 0.12::double precision, 60::bigint), + (60000::bigint, 300000::bigint, 0.10::double precision, 60::bigint), + (300000::bigint, 600000::bigint, 0.09::double precision, 60::bigint), + (600000::bigint, 9223372036854775807::bigint, 0.08::double precision, 60::bigint) +), +updated_steps AS ( + UPDATE public.capgo_credits_steps AS existing + SET + price_per_unit = desired_steps.price_per_unit, + unit_factor = desired_steps.unit_factor + FROM desired_steps + WHERE existing.type = 'build_time' + AND existing.org_id IS NULL + AND existing.step_min = desired_steps.step_min + AND existing.step_max = desired_steps.step_max + RETURNING existing.step_min, existing.step_max +) +INSERT INTO public.capgo_credits_steps ( + type, + step_min, + step_max, + price_per_unit, + unit_factor, + org_id +) +SELECT + 'build_time', + desired_steps.step_min, + desired_steps.step_max, + desired_steps.price_per_unit, + desired_steps.unit_factor, + NULL +FROM desired_steps +WHERE NOT EXISTS ( + SELECT 1 + FROM updated_steps + WHERE updated_steps.step_min = desired_steps.step_min + AND updated_steps.step_max = desired_steps.step_max +); diff --git a/supabase/seed.sql b/supabase/seed.sql index 0fe8dc7429..29cd021b1f 100644 --- a/supabase/seed.sql +++ b/supabase/seed.sql @@ -217,12 +217,12 @@ BEGIN 1073741824, NULL ), -- 1280+ GiB - ('build_time', 0, 6000, 0.5, 60, NULL), -- 0-100 minutes (in seconds, displayed as minutes) - ('build_time', 6000, 30000, 0.45, 60, NULL), -- 100-500 minutes (in seconds, displayed as minutes) - ('build_time', 30000, 60000, 0.40, 60, NULL), -- 500-1000 minutes (in seconds, displayed as minutes) - ('build_time', 60000, 300000, 0.35, 60, NULL), -- 1000-5000 minutes (in seconds, displayed as minutes) - ('build_time', 300000, 600000, 0.30, 60, NULL), -- 5000-10000 minutes (in seconds, displayed as minutes) - ('build_time', 600000, 9223372036854775807, 0.25, 60, NULL); -- 10000+ minutes (in seconds, displayed as minutes) + ('build_time', 0, 6000, 0.16, 60, NULL), -- 0-100 minutes (in seconds, displayed as minutes) + ('build_time', 6000, 30000, 0.14, 60, NULL), -- 100-500 minutes (in seconds, displayed as minutes) + ('build_time', 30000, 60000, 0.12, 60, NULL), -- 500-1000 minutes (in seconds, displayed as minutes) + ('build_time', 60000, 300000, 0.10, 60, NULL), -- 1000-5000 minutes (in seconds, displayed as minutes) + ('build_time', 300000, 600000, 0.09, 60, NULL), -- 5000-10000 minutes (in seconds, displayed as minutes) + ('build_time', 600000, 9223372036854775807, 0.08, 60, NULL); -- 10000+ minutes (in seconds, displayed as minutes) INSERT INTO "storage"."buckets" ("id", "name", "owner", "created_at", "updated_at", "public") VALUES ('capgo', 'capgo', NULL, NOW(), NOW(), 't'), diff --git a/supabase/tests/32_test_usage_credits.sql b/supabase/tests/32_test_usage_credits.sql index 3c60f60624..e6f6c40b2d 100644 --- a/supabase/tests/32_test_usage_credits.sql +++ b/supabase/tests/32_test_usage_credits.sql @@ -1,6 +1,6 @@ BEGIN; -SELECT plan(19); +SELECT plan(23); DO $$ BEGIN @@ -41,6 +41,38 @@ SELECT 'top_up_usage_credits qualifies source_ref lookups to avoid ambiguity' ); +SELECT + results_eq( + $$SELECT price_per_unit + FROM public.capgo_credits_steps + WHERE type = 'build_time' + AND org_id IS NULL + AND step_min = 0 + ORDER BY step_max ASC + LIMIT 1$$, + $$VALUES (0.16::double precision)$$, + 'build_time credit pricing starts at $0.16 per minute' + ); + +SELECT + results_eq( + $$SELECT price_per_unit + FROM public.capgo_credits_steps + WHERE type = 'build_time' + AND org_id IS NULL + ORDER BY step_max DESC, step_min DESC + LIMIT 1$$, + $$VALUES (0.08::double precision)$$, + 'build_time credit pricing floors at $0.08 per minute' + ); + +SELECT + results_eq( + $$SELECT credits_required FROM public.calculate_credit_cost('build_time', 6000)$$, + $$VALUES (16.0::numeric)$$, + 'calculate_credit_cost prices build_time through the shared credit ladder' + ); + CREATE TEMP TABLE test_credit_context ( org_id uuid, grant_id uuid, @@ -152,6 +184,76 @@ FROM grant_insert, step_insert; +CREATE TEMP TABLE test_repricing_context ( + credit_step_id bigint, + overage_event_id uuid +) ON COMMIT DROP; + +WITH repricing_step AS ( + INSERT INTO public.capgo_credits_steps ( + type, + step_min, + step_max, + price_per_unit, + unit_factor, + org_id + ) + VALUES ( + 'build_time', + 900000000, + 900006000, + 0.5, + 60, + NULL + ) + RETURNING id +), +repricing_overage AS ( + INSERT INTO public.usage_overage_events ( + org_id, + metric, + overage_amount, + credits_estimated, + credits_debited, + credit_step_id, + billing_cycle_start, + billing_cycle_end, + details + ) + SELECT + (SELECT org_id FROM test_credit_context), + 'build_time'::public.credit_metric_type, + 6000, + 50, + 0, + repricing_step.id, + current_date, + current_date, + '{}'::jsonb + FROM repricing_step + RETURNING id, credit_step_id +) +INSERT INTO test_repricing_context (credit_step_id, overage_event_id) +SELECT credit_step_id, id +FROM repricing_overage; + +UPDATE public.capgo_credits_steps +SET + price_per_unit = 0.16, + unit_factor = 60 +WHERE id = (SELECT credit_step_id FROM test_repricing_context); + +SELECT + is( + ( + SELECT credit_step_id + FROM public.usage_overage_events + WHERE id = (SELECT overage_event_id FROM test_repricing_context) + ), + (SELECT credit_step_id FROM test_repricing_context), + 'repricing build_time tiers in place preserves usage_overage_events credit_step_id links' + ); + SELECT throws_ok( $sql$ diff --git a/tests/bundle-error-cases.test.ts b/tests/bundle-error-cases.test.ts index 5e541517bf..f9bc4dad96 100644 --- a/tests/bundle-error-cases.test.ts +++ b/tests/bundle-error-cases.test.ts @@ -1,10 +1,13 @@ import { randomUUID } from 'node:crypto' import { afterAll, beforeAll, describe, expect, it } from 'vitest' -import { BASE_URL, getSupabaseClient, headers, resetAndSeedAppData, resetAppData, USER_ID } from './test-utils.ts' +import { BASE_URL, createAppVersions, getSupabaseClient, headers, resetAndSeedAppData, resetAppData, USER_ID } from './test-utils.ts' const id = randomUUID() const APPNAME = `com.bundle.error.${id}` let testOrgId: string +let metadataVersionId: number +let channelVersionId: number +let testChannelId: number beforeAll(async () => { await resetAndSeedAppData(APPNAME) @@ -20,6 +23,26 @@ beforeAll(async () => { throw new Error('App not found after seeding') testOrgId = app.owner_org + + metadataVersionId = (await createAppVersions(`1.0.0-test-metadata-${id}`, APPNAME)).id + channelVersionId = (await createAppVersions(`1.0.0-test-channel-${id}`, APPNAME)).id + + const { data: channel, error: channelError } = await getSupabaseClient() + .from('channels') + .insert({ + name: `test-channel-${id}`, + app_id: APPNAME, + version: channelVersionId, + created_by: USER_ID, + owner_org: testOrgId, + }) + .select('id') + .single() + + if (channelError || !channel) + throw channelError ?? new Error('Failed to create test channel') + + testChannelId = channel.id }) afterAll(async () => { @@ -124,27 +147,12 @@ describe('[DELETE] /bundle - Error Cases', () => { describe('[POST] /bundle/metadata - Extended Error Cases', () => { it('should return 400 when no fields to update', async () => { - // First create a version to test with - const supabase = getSupabaseClient() - const { data: version, error } = await supabase - .from('app_versions') - .insert({ - app_id: APPNAME, - name: '1.0.0-test-metadata', - owner_org: testOrgId, - }) - .select() - .single() - - if (error) - throw error - const response = await fetch(`${BASE_URL}/bundle/metadata`, { method: 'POST', headers, body: JSON.stringify({ app_id: APPNAME, - version_id: version.id, + version_id: metadataVersionId, // No updateable fields provided }), }) @@ -210,37 +218,6 @@ describe('[PUT] /bundle - Extended Error Cases', () => { }) it('should return 500 when bundle cannot be set to channel', async () => { - // Create a version and channel first - const supabase = getSupabaseClient() - - const { data: version, error: versionError } = await supabase - .from('app_versions') - .insert({ - app_id: APPNAME, - name: '1.0.0-test-channel', - owner_org: testOrgId, - }) - .select() - .single() - - if (versionError) - throw versionError - - const { data: channel, error: channelError } = await supabase - .from('channels') - .insert({ - name: 'test-channel', - app_id: APPNAME, - version: version.id, - created_by: USER_ID, - owner_org: testOrgId, - }) - .select() - .single() - - if (channelError) - throw channelError - // Try to set bundle to channel with conflicting data const response = await fetch(`${BASE_URL}/bundle`, { method: 'PUT', @@ -248,7 +225,7 @@ describe('[PUT] /bundle - Extended Error Cases', () => { body: JSON.stringify({ app_id: APPNAME, version_id: 999999, // Non-existent version ID - channel_id: channel.id, + channel_id: testChannelId, }), }) diff --git a/tests/bundle.test.ts b/tests/bundle.test.ts index 5f6d240e58..96cb35caae 100644 --- a/tests/bundle.test.ts +++ b/tests/bundle.test.ts @@ -5,6 +5,29 @@ import { BASE_URL, createAppVersions, fetchBundle, getSupabaseClient, headers, r const id = randomUUID() const APPNAME = `com.app.b.${id}` +async function putBundleToChannelWithRetry(body: { app_id: string, version_id: number, channel_id: number }, maxRetries = 3): Promise { + let response: Response | undefined + + for (let attempt = 0; attempt < maxRetries; attempt++) { + response = await fetch(`${BASE_URL}/bundle`, { + method: 'PUT', + headers, + body: JSON.stringify(body), + }) + + if (response.status === 200) + return response + + if (attempt < maxRetries - 1) + await new Promise(resolve => setTimeout(resolve, 250 * (attempt + 1))) + } + + if (!response) + throw new Error('Failed to set bundle to channel: no response received') + + return response +} + beforeAll(async () => { await resetAndSeedAppData(APPNAME) }) @@ -215,14 +238,10 @@ describe('[PUT] /bundle operations - Set bundle to channel', () => { }) it('should set bundle to channel successfully', async () => { - const response = await fetch(`${BASE_URL}/bundle`, { - method: 'PUT', - headers, - body: JSON.stringify({ - app_id: APPNAME, - version_id: versionId, - channel_id: channelId, - }), + const response = await putBundleToChannelWithRetry({ + app_id: APPNAME, + version_id: versionId, + channel_id: channelId, }) const data = await response.json() as { status: string, message: string } diff --git a/tests/credit-pricing-ui.unit.test.ts b/tests/credit-pricing-ui.unit.test.ts new file mode 100644 index 0000000000..5db730983a --- /dev/null +++ b/tests/credit-pricing-ui.unit.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from 'vitest' +import { formatCreditPricingPrice, formatCreditPricingTierLabel, formatIncludedThenPrice, getFirstTierCreditUnitPricing } from '../src/services/creditPricing' + +const messages: Record = { + 'credits-plan-overage': '{included}, then {price}', + 'minutes-short': '{minutes}m', + 'credits-pricing-price': '{price} {unit}', + 'credits-pricing-tier-first': 'Up to {to}', + 'credits-pricing-tier-range': 'From {from} to {to}', + 'credits-pricing-tier-over': 'Over {from}', + 'credits-pricing-unit-per-gib': 'per GiB', + 'credits-pricing-unit-per-mau': 'per MAU', + 'credits-pricing-unit-per-minute': 'per minute', + 'included-in-plan': 'Included in plan', +} + +function t(key: string, values: Record = {}) { + const template = messages[key] ?? key + return template.replaceAll(/\{(\w+)\}/g, (_match, placeholder) => String(values[placeholder] ?? `{${placeholder}}`)) +} + +describe('credit pricing UI helpers', () => { + it.concurrent('formats first build_time tiers with generic translated labels', () => { + expect(formatCreditPricingTierLabel({ + type: 'build_time', + step_min: 0, + step_max: 6000, + unit_factor: 60, + }, t)).toBe('Up to 100m') + + expect(formatCreditPricingPrice('build_time', 0.16, t)).toBe('$0.16 per minute') + }) + + it.concurrent('falls back to generic tier copy for custom org-scoped ranges', () => { + expect(formatCreditPricingTierLabel({ + type: 'build_time', + step_min: 3000, + step_max: 9000, + unit_factor: 60, + }, t)).toBe('From 50m to 150m') + + expect(formatCreditPricingTierLabel({ + type: 'build_time', + step_min: 9000, + step_max: Number.MAX_SAFE_INTEGER, + unit_factor: 60, + }, t)).toBe('Over 150m') + }) + + it.concurrent('formats bounded custom ranges with both dynamic endpoints', () => { + expect(formatCreditPricingTierLabel({ + type: 'build_time', + step_min: 5000, + step_max: 6000, + unit_factor: 60, + }, t)).toBe('From 84m to 100m') + }) + + it.concurrent('derives the visible first-tier pricing from the shared step list', () => { + expect(getFirstTierCreditUnitPricing([ + { + type: 'build_time', + step_min: 6000, + step_max: 30000, + price_per_unit: 0.14, + unit_factor: 60, + }, + { + type: 'bandwidth', + step_min: 0, + step_max: 1099511627776, + price_per_unit: 0.12, + unit_factor: 1073741824, + }, + { + type: 'build_time', + step_min: 0, + step_max: 6000, + price_per_unit: 0.16, + unit_factor: 60, + }, + ])).toEqual({ + bandwidth: 0.12, + build_time: 0.16, + }) + }) + + it.concurrent('formats plan overage copy from the shared price formatter', () => { + expect(formatIncludedThenPrice('build_time', 0.08, t)).toBe('Included in plan, then $0.08 per minute') + }) +}) diff --git a/tests/credits-pricing.test.ts b/tests/credits-pricing.test.ts new file mode 100644 index 0000000000..1122fcd371 --- /dev/null +++ b/tests/credits-pricing.test.ts @@ -0,0 +1,225 @@ +import { describe, expect, it } from 'vitest' +import { executeSQL, fetchWithRetry, getAuthHeaders, getAuthHeadersForCredentials, getEndpointUrl, ORG_ID, USER_EMAIL_NONMEMBER, USER_PASSWORD_NONMEMBER } from './test-utils' + +interface CreditStep { + type: string + step_min: number + price_per_unit: number +} + +describe('credits pricing API', () => { + it.concurrent('returns the updated build_time tiers from the shared pricing table', async () => { + const response = await fetchWithRetry(getEndpointUrl('/private/credits')) + + expect(response.status).toBe(200) + + const data = await response.json() as CreditStep[] + const buildSteps = data + .filter(step => step.type === 'build_time') + .sort((a, b) => a.step_min - b.step_min) + + expect(buildSteps.map(step => step.price_per_unit)).toEqual([0.16, 0.14, 0.12, 0.10, 0.09, 0.08]) + }) + + it.concurrent('preserves not_authorized for org-scoped pricing queries without auth', async () => { + const response = await fetchWithRetry(getEndpointUrl(`/private/credits?org_id=${ORG_ID}`)) + + expect(response.status).toBe(400) + + const data = await response.json() as { + error: string + } + + expect(data.error).toBe('not_authorized') + }) + + it.concurrent('rejects org-scoped pricing queries for authenticated non-members', async () => { + const response = await fetchWithRetry(getEndpointUrl(`/private/credits?org_id=${ORG_ID}`), { + headers: await getAuthHeadersForCredentials(USER_EMAIL_NONMEMBER, USER_PASSWORD_NONMEMBER), + }) + + expect(response.status).toBe(400) + + const data = await response.json() as { + error: string + } + + expect(data.error).toBe('not_authorized') + }) + + it.concurrent('prices build_time overage through the shared calculator endpoint', async () => { + const response = await fetchWithRetry(getEndpointUrl('/private/credits'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + mau: 0, + bandwidth: 0, + storage: 0, + build_time: 6000, + }), + }) + + expect(response.status).toBe(200) + + const data = await response.json() as { + total_cost: number + breakdown: { + build_time: { + cost: number + } + } + usage: { + build_time: number + } + } + + expect(data.usage.build_time).toBe(6000) + expect(data.breakdown.build_time.cost).toBe(16) + expect(data.total_cost).toBe(16) + }) + + it.concurrent('rejects negative build_time input', async () => { + const response = await fetchWithRetry(getEndpointUrl('/private/credits'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + mau: 0, + bandwidth: 0, + storage: 0, + build_time: -60, + }), + }) + + expect(response.status).toBe(400) + + const data = await response.json() as { + error: string + } + + expect(data.error).toBe('invalid_build_time') + }) + + it.concurrent('rejects org-scoped cost calculation for authenticated non-members', async () => { + const response = await fetchWithRetry(getEndpointUrl('/private/credits'), { + method: 'POST', + headers: await getAuthHeadersForCredentials(USER_EMAIL_NONMEMBER, USER_PASSWORD_NONMEMBER), + body: JSON.stringify({ + org_id: ORG_ID, + mau: 0, + bandwidth: 0, + storage: 0, + build_time: 6000, + }), + }) + + expect(response.status).toBe(400) + + const data = await response.json() as { + error: string + } + + expect(data.error).toBe('not_authorized') + }) + + it('uses org-scoped build_time tiers when an authorized org_id is supplied', async () => { + await executeSQL('DELETE FROM public.capgo_credits_steps WHERE org_id = $1 AND type = $2', [ORG_ID, 'build_time']) + + await executeSQL(` + INSERT INTO public.capgo_credits_steps (type, step_min, step_max, price_per_unit, unit_factor, org_id) + VALUES ($1, $2, $3, $4, $5, $6) + `, ['build_time', 0, 6000, 0.05, 60, ORG_ID]) + + try { + const response = await fetchWithRetry(getEndpointUrl('/private/credits'), { + method: 'POST', + headers: await getAuthHeaders(), + body: JSON.stringify({ + org_id: ORG_ID, + mau: 0, + bandwidth: 0, + storage: 0, + build_time: 6000, + }), + }) + + expect(response.status).toBe(200) + + const data = await response.json() as { + total_cost: number + breakdown: { + build_time: { + cost: number + tiers: { + price_per_unit: number + }[] + } + } + } + + expect(data.breakdown.build_time.tiers[0]?.price_per_unit).toBe(0.05) + expect(data.breakdown.build_time.cost).toBe(5) + expect(data.total_cost).toBe(5) + } + finally { + await executeSQL('DELETE FROM public.capgo_credits_steps WHERE org_id = $1 AND type = $2', [ORG_ID, 'build_time']) + } + }) + + it('falls back to the correct global tiers after a partial org-scoped override', async () => { + await executeSQL('DELETE FROM public.capgo_credits_steps WHERE org_id = $1 AND type = $2', [ORG_ID, 'build_time']) + + await executeSQL(` + INSERT INTO public.capgo_credits_steps (type, step_min, step_max, price_per_unit, unit_factor, org_id) + VALUES ($1, $2, $3, $4, $5, $6) + `, ['build_time', 0, 5000, 0.05, 60, ORG_ID]) + + try { + const response = await fetchWithRetry(getEndpointUrl('/private/credits'), { + method: 'POST', + headers: await getAuthHeaders(), + body: JSON.stringify({ + org_id: ORG_ID, + mau: 0, + bandwidth: 0, + storage: 0, + build_time: 8000, + }), + }) + + expect(response.status).toBe(200) + + const data = await response.json() as { + total_cost: number + breakdown: { + build_time: { + cost: number + tiers: { + step_min: number + step_max: number + price_per_unit: number + }[] + } + } + } + + expect(data.breakdown.build_time.tiers.map(tier => ({ + step_min: tier.step_min, + step_max: tier.step_max, + price_per_unit: tier.price_per_unit, + }))).toEqual([ + { step_min: 0, step_max: 5000, price_per_unit: 0.05 }, + { step_min: 5000, step_max: 6000, price_per_unit: 0.16 }, + { step_min: 6000, step_max: 30000, price_per_unit: 0.14 }, + ]) + expect(data.breakdown.build_time.cost).toBeCloseTo(11.68, 5) + expect(data.total_cost).toBeCloseTo(11.68, 5) + } + finally { + await executeSQL('DELETE FROM public.capgo_credits_steps WHERE org_id = $1 AND type = $2', [ORG_ID, 'build_time']) + } + }) +}) diff --git a/tests/i18n-fallback.unit.test.ts b/tests/i18n-fallback.unit.test.ts new file mode 100644 index 0000000000..e20a85718c --- /dev/null +++ b/tests/i18n-fallback.unit.test.ts @@ -0,0 +1,39 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +describe('i18n fallback loading', () => { + beforeEach(() => { + vi.resetModules() + vi.stubGlobal('localStorage', { + clear: vi.fn(), + getItem: vi.fn(() => null), + removeItem: vi.fn(), + setItem: vi.fn(), + }) + vi.stubGlobal('document', { + createElement: vi.fn(() => ({ + innerHTML: '', + })), + querySelector: vi.fn(() => ({ + setAttribute: vi.fn(), + })), + }) + }) + + afterEach(() => { + vi.unstubAllGlobals() + vi.restoreAllMocks() + }) + + it.concurrent('loads the English fallback bundle before switching to another locale', async () => { + const { i18n, loadLanguageAsync } = await import('../src/modules/i18n.ts') + + await loadLanguageAsync('fr') + + expect(i18n.global.availableLocales).toEqual(expect.arrayContaining(['en', 'fr'])) + expect(i18n.global.locale.value).toBe('fr') + expect(i18n.global.t('credits-plan-overage', { + included: 'Included in plan', + price: '$0.08 per minute', + })).toBe('Included in plan, then $0.08 per minute') + }) +}) diff --git a/tests/webhooks.test.ts b/tests/webhooks.test.ts index 8fe9f96f28..866b7bda96 100644 --- a/tests/webhooks.test.ts +++ b/tests/webhooks.test.ts @@ -49,19 +49,18 @@ beforeAll(async () => { if (appError) throw appError - const appScopedKeyResponse = await fetch(`${BASE_URL}/apikey`, { - method: 'POST', - headers, - body: JSON.stringify({ - name: `webhook-app-scoped-${globalId}`, - limited_to_apps: [webhookAppId], - }), - }) - if (appScopedKeyResponse.status !== 200) { - throw new Error(`Failed to create app-scoped API key for webhook tests: ${await appScopedKeyResponse.text()}`) + const { data: appScopedKeyData, error: appScopedKeyError } = await getSupabaseClient().rpc('create_hashed_apikey_for_user', { + p_user_id: USER_ID, + p_mode: 'all', + p_name: `webhook-app-scoped-${globalId}`, + p_limited_to_orgs: [], + p_limited_to_apps: [webhookAppId], + p_expires_at: null as unknown as string, + }) + if (appScopedKeyError || !appScopedKeyData?.key) { + throw new Error(`Failed to create app-scoped API key for webhook tests: ${appScopedKeyError?.message ?? 'missing key data'}`) } - const appScopedKeyData = await appScopedKeyResponse.json() as { id: number, key: string } appScopedKeyId = appScopedKeyData.id appScopedKey = appScopedKeyData.key }) @@ -73,10 +72,7 @@ afterAll(async () => { await (getSupabaseClient() as any).from('webhooks').delete().eq('id', createdWebhookId) } if (appScopedKeyId) { - await fetch(`${BASE_URL}/apikey/${appScopedKeyId}`, { - method: 'DELETE', - headers, - }) + await getSupabaseClient().from('apikeys').delete().eq('id', appScopedKeyId) } await getSupabaseClient().from('apps').delete().eq('app_id', webhookAppId) // Clean up test organization and stripe_info