source for getorbyt.com getorbyt.com/
client bsky orbytapp app orbyt bluesky getorbyt orbytvideo atproto video
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(i18n): Implement multi-language support across the application

- Added language switcher component for user locale selection.
- Integrated translations for English, German, Spanish (MX and LA), French, Japanese, Korean, and Portuguese (BR).
- Updated pages (app, contact, index, privacy, terms) to utilize translations for titles, subtitles, and other text elements.
- Modified middleware to handle locale detection from cookies and Accept-Language header.
- Adjusted HTML lang attributes dynamically based on user locale.
- Set prerender to false for pages to support dynamic content based on locale.

+887 -83
+13 -2
astro.config.mjs
··· 3 3 4 4 export default defineConfig({ 5 5 site: 'https://getorbyt.com', 6 + i18n: { 7 + defaultLocale: 'en', 8 + locales: ['en', 'de', 'es-MX', 'fr', 'ja', 'ko', 'pt-BR', 'es-419'], 9 + routing: { 10 + prefixDefaultLocale: false, 11 + redirectToDefaultLocale: false, 12 + }, 13 + fallback: { 14 + 'es-419': 'es-MX', 15 + }, 16 + }, 6 17 vite: { 7 18 optimizeDeps: { 8 19 exclude: ['fsevents'], ··· 14 25 configPath: 'wrangler.jsonc', 15 26 }, 16 27 routes: { 17 - strategy: 'include', 18 - include: ['/@*', '/@*/*'], 28 + strategy: 'exclude', 29 + exclude: ['/css/*', '/fonts/*', '/images/*', '/favicon/*', '/js/*'], 19 30 }, 20 31 }), 21 32 });
+12 -9
src/components/AltstorePalPeek.astro
··· 1 1 --- 2 2 import AltstorePalTipArrow from './AltstorePalTipArrow.astro'; 3 3 import '../styles/altstore-pal-peek.css'; 4 + import { useTranslations } from '../i18n/utils'; 4 5 5 6 interface Props { 6 7 href: string; 8 + locale?: string; 7 9 ariaLabel?: string; 8 10 } 9 11 10 - const { href, ariaLabel = 'Add orbyt source in AltStore PAL' } = Astro.props; 12 + const { href, locale = 'en', ariaLabel } = Astro.props; 13 + const t = useTranslations(locale); 11 14 /** Same as `href` — AltStore PAL deep link for scanning from iPhone */ 12 15 const qrCodeImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&bgcolor=000000&color=f3f5fe&data=${encodeURIComponent(href)}`; 13 16 --- ··· 15 18 <a 16 19 class="altstore-pal-peek" 17 20 href={href} 18 - aria-label={ariaLabel} 21 + aria-label={ariaLabel ?? t('altstore.aria_label')} 19 22 aria-haspopup="dialog" 20 23 data-altstore-pal-qr-trigger 21 24 > 22 25 <span class="altstore-pal-peek__tip" aria-hidden="true"> 23 - <span class="altstore-pal-peek__tip-label">get it on altstore</span> 26 + <span class="altstore-pal-peek__tip-label">{t('altstore.tip_label')}</span> 24 27 <AltstorePalTipArrow /> 25 28 </span> 26 29 <img ··· 41 44 > 42 45 <div class="altstore-pal-qr-dialog__panel"> 43 46 <h2 id="altstore-pal-qr-dialog-title" class="altstore-pal-qr-dialog__title"> 44 - Scan with your iPhone 47 + {t('altstore.dialog.title')} 45 48 </h2> 46 49 <figure class="altstore-pal-qr-dialog__media"> 47 50 <div class="altstore-pal-qr-dialog__qr-wrap"> 48 51 <img 49 52 class="altstore-pal-qr-dialog__qr" 50 53 src={qrCodeImageUrl} 51 - alt="QR code for AltStore PAL — scan with your iPhone" 54 + alt={t('altstore.dialog.qr_alt')} 52 55 width="200" 53 56 height="200" 54 57 loading="lazy" ··· 56 59 /> 57 60 </div> 58 61 <figcaption class="altstore-pal-qr-dialog__hint"> 59 - Adds the orbyt source in AltStore PAL 62 + {t('altstore.dialog.hint')} 60 63 </figcaption> 61 64 </figure> 62 65 <form class="altstore-pal-qr-dialog__dismiss" method="dialog"> 63 66 <button type="submit" class="altstore-pal-qr-dialog__close"> 64 - Close 67 + {t('altstore.dialog.close')} 65 68 </button> 66 69 </form> 67 70 <p class="altstore-pal-qr-dialog__new"> 68 - Need AltStore?{' '} 71 + {t('altstore.dialog.need_altstore')}{' '} 69 72 <a 70 73 class="altstore-pal-qr-dialog__altstore-link" 71 74 href="https://altstore.io/#Downloads" 72 75 target="_blank" 73 76 rel="noopener noreferrer" 74 77 > 75 - Get AltStore 78 + {t('altstore.dialog.get_altstore')} 76 79 </a> 77 80 </p> 78 81 </div>
+113
src/components/LanguageSwitcher.astro
··· 1 + --- 2 + import { SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_LABELS } from '../i18n/utils' 3 + 4 + interface Props { 5 + currentLocale: string 6 + currentPath?: string 7 + } 8 + 9 + const { currentLocale, currentPath = '/' } = Astro.props 10 + const allLocales = [DEFAULT_LOCALE, ...SUPPORTED_LOCALES] 11 + 12 + const LOCALE_SHORT: Record<string, string> = { 13 + en: 'EN', de: 'DE', fr: 'FR', ja: 'JA', ko: 'KO', 14 + 'es-MX': 'ES', 'pt-BR': 'PT', 'es-419': 'LA', 15 + } 16 + const currentShort = LOCALE_SHORT[currentLocale] ?? currentLocale.slice(0, 2).toUpperCase() 17 + --- 18 + 19 + <div class="lang-switcher"> 20 + <span class="lang-icon" aria-hidden="true"> 21 + <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none"> 22 + <path d="M8.673 3.063c-.244.075-.523.351-.609.603-.047.138-.064.341-.064.761V5H5.947c-2.349 0-2.34-.001-2.641.3a.96.96 0 0 0 0 1.4c.314.314.127.3 4.101.3 3.22 0 3.513.005 3.513.065 0 .149-.218 1.021-.348 1.394-.285.818-.892 1.927-1.409 2.575l-.171.213-.257-.349A9.527 9.527 0 0 1 7.579 8.86c-.186-.442-.317-.615-.553-.733-.552-.275-1.21-.025-1.4.533-.097.284-.082.462.07.852.387.992.953 1.976 1.637 2.842l.263.334-.172.148c-.863.747-2.316 1.621-3.528 2.122-.475.196-.642.323-.773.585a.989.989 0 0 0 .82 1.449c.252.021.632-.121 1.637-.613 1.194-.583 2.298-1.298 3.168-2.051l.248-.214.272.231a17.26 17.26 0 0 0 1.627 1.188c.217.135.402.265.411.288.009.024-.049.222-.129.441-.48 1.319-1.129 3.357-1.164 3.658a.972.972 0 0 0 .285.78.987.987 0 0 0 1.404.001c.158-.159.19-.228.359-.768.102-.326.284-.895.406-1.263l.22-.67h5.626l.22.67c.122.368.304.937.406 1.263.169.54.201.609.359.768a.988.988 0 0 0 1.042.233c.412-.141.704-.612.645-1.042-.053-.387-.884-2.912-1.413-4.292-.731-1.907-1.718-4.047-2.406-5.215-.373-.632-.969-.985-1.666-.985-.701 0-1.291.351-1.673.996-.285.48-1.005 1.917-1.385 2.764a26.31 26.31 0 0 1-.346.756c-.05.056-1.688-1.164-1.666-1.242.006-.02.118-.171.249-.337 1.136-1.429 1.994-3.352 2.206-4.947l.052-.39h.584c.731 0 .922-.049 1.176-.303.183-.183.303-.46.303-.697 0-.237-.12-.514-.303-.697-.305-.305-.292-.303-2.644-.303H10v-.553c0-.696-.052-.893-.303-1.144-.279-.279-.63-.361-1.024-.24m7.344 9.416c.605 1.197 1.543 3.319 1.543 3.491 0 .017-.918.03-2.04.03-1.122 0-2.04-.006-2.04-.012 0-.007.036-.087.08-.179a.803.803 0 0 0 .08-.269c0-.244 1.112-2.699 1.711-3.775.131-.236.146-.25.197-.181.03.041.241.444.469.895" fill="currentColor" fill-rule="evenodd"/> 23 + </svg> 24 + </span> 25 + <span class="lang-code">{currentShort}</span> 26 + <select 27 + class="lang-select" 28 + aria-label="Select language" 29 + onchange="orbytSwitchLang(this.value)" 30 + > 31 + {allLocales.map((locale) => ( 32 + <option value={locale} selected={locale === currentLocale}> 33 + {LOCALE_LABELS[locale]} 34 + </option> 35 + ))} 36 + </select> 37 + </div> 38 + 39 + <script is:inline define:vars={{ SUPPORTED_LOCALES, DEFAULT_LOCALE, currentPath }}> 40 + window.orbytSwitchLang = function (locale) { 41 + document.cookie = `orbyt-locale=${locale};max-age=31536000;path=/;samesite=lax` 42 + 43 + let path = currentPath 44 + for (const l of SUPPORTED_LOCALES) { 45 + if (path === `/${l}` || path.startsWith(`/${l}/`)) { 46 + path = path.slice(l.length + 1) || '/' 47 + break 48 + } 49 + } 50 + 51 + const dest = locale === DEFAULT_LOCALE 52 + ? (path || '/') 53 + : `/${locale}${path === '/' ? '/' : path}` 54 + 55 + window.location.href = dest 56 + } 57 + </script> 58 + 59 + <style> 60 + .lang-switcher { 61 + position: fixed; 62 + bottom: 1.25rem; 63 + right: 1.25rem; 64 + z-index: 100; 65 + display: inline-flex; 66 + align-items: center; 67 + gap: 5px; 68 + padding: 6px 10px 6px 8px; 69 + background: rgba(5, 7, 10, 0.72); 70 + backdrop-filter: blur(12px); 71 + -webkit-backdrop-filter: blur(12px); 72 + border: 1px solid rgba(255, 255, 255, 0.1); 73 + border-radius: 999px; 74 + cursor: pointer; 75 + color: rgba(243, 245, 254, 0.65); 76 + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease; 77 + } 78 + 79 + .lang-switcher:hover { 80 + color: #f3f5fe; 81 + border-color: rgba(255, 255, 255, 0.25); 82 + background: rgba(5, 7, 10, 0.88); 83 + } 84 + 85 + .lang-icon { 86 + display: flex; 87 + align-items: center; 88 + flex-shrink: 0; 89 + } 90 + 91 + .lang-code { 92 + font-family: 'Figtree', sans-serif; 93 + font-size: 11px; 94 + font-weight: 700; 95 + letter-spacing: 0.1em; 96 + line-height: 1; 97 + } 98 + 99 + .lang-select { 100 + position: absolute; 101 + inset: 0; 102 + width: 100%; 103 + height: 100%; 104 + opacity: 0; 105 + cursor: pointer; 106 + font-size: 16px; 107 + } 108 + 109 + .lang-select option { 110 + background: #111; 111 + color: #f3f5fe; 112 + } 113 + </style>
+16 -5
src/components/SiteFooter.astro
··· 1 1 --- 2 + import LanguageSwitcher from './LanguageSwitcher.astro' 3 + import { useTranslations } from '../i18n/utils' 4 + 5 + interface Props { 6 + locale?: string 7 + currentPath?: string 8 + } 9 + 10 + const { locale = 'en', currentPath = '/' } = Astro.props 11 + const t = useTranslations(locale) 2 12 --- 3 13 <footer class="site-footer"> 4 14 <div class="site-footer__separator"> ··· 11 21 <rect fill="url(#squigglePattern)" width="100%" height="100%"/> 12 22 </svg> 13 23 </div> 14 - <a href="https://bsky.app/profile/getorbyt.com">bluesky</a> 15 - <a href="https://community.getorbyt.com">community</a> 16 - <a href="/terms">terms</a> 17 - <a href="/privacy">privacy</a> 18 - <a href="/contact">contact</a> 24 + <a href="https://bsky.app/profile/getorbyt.com">{t('footer.link.bluesky')}</a> 25 + <a href="https://community.getorbyt.com">{t('footer.link.community')}</a> 26 + <a href="/terms">{t('footer.link.terms')}</a> 27 + <a href="/privacy">{t('footer.link.privacy')}</a> 28 + <a href="/contact">{t('footer.link.contact')}</a> 19 29 </footer> 30 + <LanguageSwitcher currentLocale={locale} currentPath={currentPath} />
+6
src/env.d.ts
··· 3 3 declare module 'cloudflare:workers' { 4 4 export const env: Env; 5 5 } 6 + 7 + declare namespace App { 8 + interface Locals { 9 + locale: string; 10 + } 11 + }
+66
src/i18n/translations/de.json
··· 1 + { 2 + "index.title": "orbyt – Video-Communities", 3 + "index.meta.description": "eine neue Video-App für Bluesky", 4 + "index.motto": "Video-Communities", 5 + "index.button.ios": "iOS", 6 + "index.button.android_waitlist": "Android-Warteliste", 7 + "index.schema.organization_description": "eine neue Video-App für Bluesky", 8 + 9 + "app.title": "App herunterladen – orbyt", 10 + "app.subtitle.desktop": "Zum Herunterladen scannen", 11 + "app.subtitle.mobile": "App herunterladen", 12 + "app.qr.alt": "QR-Code zum Herunterladen der orbyt-App", 13 + "app.qr.caption": "oder direkt unten herunterladen", 14 + "app.button.ios": "iOS", 15 + "app.button.android_waitlist": "Android-Warteliste", 16 + 17 + "404.title": "404 – Seite nicht gefunden | Orbyt", 18 + "404.heading": "Kein Signal", 19 + "404.message": "Diese Seite existiert nicht oder wurde verschoben.", 20 + "404.action.back": "Zurück", 21 + "404.action.home": "Startseite", 22 + 23 + "contact.title": "Kontakt – Orbyt", 24 + "contact.logo.alt": "Orbyt", 25 + "contact.heading": "Kontakt", 26 + "contact.section.general": "Allgemeine Anfragen", 27 + "contact.section.press": "Presse & Medienanfragen", 28 + "contact.section.bugs": "Fehlerberichte", 29 + "contact.section.copyright": "Urheberrecht und Inhaltsprobleme", 30 + "contact.copyright.html": "Orbyt ist ein Drittanbieter-Client für Bluesky. Inhalte werden im AT-Protokoll-Netzwerk gehostet, nicht von Orbyt. Für Urheberrechtsbeschwerden oder inhaltsbezogene rechtliche Probleme wenden Sie sich bitte direkt an den <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">Bluesky-Support</a>.", 31 + 32 + "footer.link.bluesky": "bluesky", 33 + "footer.link.community": "Community", 34 + "footer.link.terms": "Nutzungsbedingungen", 35 + "footer.link.privacy": "Datenschutz", 36 + "footer.link.contact": "Kontakt", 37 + 38 + "privacy.title": "Datenschutz – Orbyt", 39 + "privacy.heading": "Datenschutzerklärung", 40 + "privacy.last_updated": "Zuletzt aktualisiert: 2. Februar 2026", 41 + "privacy.logo.alt": "Orbyt", 42 + 43 + "terms.title": "Nutzungsbedingungen – Orbyt", 44 + "terms.heading": "Nutzungsbedingungen", 45 + "terms.last_updated": "Zuletzt aktualisiert: 2. Februar 2026", 46 + "terms.logo.alt": "Orbyt", 47 + 48 + "profile.install_cta": "App herunterladen", 49 + "profile.empty_feed": "noch nichts hier...", 50 + "profile.load_more": "Mehr laden", 51 + "profile.loading": "Lädt...", 52 + "profile.breadcrumb.home": "Startseite", 53 + 54 + "post.install_cta": "App herunterladen", 55 + "post.breadcrumb.home": "Startseite", 56 + "post.caption.more": "Mehr", 57 + 58 + "altstore.tip_label": "bei AltStore holen", 59 + "altstore.aria_label": "orbyt-Quelle in AltStore PAL hinzufügen", 60 + "altstore.dialog.title": "Mit dem iPhone scannen", 61 + "altstore.dialog.qr_alt": "QR-Code für AltStore PAL – mit dem iPhone scannen", 62 + "altstore.dialog.hint": "Fügt orbyt zu AltStore PAL hinzu", 63 + "altstore.dialog.close": "Schließen", 64 + "altstore.dialog.need_altstore": "Neu bei AltStore?", 65 + "altstore.dialog.get_altstore": "Mehr erfahren" 66 + }
+66
src/i18n/translations/en.json
··· 1 + { 2 + "index.title": "orbyt - video communities", 3 + "index.meta.description": "a new video app for bluesky", 4 + "index.motto": "video communities", 5 + "index.button.ios": "iOS", 6 + "index.button.android_waitlist": "Android Waitlist", 7 + "index.schema.organization_description": "a new video app for bluesky", 8 + 9 + "app.title": "Get the App - orbyt", 10 + "app.subtitle.desktop": "scan to get the app", 11 + "app.subtitle.mobile": "get the app", 12 + "app.qr.alt": "QR code to download orbyt app", 13 + "app.qr.caption": "or download directly below", 14 + "app.button.ios": "iOS", 15 + "app.button.android_waitlist": "Android Waitlist", 16 + 17 + "404.title": "404 - Page Not Found | Orbyt", 18 + "404.heading": "No Signal", 19 + "404.message": "This page doesn't exist or has been moved.", 20 + "404.action.back": "Go Back", 21 + "404.action.home": "Homepage", 22 + 23 + "contact.title": "Contact Us - Orbyt", 24 + "contact.logo.alt": "Orbyt", 25 + "contact.heading": "Contact Us", 26 + "contact.section.general": "For general inquiries", 27 + "contact.section.press": "Press & media requests", 28 + "contact.section.bugs": "Bug reports", 29 + "contact.section.copyright": "Copyright and content issues", 30 + "contact.copyright.html": "Orbyt is a third-party client for Bluesky. Content is hosted on the AT Protocol network, not by Orbyt. For copyright takedowns or content-related legal issues, please contact <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">Bluesky Support</a> directly.", 31 + 32 + "footer.link.bluesky": "bluesky", 33 + "footer.link.community": "community", 34 + "footer.link.terms": "terms", 35 + "footer.link.privacy": "privacy", 36 + "footer.link.contact": "contact", 37 + 38 + "privacy.title": "Privacy Policy - Orbyt", 39 + "privacy.heading": "Privacy Policy", 40 + "privacy.last_updated": "Last Updated: February 2, 2026", 41 + "privacy.logo.alt": "Orbyt", 42 + 43 + "terms.title": "Terms of Use - Orbyt", 44 + "terms.heading": "Terms of Use", 45 + "terms.last_updated": "Last Updated: February 2, 2026", 46 + "terms.logo.alt": "Orbyt", 47 + 48 + "profile.install_cta": "Get the app", 49 + "profile.empty_feed": "nothing here, yet...", 50 + "profile.load_more": "Load more", 51 + "profile.loading": "Loading...", 52 + "profile.breadcrumb.home": "Home", 53 + 54 + "post.install_cta": "Get the app", 55 + "post.breadcrumb.home": "Home", 56 + "post.caption.more": "More", 57 + 58 + "altstore.tip_label": "get it on altstore", 59 + "altstore.aria_label": "Add orbyt source in AltStore PAL", 60 + "altstore.dialog.title": "Scan with your iPhone", 61 + "altstore.dialog.qr_alt": "QR code for AltStore PAL — scan with your iPhone", 62 + "altstore.dialog.hint": "Adds the orbyt source in AltStore PAL", 63 + "altstore.dialog.close": "Close", 64 + "altstore.dialog.need_altstore": "New to AltStore?", 65 + "altstore.dialog.get_altstore": "Learn More" 66 + }
+5
src/i18n/translations/es-419.json
··· 1 + { 2 + "404.action.back": "Volver", 3 + "profile.install_cta": "Descarga la app", 4 + "post.install_cta": "Descarga la app" 5 + }
+66
src/i18n/translations/es-MX.json
··· 1 + { 2 + "index.title": "orbyt – comunidades de video", 3 + "index.meta.description": "una nueva app de video para bluesky", 4 + "index.motto": "comunidades de video", 5 + "index.button.ios": "iOS", 6 + "index.button.android_waitlist": "Lista de espera Android", 7 + "index.schema.organization_description": "una nueva app de video para bluesky", 8 + 9 + "app.title": "Obtén la app – orbyt", 10 + "app.subtitle.desktop": "Escanea para obtener la app", 11 + "app.subtitle.mobile": "Obtén la app", 12 + "app.qr.alt": "Código QR para descargar la app orbyt", 13 + "app.qr.caption": "o descarga directamente abajo", 14 + "app.button.ios": "iOS", 15 + "app.button.android_waitlist": "Lista de espera Android", 16 + 17 + "404.title": "404 – Página no encontrada | Orbyt", 18 + "404.heading": "Sin señal", 19 + "404.message": "Esta página no existe o fue movida.", 20 + "404.action.back": "Regresar", 21 + "404.action.home": "Inicio", 22 + 23 + "contact.title": "Contáctanos – Orbyt", 24 + "contact.logo.alt": "Orbyt", 25 + "contact.heading": "Contáctanos", 26 + "contact.section.general": "Consultas generales", 27 + "contact.section.press": "Prensa y medios", 28 + "contact.section.bugs": "Reporte de errores", 29 + "contact.section.copyright": "Derechos de autor y contenido", 30 + "contact.copyright.html": "Orbyt es un cliente de terceros para Bluesky. El contenido está alojado en la red AT Protocol, no por Orbyt. Para retiradas por derechos de autor o problemas legales relacionados con el contenido, comunícate directamente con el <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">soporte de Bluesky</a>.", 31 + 32 + "footer.link.bluesky": "bluesky", 33 + "footer.link.community": "comunidad", 34 + "footer.link.terms": "términos", 35 + "footer.link.privacy": "privacidad", 36 + "footer.link.contact": "contacto", 37 + 38 + "privacy.title": "Política de privacidad – Orbyt", 39 + "privacy.heading": "Política de privacidad", 40 + "privacy.last_updated": "Última actualización: 2 de febrero de 2026", 41 + "privacy.logo.alt": "Orbyt", 42 + 43 + "terms.title": "Términos de uso – Orbyt", 44 + "terms.heading": "Términos de uso", 45 + "terms.last_updated": "Última actualización: 2 de febrero de 2026", 46 + "terms.logo.alt": "Orbyt", 47 + 48 + "profile.install_cta": "Obtén la app", 49 + "profile.empty_feed": "nada aquí, todavía...", 50 + "profile.load_more": "Cargar más", 51 + "profile.loading": "Cargando...", 52 + "profile.breadcrumb.home": "Inicio", 53 + 54 + "post.install_cta": "Obtén la app", 55 + "post.breadcrumb.home": "Inicio", 56 + "post.caption.more": "Más", 57 + 58 + "altstore.tip_label": "consíguelo en AltStore", 59 + "altstore.aria_label": "Agregar fuente de orbyt en AltStore PAL", 60 + "altstore.dialog.title": "Escanea con tu iPhone", 61 + "altstore.dialog.qr_alt": "Código QR para AltStore PAL — escanea con tu iPhone", 62 + "altstore.dialog.hint": "Agrega la fuente de orbyt en AltStore PAL", 63 + "altstore.dialog.close": "Cerrar", 64 + "altstore.dialog.need_altstore": "¿Nuevo en AltStore?", 65 + "altstore.dialog.get_altstore": "Más información" 66 + }
+66
src/i18n/translations/fr.json
··· 1 + { 2 + "index.title": "orbyt – communautés vidéo", 3 + "index.meta.description": "une nouvelle app vidéo pour bluesky", 4 + "index.motto": "communautés vidéo", 5 + "index.button.ios": "iOS", 6 + "index.button.android_waitlist": "Liste d'attente Android", 7 + "index.schema.organization_description": "une nouvelle app vidéo pour bluesky", 8 + 9 + "app.title": "Obtenir l'app – orbyt", 10 + "app.subtitle.desktop": "Scannez pour obtenir l'app", 11 + "app.subtitle.mobile": "Obtenir l'app", 12 + "app.qr.alt": "QR code pour télécharger l'app orbyt", 13 + "app.qr.caption": "ou télécharger directement ci-dessous", 14 + "app.button.ios": "iOS", 15 + "app.button.android_waitlist": "Liste d'attente Android", 16 + 17 + "404.title": "404 – Page introuvable | Orbyt", 18 + "404.heading": "Pas de signal", 19 + "404.message": "Cette page n'existe pas ou a été déplacée.", 20 + "404.action.back": "Retour", 21 + "404.action.home": "Accueil", 22 + 23 + "contact.title": "Contactez-nous – Orbyt", 24 + "contact.logo.alt": "Orbyt", 25 + "contact.heading": "Contactez-nous", 26 + "contact.section.general": "Questions générales", 27 + "contact.section.press": "Presse et médias", 28 + "contact.section.bugs": "Signalement de bugs", 29 + "contact.section.copyright": "Droits d'auteur et contenu", 30 + "contact.copyright.html": "Orbyt est une application tierce pour Bluesky. Le contenu est hébergé sur le réseau AT Protocol, pas par Orbyt. Pour les demandes de retrait pour violation de droits d'auteur, veuillez contacter directement le <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">support Bluesky</a>.", 31 + 32 + "footer.link.bluesky": "bluesky", 33 + "footer.link.community": "communauté", 34 + "footer.link.terms": "conditions", 35 + "footer.link.privacy": "confidentialité", 36 + "footer.link.contact": "contact", 37 + 38 + "privacy.title": "Politique de confidentialité – Orbyt", 39 + "privacy.heading": "Politique de confidentialité", 40 + "privacy.last_updated": "Dernière mise à jour : 2 février 2026", 41 + "privacy.logo.alt": "Orbyt", 42 + 43 + "terms.title": "Conditions d'utilisation – Orbyt", 44 + "terms.heading": "Conditions d'utilisation", 45 + "terms.last_updated": "Dernière mise à jour : 2 février 2026", 46 + "terms.logo.alt": "Orbyt", 47 + 48 + "profile.install_cta": "Obtenir l'app", 49 + "profile.empty_feed": "rien ici, pour l'instant…", 50 + "profile.load_more": "Charger plus", 51 + "profile.loading": "Chargement…", 52 + "profile.breadcrumb.home": "Accueil", 53 + 54 + "post.install_cta": "Obtenir l'app", 55 + "post.breadcrumb.home": "Accueil", 56 + "post.caption.more": "Plus", 57 + 58 + "altstore.tip_label": "disponible sur AltStore", 59 + "altstore.aria_label": "Ajouter la source orbyt dans AltStore PAL", 60 + "altstore.dialog.title": "Scannez avec votre iPhone", 61 + "altstore.dialog.qr_alt": "QR code pour AltStore PAL — scannez avec votre iPhone", 62 + "altstore.dialog.hint": "Ajoute la source orbyt dans AltStore PAL", 63 + "altstore.dialog.close": "Fermer", 64 + "altstore.dialog.need_altstore": "Nouveau sur AltStore ?", 65 + "altstore.dialog.get_altstore": "En savoir plus" 66 + }
+66
src/i18n/translations/ja.json
··· 1 + { 2 + "index.title": "orbyt – ビデオコミュニティ", 3 + "index.meta.description": "Bluesky向けの新しいビデオアプリ", 4 + "index.motto": "ビデオコミュニティ", 5 + "index.button.ios": "iOS", 6 + "index.button.android_waitlist": "Android 順番待ちリスト", 7 + "index.schema.organization_description": "Bluesky向けの新しいビデオアプリ", 8 + 9 + "app.title": "アプリを入手 – orbyt", 10 + "app.subtitle.desktop": "スキャンしてアプリを入手", 11 + "app.subtitle.mobile": "アプリを入手", 12 + "app.qr.alt": "orbytアプリをダウンロードするQRコード", 13 + "app.qr.caption": "または下からダウンロード", 14 + "app.button.ios": "iOS", 15 + "app.button.android_waitlist": "Android 順番待ちリスト", 16 + 17 + "404.title": "404 – ページが見つかりません | Orbyt", 18 + "404.heading": "信号なし", 19 + "404.message": "このページは存在しないか、移動されました。", 20 + "404.action.back": "戻る", 21 + "404.action.home": "ホーム", 22 + 23 + "contact.title": "お問い合わせ – Orbyt", 24 + "contact.logo.alt": "Orbyt", 25 + "contact.heading": "お問い合わせ", 26 + "contact.section.general": "一般的なお問い合わせ", 27 + "contact.section.press": "プレス・メディア向けお問い合わせ", 28 + "contact.section.bugs": "バグ報告", 29 + "contact.section.copyright": "著作権・コンテンツに関する問題", 30 + "contact.copyright.html": "OrbytはBluesky向けのサードパーティクライアントです。コンテンツはAT Protocolネットワーク上でホストされており、Orbytが管理しているわけではありません。著作権の削除申請やコンテンツに関する法的問題については、<a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">Blueskyサポート</a>に直接お問い合わせください。", 31 + 32 + "footer.link.bluesky": "bluesky", 33 + "footer.link.community": "コミュニティ", 34 + "footer.link.terms": "利用規約", 35 + "footer.link.privacy": "プライバシー", 36 + "footer.link.contact": "お問い合わせ", 37 + 38 + "privacy.title": "プライバシーポリシー – Orbyt", 39 + "privacy.heading": "プライバシーポリシー", 40 + "privacy.last_updated": "最終更新日:2026年2月2日", 41 + "privacy.logo.alt": "Orbyt", 42 + 43 + "terms.title": "利用規約 – Orbyt", 44 + "terms.heading": "利用規約", 45 + "terms.last_updated": "最終更新日:2026年2月2日", 46 + "terms.logo.alt": "Orbyt", 47 + 48 + "profile.install_cta": "アプリを入手", 49 + "profile.empty_feed": "まだ何もありません...", 50 + "profile.load_more": "もっと見る", 51 + "profile.loading": "読み込み中...", 52 + "profile.breadcrumb.home": "ホーム", 53 + 54 + "post.install_cta": "アプリを入手", 55 + "post.breadcrumb.home": "ホーム", 56 + "post.caption.more": "もっと", 57 + 58 + "altstore.tip_label": "AltStoreで入手", 59 + "altstore.aria_label": "AltStore PALにorbytソースを追加", 60 + "altstore.dialog.title": "iPhoneでスキャン", 61 + "altstore.dialog.qr_alt": "AltStore PAL用QRコード — iPhoneでスキャン", 62 + "altstore.dialog.hint": "AltStore PALにorbytソースを追加します", 63 + "altstore.dialog.close": "閉じる", 64 + "altstore.dialog.need_altstore": "AltStore初めてですか?", 65 + "altstore.dialog.get_altstore": "詳細を見る" 66 + }
+66
src/i18n/translations/ko.json
··· 1 + { 2 + "index.title": "orbyt – 비디오 커뮤니티", 3 + "index.meta.description": "블루스카이를 위한 새로운 비디오 앱", 4 + "index.motto": "비디오 커뮤니티", 5 + "index.button.ios": "iOS", 6 + "index.button.android_waitlist": "안드로이드 대기 목록", 7 + "index.schema.organization_description": "블루스카이를 위한 새로운 비디오 앱", 8 + 9 + "app.title": "앱 다운로드 – orbyt", 10 + "app.subtitle.desktop": "스캔하여 앱 다운로드", 11 + "app.subtitle.mobile": "앱 다운로드", 12 + "app.qr.alt": "orbyt 앱 다운로드 QR 코드", 13 + "app.qr.caption": "또는 아래에서 직접 다운로드", 14 + "app.button.ios": "iOS", 15 + "app.button.android_waitlist": "안드로이드 대기 목록", 16 + 17 + "404.title": "404 – 페이지를 찾을 수 없음 | Orbyt", 18 + "404.heading": "신호 없음", 19 + "404.message": "이 페이지는 존재하지 않거나 이동되었습니다.", 20 + "404.action.back": "뒤로", 21 + "404.action.home": "홈", 22 + 23 + "contact.title": "문의하기 – Orbyt", 24 + "contact.logo.alt": "Orbyt", 25 + "contact.heading": "문의하기", 26 + "contact.section.general": "일반 문의", 27 + "contact.section.press": "언론 및 미디어 문의", 28 + "contact.section.bugs": "버그 신고", 29 + "contact.section.copyright": "저작권 및 콘텐츠 문제", 30 + "contact.copyright.html": "Orbyt는 블루스카이를 위한 서드파티 클라이언트입니다. 콘텐츠는 Orbyt가 아닌 AT 프로토콜 네트워크에서 호스팅됩니다. 저작권 관련 삭제 요청이나 콘텐츠 관련 법적 문제는 <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">블루스카이 지원팀</a>에 직접 문의하세요.", 31 + 32 + "footer.link.bluesky": "블루스카이", 33 + "footer.link.community": "커뮤니티", 34 + "footer.link.terms": "이용약관", 35 + "footer.link.privacy": "개인정보처리방침", 36 + "footer.link.contact": "문의", 37 + 38 + "privacy.title": "개인정보처리방침 – Orbyt", 39 + "privacy.heading": "개인정보처리방침", 40 + "privacy.last_updated": "최종 업데이트: 2026년 2월 2일", 41 + "privacy.logo.alt": "Orbyt", 42 + 43 + "terms.title": "이용약관 – Orbyt", 44 + "terms.heading": "이용약관", 45 + "terms.last_updated": "최종 업데이트: 2026년 2월 2일", 46 + "terms.logo.alt": "Orbyt", 47 + 48 + "profile.install_cta": "앱 다운로드", 49 + "profile.empty_feed": "아직 아무것도 없어요...", 50 + "profile.load_more": "더 보기", 51 + "profile.loading": "불러오는 중...", 52 + "profile.breadcrumb.home": "홈", 53 + 54 + "post.install_cta": "앱 다운로드", 55 + "post.breadcrumb.home": "홈", 56 + "post.caption.more": "더 보기", 57 + 58 + "altstore.tip_label": "AltStore에서 받기", 59 + "altstore.aria_label": "AltStore PAL에 orbyt 소스 추가", 60 + "altstore.dialog.title": "iPhone으로 스캔하세요", 61 + "altstore.dialog.qr_alt": "AltStore PAL QR 코드 — iPhone으로 스캔", 62 + "altstore.dialog.hint": "AltStore PAL에 orbyt 소스를 추가합니다", 63 + "altstore.dialog.close": "닫기", 64 + "altstore.dialog.need_altstore": "AltStore가 처음이신가요?", 65 + "altstore.dialog.get_altstore": "자세히 알아보기" 66 + }
+66
src/i18n/translations/pt-BR.json
··· 1 + { 2 + "index.title": "orbyt – comunidades de vídeo", 3 + "index.meta.description": "um novo app de vídeo para o bluesky", 4 + "index.motto": "comunidades de vídeo", 5 + "index.button.ios": "iOS", 6 + "index.button.android_waitlist": "Lista de espera Android", 7 + "index.schema.organization_description": "um novo app de vídeo para o bluesky", 8 + 9 + "app.title": "Baixar o app – orbyt", 10 + "app.subtitle.desktop": "Escaneie para baixar o app", 11 + "app.subtitle.mobile": "Baixar o app", 12 + "app.qr.alt": "QR code para baixar o app orbyt", 13 + "app.qr.caption": "ou baixe diretamente abaixo", 14 + "app.button.ios": "iOS", 15 + "app.button.android_waitlist": "Lista de espera Android", 16 + 17 + "404.title": "404 – Página não encontrada | Orbyt", 18 + "404.heading": "Sem sinal", 19 + "404.message": "Esta página não existe ou foi movida.", 20 + "404.action.back": "Voltar", 21 + "404.action.home": "Início", 22 + 23 + "contact.title": "Fale conosco – Orbyt", 24 + "contact.logo.alt": "Orbyt", 25 + "contact.heading": "Fale conosco", 26 + "contact.section.general": "Consultas gerais", 27 + "contact.section.press": "Imprensa e mídia", 28 + "contact.section.bugs": "Relatório de bugs", 29 + "contact.section.copyright": "Direitos autorais e conteúdo", 30 + "contact.copyright.html": "O Orbyt é um cliente de terceiros para o Bluesky. O conteúdo está hospedado na rede AT Protocol, não pelo Orbyt. Para solicitações de remoção por direitos autorais ou problemas legais relacionados ao conteúdo, entre em contato diretamente com o <a href=\"https://bsky.social/about/support\" target=\"_blank\" rel=\"noopener\">suporte do Bluesky</a>.", 31 + 32 + "footer.link.bluesky": "bluesky", 33 + "footer.link.community": "comunidade", 34 + "footer.link.terms": "termos", 35 + "footer.link.privacy": "privacidade", 36 + "footer.link.contact": "contato", 37 + 38 + "privacy.title": "Política de privacidade – Orbyt", 39 + "privacy.heading": "Política de privacidade", 40 + "privacy.last_updated": "Última atualização: 2 de fevereiro de 2026", 41 + "privacy.logo.alt": "Orbyt", 42 + 43 + "terms.title": "Termos de uso – Orbyt", 44 + "terms.heading": "Termos de uso", 45 + "terms.last_updated": "Última atualização: 2 de fevereiro de 2026", 46 + "terms.logo.alt": "Orbyt", 47 + 48 + "profile.install_cta": "Baixar o app", 49 + "profile.empty_feed": "nada aqui, ainda...", 50 + "profile.load_more": "Carregar mais", 51 + "profile.loading": "Carregando...", 52 + "profile.breadcrumb.home": "Início", 53 + 54 + "post.install_cta": "Baixar o app", 55 + "post.breadcrumb.home": "Início", 56 + "post.caption.more": "Mais", 57 + 58 + "altstore.tip_label": "disponível no AltStore", 59 + "altstore.aria_label": "Adicionar fonte do orbyt no AltStore PAL", 60 + "altstore.dialog.title": "Escaneie com seu iPhone", 61 + "altstore.dialog.qr_alt": "QR code para AltStore PAL — escaneie com seu iPhone", 62 + "altstore.dialog.hint": "Adiciona a fonte do orbyt no AltStore PAL", 63 + "altstore.dialog.close": "Fechar", 64 + "altstore.dialog.need_altstore": "Novo no AltStore?", 65 + "altstore.dialog.get_altstore": "Saiba mais" 66 + }
+80
src/i18n/utils.ts
··· 1 + import en from './translations/en.json' 2 + import de from './translations/de.json' 3 + import esMX from './translations/es-MX.json' 4 + import fr from './translations/fr.json' 5 + import ja from './translations/ja.json' 6 + import ko from './translations/ko.json' 7 + import ptBR from './translations/pt-BR.json' 8 + import es419 from './translations/es-419.json' 9 + 10 + export const DEFAULT_LOCALE = 'en' 11 + 12 + export const SUPPORTED_LOCALES = ['de', 'es-MX', 'fr', 'ja', 'ko', 'pt-BR', 'es-419'] as const 13 + export type SupportedLocale = typeof SUPPORTED_LOCALES[number] | 'en' 14 + 15 + export const LOCALE_LABELS: Record<string, string> = { 16 + en: 'English', 17 + de: 'Deutsch', 18 + 'es-MX': 'Español (MX)', 19 + fr: 'Français', 20 + ja: '日本語', 21 + ko: '한국어', 22 + 'pt-BR': 'Português', 23 + 'es-419': 'Español (LA)', 24 + } 25 + 26 + const FALLBACKS: Partial<Record<string, string>> = { 27 + 'es-419': 'es-MX', 28 + } 29 + 30 + const translations: Record<string, Record<string, string>> = { 31 + en, 32 + de, 33 + 'es-MX': esMX, 34 + fr, 35 + ja, 36 + ko, 37 + 'pt-BR': ptBR, 38 + 'es-419': es419, 39 + } 40 + 41 + export function isValidLocale(locale: string): boolean { 42 + return locale === DEFAULT_LOCALE || (SUPPORTED_LOCALES as readonly string[]).includes(locale) 43 + } 44 + 45 + export function useTranslations(locale: string) { 46 + const dict = translations[locale] ?? {} 47 + const fallback = FALLBACKS[locale] 48 + const fallbackDict = fallback ? (translations[fallback] ?? {}) : {} 49 + 50 + return function t(key: string): string { 51 + return (dict as Record<string, string>)[key] 52 + ?? (fallbackDict as Record<string, string>)[key] 53 + ?? en[key as keyof typeof en] 54 + ?? key 55 + } 56 + } 57 + 58 + export function localeToHtmlLang(locale: string): string { 59 + return locale 60 + } 61 + 62 + export function getLocaleUrl(locale: string, path = '/'): string { 63 + const normalizedPath = path.startsWith('/') ? path : `/${path}` 64 + if (locale === DEFAULT_LOCALE) return normalizedPath 65 + return `/${locale}${normalizedPath === '/' ? '/' : normalizedPath}` 66 + } 67 + 68 + export function hreflangLinks(canonicalPath: string): Array<{ hreflang: string; href: string }> { 69 + const base = 'https://getorbyt.com' 70 + const links = [ 71 + { hreflang: 'x-default', href: `${base}${canonicalPath}` }, 72 + { hreflang: 'en', href: `${base}${canonicalPath}` }, 73 + ] 74 + for (const locale of SUPPORTED_LOCALES) { 75 + const bcp47 = locale 76 + const path = `/${locale}${canonicalPath === '/' ? '/' : canonicalPath}` 77 + links.push({ hreflang: bcp47, href: `${base}${path}` }) 78 + } 79 + return links 80 + }
+77
src/middleware.ts
··· 1 + import { defineMiddleware } from 'astro:middleware' 2 + import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from './i18n/utils' 3 + 4 + const LOCALE_COOKIE = 'orbyt-locale' 5 + 6 + function matchAcceptLanguage(header: string | null): string { 7 + if (!header) return DEFAULT_LOCALE 8 + 9 + const langs = header 10 + .split(',') 11 + .map((part) => { 12 + const [code, q] = part.trim().split(';q=') 13 + return { code: code.trim(), q: q ? parseFloat(q) : 1 } 14 + }) 15 + .sort((a, b) => b.q - a.q) 16 + 17 + for (const { code } of langs) { 18 + const lower = code.toLowerCase() 19 + // Exact match (case-insensitive) 20 + const exact = (SUPPORTED_LOCALES as readonly string[]).find( 21 + (l) => l.toLowerCase() === lower 22 + ) 23 + if (exact) return exact 24 + // Base language match: 'de-AT' → 'de', 'pt-PT' → 'pt-BR' 25 + const base = lower.split('-')[0] 26 + const prefix = (SUPPORTED_LOCALES as readonly string[]).find( 27 + (l) => l.toLowerCase().startsWith(base) 28 + ) 29 + if (prefix) return prefix 30 + // English variants 31 + if (base === 'en') return DEFAULT_LOCALE 32 + } 33 + 34 + return DEFAULT_LOCALE 35 + } 36 + 37 + export const onRequest = defineMiddleware(async (context, next) => { 38 + // If this is a rewritten request from a locale-prefix path, skip re-detection 39 + const preResolved = context.request.headers.get('x-orbyt-locale') 40 + if (preResolved && isValidLocale(preResolved)) { 41 + context.locals.locale = preResolved 42 + return next() 43 + } 44 + 45 + const { pathname } = new URL(context.request.url) 46 + const segments = pathname.split('/').filter(Boolean) 47 + const first = segments[0] 48 + 49 + // If path starts with a supported locale prefix, consume it and rewrite internally 50 + if (first && (SUPPORTED_LOCALES as readonly string[]).includes(first)) { 51 + context.locals.locale = first 52 + const rest = segments.slice(1).join('/') 53 + const rewritePath = rest ? `/${rest}` : '/' 54 + const rewriteUrl = new URL(rewritePath, context.request.url) 55 + const headers = new Headers(context.request.headers) 56 + headers.set('x-orbyt-locale', first) 57 + return context.rewrite(new Request(rewriteUrl, { headers })) 58 + } 59 + 60 + // No locale prefix — detect from cookie then Accept-Language 61 + const cookieVal = context.cookies.get(LOCALE_COOKIE)?.value 62 + let locale: string 63 + 64 + if (cookieVal && isValidLocale(cookieVal)) { 65 + locale = cookieVal 66 + } else { 67 + locale = matchAcceptLanguage(context.request.headers.get('Accept-Language')) 68 + } 69 + 70 + // Redirect root to locale prefix when non-English is detected/preferred 71 + if (pathname === '/' && locale !== DEFAULT_LOCALE) { 72 + return context.redirect(`/${locale}/`, 302) 73 + } 74 + 75 + context.locals.locale = locale 76 + return next() 77 + })
+12 -8
src/pages/404.astro
··· 1 1 --- 2 - export const prerender = true; 2 + export const prerender = false; 3 3 4 4 import DocumentColorScheme from '../components/DocumentColorScheme.astro'; 5 5 import SiteFooter from '../components/SiteFooter.astro'; 6 6 import { SITE_DOCUMENT } from '../utils/site-document-theme'; 7 + import { useTranslations, localeToHtmlLang } from '../i18n/utils'; 8 + 9 + const locale = Astro.locals.locale ?? 'en'; 10 + const t = useTranslations(locale); 7 11 --- 8 12 9 13 <!doctype html> 10 - <html lang="en" class="orbyt-doc-dark"> 14 + <html lang={localeToHtmlLang(locale)} class="orbyt-doc-dark"> 11 15 <head> 12 16 <meta charset="UTF-8"> 13 17 <DocumentColorScheme ··· 16 20 tileColor={SITE_DOCUMENT.orbytBlack} 17 21 /> 18 22 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 19 - <title>404 - Page Not Found | Orbyt</title> 23 + <title>{t('404.title')}</title> 20 24 21 25 <!-- Critical: Load fonts immediately, before any external resources --> 22 26 <link rel="stylesheet" href="/css/fonts.css"> ··· 172 176 <div class="container"> 173 177 <main class="main-content"> 174 178 <div class="tv-static" aria-hidden="true"></div> 175 - <h1 class="error-title">No Signal</h1> 176 - <p class="error-message">This page doesn't exist or has been moved.</p> 179 + <h1 class="error-title">{t('404.heading')}</h1> 180 + <p class="error-message">{t('404.message')}</p> 177 181 <div class="error-actions"> 178 - <a href="#" onclick="window.history.back(); return false;" class="nav-button">Go Back</a> 179 - <a href="/" class="nav-button nav-button--secondary">Homepage</a> 182 + <a href="#" onclick="window.history.back(); return false;" class="nav-button">{t('404.action.back')}</a> 183 + <a href="/" class="nav-button nav-button--secondary">{t('404.action.home')}</a> 180 184 </div> 181 185 </main> 182 186 183 - <SiteFooter /> 187 + <SiteFooter locale={locale} /> 184 188 </div> 185 189 </body> 186 190 </html>
+16 -9
src/pages/@[handle].astro
··· 9 9 import { getVideoAmbientBackdropInlineStyle } from '../utils/video-ambient-backdrop'; 10 10 import DocumentColorScheme from '../components/DocumentColorScheme.astro'; 11 11 import { ClientRouter } from 'astro:transitions'; 12 + import { useTranslations, localeToHtmlLang } from '../i18n/utils'; 12 13 13 14 const { handle } = Astro.params; 14 15 ··· 64 65 65 66 // Cache at Cloudflare edge: 60s fresh, serve stale up to 5min while revalidating 66 67 Astro.response.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300'); 68 + 69 + const locale = Astro.locals.locale ?? 'en'; 70 + const t = useTranslations(locale); 67 71 --- 68 72 69 73 <!doctype html> 70 - <html lang="en" class={uiColorScheme === 'dark' ? 'orbyt-doc-dark' : 'orbyt-doc-light'}> 74 + <html lang={localeToHtmlLang(locale)} class={uiColorScheme === 'dark' ? 'orbyt-doc-dark' : 'orbyt-doc-light'}> 71 75 72 76 <head> 73 77 <meta charset="utf-8"> ··· 217 221 <span class="logo-text">orbyt</span> 218 222 </a> 219 223 <a class='install-button mobile' href="/ios"> 220 - Get the app 224 + {t('profile.install_cta')} 221 225 <svg class="svg-button" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg"> 222 226 <path d="M1.93892 19.9988L18.9736 2.96362" stroke="currentColor" stroke-width="3.993"/> 223 227 <path d="M19 16.7368L19 2.93875L5.20193 2.93875" stroke="currentColor" stroke-width="3.993"/> ··· 256 260 257 261 <div class="install-button-wrapper desktop"> 258 262 <a class='install-button' href="/ios"> 259 - Get the app 263 + {t('profile.install_cta')} 260 264 <svg class="svg-button" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg"> 261 265 <path d="M1.93892 19.9988L18.9736 2.96362" stroke="currentColor" stroke-width="3.993"/> 262 266 <path d="M19 16.7368L19 2.93875L5.20193 2.93875" stroke="currentColor" stroke-width="3.993"/> ··· 281 285 )) : ( 282 286 <div class="empty-feed"> 283 287 <div class="tv-static" aria-hidden="true"></div> 284 - <p class="empty-feed-message">nothing here, yet...</p> 288 + <p class="empty-feed-message">{t('profile.empty_feed')}</p> 285 289 </div> 286 290 )} 287 291 {nextCursor && ( 288 292 <footer> 289 - <a id="load-more">Load more</a> 293 + <a id="load-more" data-load-more-text={t('profile.load_more')} data-loading-text={t('profile.loading')}>{t('profile.load_more')}</a> 290 294 </footer> 291 295 )} 292 296 </div> ··· 311 315 const handle = profileEl.dataset.handle; 312 316 let cursor = profileEl.dataset.cursor; 313 317 318 + const loadMoreText = loadMoreBtn.dataset.loadMoreText || 'Load more'; 319 + const loadingText = loadMoreBtn.dataset.loadingText || 'Loading...'; 320 + 314 321 loadMoreBtn.addEventListener('click', async (e) => { 315 322 e.preventDefault(); 316 323 if (!cursor) return; 317 - 318 - loadMoreBtn.textContent = 'Loading...'; 324 + 325 + loadMoreBtn.textContent = loadingText; 319 326 320 327 try { 321 328 const { data, error } = await loadMorePosts(handle ?? '', cursor); ··· 354 361 profileEl.dataset.cursor = cursor || ''; 355 362 356 363 if (cursor) { 357 - loadMoreBtn.textContent = 'Load more'; 364 + loadMoreBtn.textContent = loadMoreText; 358 365 } else { 359 366 loadMoreBtn.parentElement?.remove(); 360 367 } 361 368 } catch (error) { 362 369 console.error('Error loading more:', error); 363 - loadMoreBtn.textContent = 'Load more'; 370 + loadMoreBtn.textContent = loadMoreText; 364 371 } 365 372 }); 366 373 }
+8 -4
src/pages/@[handle]/[postId].astro
··· 7 7 import { SITE_DOCUMENT } from '../../utils/site-document-theme'; 8 8 import { getVideoAmbientBackdropInlineStyle } from '../../utils/video-ambient-backdrop'; 9 9 import { ClientRouter } from 'astro:transitions'; 10 + import { useTranslations, localeToHtmlLang } from '../../i18n/utils'; 10 11 11 12 const { handle, postId } = Astro.params; 12 13 // handle is "wsj.com", postId is "3mc3tjpupzo2i" ··· 89 90 90 91 const videoAmbientBackdropStyle = getVideoAmbientBackdropInlineStyle(thumbnail); 91 92 93 + const locale = Astro.locals.locale ?? 'en'; 94 + const t = useTranslations(locale); 95 + 92 96 // Title: "post by @[handle]" 93 97 const finalOgTitle = `post by @${authorHandle}`; 94 98 --- 95 99 96 100 <!doctype html> 97 - <html lang="en" class="orbyt-doc-dark"> 101 + <html lang={localeToHtmlLang(locale)} class="orbyt-doc-dark"> 98 102 99 103 <head> 100 104 <meta charset="utf-8"> ··· 386 390 </div> 387 391 388 392 <a class='install-button' href="/ios"> 389 - Get the app 393 + {t('post.install_cta')} 390 394 <svg class="svg-button" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg"> 391 395 <path d="M1.93892 19.9988L18.9736 2.96362" stroke-width="3.993"/> 392 396 <path d="M19 16.7368L19 2.93875L5.20193 2.93875" stroke-width="3.993"/> ··· 424 428 aria-expanded="false" 425 429 aria-controls="post-caption-mobile" 426 430 > 427 - More 431 + {t('post.caption.more')} 428 432 </button> 429 433 </div> 430 434 </div> ··· 466 470 </div> 467 471 468 472 <a class='install-button' href="/ios"> 469 - Get the app 473 + {t('post.install_cta')} 470 474 <svg class="svg-button" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg"> 471 475 <path d="M1.93892 19.9988L18.9736 2.96362" stroke-width="3.993"/> 472 476 <path d="M19 16.7368L19 2.93875L5.20193 2.93875" stroke-width="3.993"/>
+12 -9
src/pages/app.astro
··· 7 7 iosDownloadHrefFromOptions, 8 8 } from '../utils/ios-distribution'; 9 9 import { SITE_DOCUMENT } from '../utils/site-document-theme'; 10 + import { useTranslations, localeToHtmlLang } from '../i18n/utils'; 10 11 11 12 export const prerender = false; 12 13 14 + const locale = Astro.locals.locale ?? 'en'; 15 + const t = useTranslations(locale); 13 16 const appUrl = 'https://getorbyt.com/app'; 14 17 const qrCodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&bgcolor=000000&color=f3f5fe&data=${encodeURIComponent(appUrl)}`; 15 18 const iosDownload = getIosDownloadOptionsFromRequest(Astro.request); ··· 17 20 const showAltstorePalPeek = iosDownload.primary === 'altstore'; 18 21 --- 19 22 <!doctype html> 20 - <html lang="en" class="orbyt-doc-dark"> 23 + <html lang={localeToHtmlLang(locale)} class="orbyt-doc-dark"> 21 24 <head> 22 25 <meta charset="utf-8"> 23 26 <DocumentColorScheme ··· 29 32 <link rel="stylesheet" href="/css/caveat.css" /> 30 33 <link rel="stylesheet" href="/css/klee-one.css" /> 31 34 <meta name="viewport" content="width=device-width, initial-scale=1"> 32 - <title>Get the App - orbyt</title> 35 + <title>{t('app.title')}</title> 33 36 34 37 <style> 35 38 @font-face { ··· 231 234 </svg> 232 235 </a> 233 236 234 - <div class="subtitle subtitle-desktop">scan to get the app</div> 235 - <div class="subtitle subtitle-mobile">get the app</div> 237 + <div class="subtitle subtitle-desktop">{t('app.subtitle.desktop')}</div> 238 + <div class="subtitle subtitle-mobile">{t('app.subtitle.mobile')}</div> 236 239 237 240 <div class="qr-section"> 238 241 <div class="qr-container"> 239 - <img class="qr-code" src={qrCodeUrl} alt="QR code to download orbyt app" /> 242 + <img class="qr-code" src={qrCodeUrl} alt={t('app.qr.alt')} /> 240 243 </div> 241 - <div class="qr-label">or download directly below</div> 244 + <div class="qr-label">{t('app.qr.caption')}</div> 242 245 </div> 243 246 244 247 <div class="buttons"> ··· 248 251 { 'download-button-wrap--pal': showAltstorePalPeek }, 249 252 ]} 250 253 > 251 - {showAltstorePalPeek && <AltstorePalPeek href={iosHref} />} 252 - <a href="/ios" class="download-button">iOS</a> 254 + {showAltstorePalPeek && <AltstorePalPeek href={iosHref} locale={locale} />} 255 + <a href="/ios" class="download-button">{t('app.button.ios')}</a> 253 256 </div> 254 - <a href="/android" class="download-button">Android Waitlist</a> 257 + <a href="/android" class="download-button">{t('app.button.android_waitlist')}</a> 255 258 </div> 256 259 </div> 257 260 </div>
+15 -11
src/pages/contact.astro
··· 1 1 --- 2 - export const prerender = true; 2 + export const prerender = false; 3 3 4 4 import DocumentColorScheme from '../components/DocumentColorScheme.astro'; 5 5 import SiteFooter from '../components/SiteFooter.astro'; 6 6 import { SITE_DOCUMENT } from '../utils/site-document-theme'; 7 + import { useTranslations, localeToHtmlLang } from '../i18n/utils'; 8 + 9 + const locale = Astro.locals.locale ?? 'en'; 10 + const t = useTranslations(locale); 7 11 --- 8 12 <!DOCTYPE html> 9 - <html lang="en" class="orbyt-doc-dark"> 13 + <html lang={localeToHtmlLang(locale)} class="orbyt-doc-dark"> 10 14 <head> 11 15 <meta charset="UTF-8"> 12 16 <DocumentColorScheme ··· 15 19 tileColor={SITE_DOCUMENT.orbytBlack} 16 20 /> 17 21 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 18 - <title>Contact Us - Orbyt</title> 22 + <title>{t('contact.title')}</title> 19 23 20 24 <link rel="preload" href="/fonts/Figtree/Figtree-VariableFont_wght.woff2" as="font" type="font/woff2" crossorigin> 21 25 ··· 32 36 <div class="site-doc"> 33 37 <header class="site-doc__masthead"> 34 38 <a href="/" class="site-doc__logo"> 35 - <img src="/images/orbyt-logo.svg" alt="Orbyt"> 39 + <img src="/images/orbyt-logo.svg" alt={t('contact.logo.alt')}> 36 40 </a> 37 - <h1>Contact Us</h1> 41 + <h1>{t('contact.heading')}</h1> 38 42 </header> 39 43 40 44 <main class="site-doc__prose"> 41 45 42 - <h2>For general inquiries</h2> 46 + <h2>{t('contact.section.general')}</h2> 43 47 <p><a href="mailto:hello@getorbyt.com">hello@getorbyt.com</a></p> 44 48 45 - <h2>Press &amp; media requests</h2> 49 + <h2>{t('contact.section.press')}</h2> 46 50 <p><a href="mailto:press@getorbyt.com">press@getorbyt.com</a></p> 47 51 48 - <h2>Bug reports</h2> 52 + <h2>{t('contact.section.bugs')}</h2> 49 53 <p><a href="mailto:support@getorbyt.com">support@getorbyt.com</a></p> 50 54 51 - <h2>Copyright and content issues</h2> 52 - <p>Orbyt is a third-party client for Bluesky. Content is hosted on the AT Protocol network, not by Orbyt. For copyright takedowns or content-related legal issues, please contact <a href="https://bsky.social/about/support" target="_blank" rel="noopener">Bluesky Support</a> directly.</p> 55 + <h2>{t('contact.section.copyright')}</h2> 56 + <p set:html={t('contact.copyright.html')} /> 53 57 </main> 54 58 55 - <SiteFooter /> 59 + <SiteFooter locale={locale} currentPath="/contact" /> 56 60 </div> 57 61 58 62 </body>
+18 -12
src/pages/index.astro
··· 9 9 iosDownloadHrefFromOptions, 10 10 } from '../utils/ios-distribution'; 11 11 import { SITE_DOCUMENT } from '../utils/site-document-theme'; 12 + import { useTranslations, hreflangLinks, localeToHtmlLang } from '../i18n/utils'; 12 13 14 + const locale = Astro.locals.locale ?? 'en'; 15 + const t = useTranslations(locale); 13 16 const iosDownload = getIosDownloadOptionsFromRequest(Astro.request); 14 17 const iosHref = iosDownloadHrefFromOptions(iosDownload); 15 18 const showAltstorePalPeek = iosDownload.primary === 'altstore'; 16 19 --- 17 20 18 21 <!doctype html> 19 - <html lang="en" class="orbyt-doc-dark"> 22 + <html lang={localeToHtmlLang(locale)} class="orbyt-doc-dark"> 20 23 <head> 21 24 <meta charset="utf-8"> 22 25 <DocumentColorScheme ··· 29 32 <link rel="stylesheet" href="/css/caveat.css" /> 30 33 <link rel="stylesheet" href="/css/klee-one.css" /> 31 34 <meta name="google-site-verification" content="4xaimUg28uLCqKwsPNIz6QeG_tfnLlAdHgS5bC_90JU"/> 32 - <title>orbyt - video communities</title> 33 - <meta name="description" content="a new video app for bluesky"> 35 + {hreflangLinks('/').map(({ hreflang, href }) => ( 36 + <link rel="alternate" hreflang={hreflang} href={href} /> 37 + ))} 38 + <title>{t('index.title')}</title> 39 + <meta name="description" content={t('index.meta.description')}> 34 40 <meta name="viewport" content="width=device-width, initial-scale=1"> 35 41 36 42 <link rel="preload" href="/fonts/Figtree/Figtree-VariableFont_wght.woff2" as="font" type="font/woff2" crossorigin> 37 43 <link rel="preload" href="/fonts/Caveat/Caveat-latin-400.woff2" as="font" type="font/woff2" crossorigin> 38 44 39 - <meta property="og:title" content="orbyt - video communities"/> 40 - <meta property="og:description" content="a new video app for bluesky"/> 45 + <meta property="og:title" content={t('index.title')}/> 46 + <meta property="og:description" content={t('index.meta.description')}/> 41 47 <meta property="og:type" content="website"/> 42 48 <meta property="og:url" content="https://getorbyt.com/"/> 43 49 <meta property="og:image" content="https://getorbyt.com/images/orbyt-logotype.png"/> 44 50 <meta property="og:image:type" content="image/png"/> 45 51 46 52 <meta name="twitter:card" content="summary_large_image"/> 47 - <meta name="twitter:title" content="orbyt - video communities"/> 48 - <meta name="twitter:description" content="a new video app for bluesky"/> 53 + <meta name="twitter:title" content={t('index.title')}/> 54 + <meta name="twitter:description" content={t('index.meta.description')}/> 49 55 <meta name="twitter:image" content="https://getorbyt.com/images/orbyt-logotype.png"/> 50 56 51 57 <script type="application/ld+json" is:inline> ··· 258 264 </div> 259 265 </div> 260 266 <div class="home-motto"> 261 - <p class="motto-text">video communities</p> 267 + <p class="motto-text">{t('index.motto')}</p> 262 268 </div> 263 269 <div class="home-actions"> 264 270 <div ··· 267 273 { 'download-button-wrap--pal': showAltstorePalPeek }, 268 274 ]} 269 275 > 270 - {showAltstorePalPeek && <AltstorePalPeek href={iosHref} />} 271 - <a href="/ios" class="download-button">iOS</a> 276 + {showAltstorePalPeek && <AltstorePalPeek href={iosHref} locale={locale} />} 277 + <a href="/ios" class="download-button">{t('index.button.ios')}</a> 272 278 </div> 273 - <a href="/android" class="download-button">Android Waitlist</a> 279 + <a href="/android" class="download-button">{t('index.button.android_waitlist')}</a> 274 280 </div> 275 281 </main> 276 282 277 - <SiteFooter /> 283 + <SiteFooter locale={locale} currentPath="/" /> 278 284 </div> 279 285 </body> 280 286 </html>
+11 -7
src/pages/privacy.astro
··· 1 1 --- 2 - export const prerender = true; 2 + export const prerender = false; 3 3 4 4 import DocumentColorScheme from '../components/DocumentColorScheme.astro'; 5 5 import SiteFooter from '../components/SiteFooter.astro'; 6 6 import { SITE_DOCUMENT } from '../utils/site-document-theme'; 7 + import { useTranslations, localeToHtmlLang } from '../i18n/utils'; 8 + 9 + const locale = Astro.locals.locale ?? 'en'; 10 + const t = useTranslations(locale); 7 11 --- 8 12 <!DOCTYPE html> 9 - <html lang="en" class="orbyt-doc-dark"> 13 + <html lang={localeToHtmlLang(locale)} class="orbyt-doc-dark"> 10 14 <head> 11 15 <meta charset="UTF-8"> 12 16 <DocumentColorScheme ··· 15 19 tileColor={SITE_DOCUMENT.orbytBlack} 16 20 /> 17 21 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 18 - <title>Privacy Policy - Orbyt</title> 22 + <title>{t('privacy.title')}</title> 19 23 20 24 <link rel="preload" href="/fonts/Figtree/Figtree-VariableFont_wght.woff2" as="font" type="font/woff2" crossorigin> 21 25 ··· 33 37 <div class="site-doc"> 34 38 <header class="site-doc__masthead"> 35 39 <a href="/" class="site-doc__logo"> 36 - <img src="/images/orbyt-logo.svg" alt="Orbyt"> 40 + <img src="/images/orbyt-logo.svg" alt={t('privacy.logo.alt')}> 37 41 </a> 38 - <h1>Privacy Policy</h1> 39 - <p class="site-doc__subtitle">Last Updated: February 2, 2026</p> 42 + <h1>{t('privacy.heading')}</h1> 43 + <p class="site-doc__subtitle">{t('privacy.last_updated')}</p> 40 44 </header> 41 45 42 46 <main class="site-doc__prose"> ··· 175 179 <p>If you have questions about this Privacy Policy, visit our <a href="/contact">contact page</a>.</p> 176 180 </main> 177 181 178 - <SiteFooter /> 182 + <SiteFooter locale={locale} currentPath="/privacy" /> 179 183 </div> 180 184 181 185 </body>
+11 -7
src/pages/terms.astro
··· 1 1 --- 2 - export const prerender = true; 2 + export const prerender = false; 3 3 4 4 import DocumentColorScheme from '../components/DocumentColorScheme.astro'; 5 5 import SiteFooter from '../components/SiteFooter.astro'; 6 6 import { SITE_DOCUMENT } from '../utils/site-document-theme'; 7 + import { useTranslations, localeToHtmlLang } from '../i18n/utils'; 8 + 9 + const locale = Astro.locals.locale ?? 'en'; 10 + const t = useTranslations(locale); 7 11 --- 8 12 <!DOCTYPE html> 9 - <html lang="en" class="orbyt-doc-dark"> 13 + <html lang={localeToHtmlLang(locale)} class="orbyt-doc-dark"> 10 14 <head> 11 15 <meta charset="UTF-8"> 12 16 <DocumentColorScheme ··· 15 19 tileColor={SITE_DOCUMENT.orbytBlack} 16 20 /> 17 21 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 18 - <title>Terms of Use - Orbyt</title> 22 + <title>{t('terms.title')}</title> 19 23 20 24 <link rel="preload" href="/fonts/Figtree/Figtree-VariableFont_wght.woff2" as="font" type="font/woff2" crossorigin> 21 25 ··· 33 37 <div class="site-doc"> 34 38 <header class="site-doc__masthead"> 35 39 <a href="/" class="site-doc__logo"> 36 - <img src="/images/orbyt-logo.svg" alt="Orbyt"> 40 + <img src="/images/orbyt-logo.svg" alt={t('terms.logo.alt')}> 37 41 </a> 38 - <h1>Terms of Use</h1> 39 - <p class="site-doc__subtitle">Last Updated: February 2, 2026</p> 42 + <h1>{t('terms.heading')}</h1> 43 + <p class="site-doc__subtitle">{t('terms.last_updated')}</p> 40 44 </header> 41 45 42 46 <main class="site-doc__prose"> ··· 147 151 <p>If you have any questions about these Terms or Orbyt, visit our <a href="/contact">contact page</a>.</p> 148 152 </main> 149 153 150 - <SiteFooter /> 154 + <SiteFooter locale={locale} currentPath="/terms" /> 151 155 </div> 152 156 153 157 </body>