diff --git a/.github/workflows/blogs_fix_locales_all.yml b/.github/workflows/blogs_fix_locales_all.yml deleted file mode 100644 index 46ddbfd2c..000000000 --- a/.github/workflows/blogs_fix_locales_all.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Fix locales of all blogs - -on: - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' - -jobs: - fix-locales-of-all-blogs: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run blogs:fix_locales_all - - name: Commit changes - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore: fix locales of all blogs" - - name: Push changes - uses: ad-m/github-push-action@master - with: - branch: ${{ github.ref }} - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/blogs_remove_mismatching_locales.yml b/.github/workflows/blogs_remove_mismatching_locales.yml deleted file mode 100644 index 61e8d2d7f..000000000 --- a/.github/workflows/blogs_remove_mismatching_locales.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Remove Mismatching Locales - -on: - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' - -jobs: - remove-mismatching-locales: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run blogs:remove_mismatching_locales - env: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - - name: Commit changes - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore: remove mismatching locales" - - name: Push changes - uses: ad-m/github-push-action@master - with: - branch: ${{ github.ref }} - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/blogs_sync_seobot.yml b/.github/workflows/blogs_sync_seobot.yml index 936225e69..965ec4adb 100644 --- a/.github/workflows/blogs_sync_seobot.yml +++ b/.github/workflows/blogs_sync_seobot.yml @@ -31,14 +31,13 @@ jobs: branch: ${{ github.ref }} github_token: ${{ secrets.GITHUB_TOKEN }} - run: bun run blogs:delete_broken_all - - run: bun run blogs:fix_locales_all - run: bun run fix_code_languages_all - name: Commit changes run: | git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" git add -A - git diff --quiet && git diff --staged --quiet || git commit -m "chore: delete broken blogs, fix locales and code languages" + git diff --quiet && git diff --staged --quiet || git commit -m "chore: delete broken blogs and fix code languages" - name: Push changes uses: ad-m/github-push-action@master with: diff --git a/.github/workflows/blogs_translate_all.yml b/.github/workflows/blogs_translate_all.yml deleted file mode 100644 index 3ea9e1fb9..000000000 --- a/.github/workflows/blogs_translate_all.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Translate All Blogs - -on: - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' - -jobs: - translate-all-blogs: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run blogs:translate_all - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - - name: Commit changes - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore: translate all blogs" - - name: Push changes - uses: ad-m/github-push-action@master - with: - branch: ${{ github.ref }} - github_token: ${{ secrets.GITHUB_TOKEN }} - - run: bun run blogs:delete_broken_all - - run: bun run blogs:fix_locales_all - - run: bun run fix_code_languages_all - - name: Commit changes - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore: delete broken blogs, fix locales and code languages" - - name: Push changes - uses: ad-m/github-push-action@master - with: - branch: ${{ github.ref }} - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/blogs_translate_untranslated.yml b/.github/workflows/blogs_translate_untranslated.yml deleted file mode 100644 index d008fd5b0..000000000 --- a/.github/workflows/blogs_translate_untranslated.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Translate Untranslated Blogs - -on: - workflow_dispatch: - -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' - -jobs: - translate-untranslated-blogs: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v6 - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run blogs:translate_untranslated -- --keep-pushing - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - - name: Commit changes - run: | - git pull - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore: translate untranslated blogs" - - name: Push changes - uses: ad-m/github-push-action@master - with: - branch: ${{ github.ref }} - github_token: ${{ secrets.GITHUB_TOKEN }} - - run: bun run blogs:delete_broken_all - - run: bun run blogs:fix_locales_all - - run: bun run fix_code_languages_all - - name: Commit changes - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "github-actions[bot]" - git add -A - git commit -m "chore: delete broken blogs, fix locales and code languages" - - name: Push changes - uses: ad-m/github-push-action@master - with: - branch: ${{ github.ref }} - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index bf63680db..934099eb2 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -14,8 +14,6 @@ on: - 'apps/shared/**' - 'apps/web/public/**' - 'apps/web/src/config/plugins.ts' - - 'messages/**' - - 'project.inlang/**' - 'configs.json' - 'package.json' - 'bun.lock' @@ -69,7 +67,7 @@ jobs: apps/docs/.astro node_modules/.astro node_modules/.cache - key: ${{ runner.os }}-astro-docs-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('apps/docs/**', 'apps/shared/**', 'apps/web/public/**', 'apps/web/src/config/plugins.ts', 'messages/**', 'project.inlang/**', 'configs.json') }} + key: ${{ runner.os }}-astro-docs-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('apps/docs/**', 'apps/shared/**', 'apps/web/public/**', 'apps/web/src/config/plugins.ts', 'configs.json') }} restore-keys: | ${{ runner.os }}-astro-docs-${{ hashFiles('**/bun.lock') }}- ${{ runner.os }}-astro-docs- diff --git a/.github/workflows/deploy-translation.yml b/.github/workflows/deploy-translation.yml new file mode 100644 index 000000000..539d44b1b --- /dev/null +++ b/.github/workflows/deploy-translation.yml @@ -0,0 +1,47 @@ +name: Deploy Translation Worker + +permissions: + contents: read + +concurrency: + group: deploy-translation + cancel-in-progress: false + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - 'apps/translation-worker/**' + - 'package.json' + - 'bun.lock' + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.3.11 + - name: Cache bun modules + uses: actions/cache@v5 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - run: bun install --frozen-lockfile + - run: bun run ci:verify:translation + - run: bun run deploy:translation + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index a0bd8b0d4..14ac7cd13 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -12,8 +12,6 @@ on: paths: - 'apps/web/**' - 'apps/shared/**' - - 'messages/**' - - 'project.inlang/**' - 'scripts/**' - 'configs.json' - 'package.json' @@ -68,7 +66,7 @@ jobs: apps/web/.astro node_modules/.astro node_modules/.cache - key: ${{ runner.os }}-astro-web-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('apps/web/**', 'apps/shared/**', 'messages/**', 'project.inlang/**', 'scripts/**', 'configs.json') }} + key: ${{ runner.os }}-astro-web-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('apps/web/**', 'apps/shared/**', 'scripts/**', 'configs.json') }} restore-keys: | ${{ runner.os }}-astro-web-${{ hashFiles('**/bun.lock') }}- ${{ runner.os }}-astro-web- diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e9ac15e64..e0120e0e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,6 +24,7 @@ jobs: outputs: web: ${{ steps.filter.outputs.web }} docs: ${{ steps.filter.outputs.docs }} + translation: ${{ steps.filter.outputs.translation }} steps: - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd with: @@ -38,6 +39,7 @@ jobs: { echo "web=true" echo "docs=true" + echo "translation=true" } >> "$GITHUB_OUTPUT" exit 0 fi @@ -62,18 +64,24 @@ jobs: web=false docs=false + translation=false - if matches '^(apps/web/|apps/shared/|messages/|project\.inlang/|scripts/|configs\.json$|package\.json$|bun\.lock$|\.github/workflows/test\.yml$|\.github/workflows/deploy-web\.yml$)'; then + if matches '^(apps/web/|apps/shared/|scripts/|configs\.json$|package\.json$|bun\.lock$|\.github/workflows/test\.yml$|\.github/workflows/deploy-web\.yml$)'; then web=true fi - if matches '^(apps/docs/|apps/shared/|apps/web/public/|apps/web/src/config/plugins\.ts$|messages/|project\.inlang/|configs\.json$|package\.json$|bun\.lock$|\.github/workflows/test\.yml$|\.github/workflows/deploy-docs\.yml$)'; then + if matches '^(apps/docs/|apps/shared/|apps/web/public/|apps/web/src/config/plugins\.ts$|configs\.json$|package\.json$|bun\.lock$|\.github/workflows/test\.yml$|\.github/workflows/deploy-docs\.yml$)'; then docs=true fi + if matches '^(apps/translation-worker/|package\.json$|bun\.lock$|\.github/workflows/test\.yml$|\.github/workflows/deploy-translation\.yml$)'; then + translation=true + fi + { echo "web=$web" echo "docs=$docs" + echo "translation=$translation" } >> "$GITHUB_OUTPUT" web: @@ -107,7 +115,7 @@ jobs: apps/web/.astro node_modules/.astro node_modules/.cache - key: ${{ runner.os }}-astro-web-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('apps/web/**', 'apps/shared/**', 'messages/**', 'project.inlang/**', 'scripts/**', 'configs.json') }} + key: ${{ runner.os }}-astro-web-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('apps/web/**', 'apps/shared/**', 'scripts/**', 'configs.json') }} restore-keys: | ${{ runner.os }}-astro-web-${{ hashFiles('**/bun.lock') }}- ${{ runner.os }}-astro-web- @@ -150,7 +158,7 @@ jobs: apps/docs/.astro node_modules/.astro node_modules/.cache - key: ${{ runner.os }}-astro-docs-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('apps/docs/**', 'apps/shared/**', 'apps/web/public/**', 'apps/web/src/config/plugins.ts', 'messages/**', 'project.inlang/**', 'configs.json') }} + key: ${{ runner.os }}-astro-docs-${{ hashFiles('**/bun.lock') }}-${{ hashFiles('apps/docs/**', 'apps/shared/**', 'apps/web/public/**', 'apps/web/src/config/plugins.ts', 'configs.json') }} restore-keys: | ${{ runner.os }}-astro-docs-${{ hashFiles('**/bun.lock') }}- ${{ runner.os }}-astro-docs- @@ -161,3 +169,27 @@ jobs: ORAMA_CLOUD_API_KEY: ${{ secrets.ORAMA_CLOUD_API_KEY }} ORAMA_CLOUD_ENDPOINT: ${{ secrets.ORAMA_CLOUD_ENDPOINT }} CLOUDFLARE_TURNSTILE_SITE_KEY: ${{ secrets.CLOUDFLARE_TURNSTILE_SITE_KEY }} + + translation: + name: Translation Worker Check + needs: changes + if: needs.changes.outputs.translation == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: 1.3.11 + - name: Cache bun modules + uses: actions/cache@v5 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + - run: bun install --frozen-lockfile + - run: bun run ci:verify:translation diff --git a/CLAUDE.md b/CLAUDE.md index cbf17129a..c0b6f96d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,7 +83,7 @@ When creating or modifying pages, always consider SEO: - Pages: `src/pages/` - Components: `src/components/` - Layouts: `src/layouts/` -- Translations: `src/paraglide/messages.ts` +- UI copy helpers: `apps/web/src/copy/messages.ts`, `apps/docs/src/copy/messages.ts` - SEO helpers: `src/lib/ldJson.ts` - Styles: Tailwind CSS diff --git a/README.md b/README.md index 02027b6c2..035b121e8 100644 --- a/README.md +++ b/README.md @@ -94,18 +94,17 @@ Key folders and files in this repo: │ │ │ └── content.config.ts │ │ ├── astro.config.mjs │ │ └── wrangler.jsonc -│ └── docs/ -│ ├── public -> ../web/public -│ ├── src/ -│ │ ├── components/ -│ │ ├── content/ -│ │ │ ├── docs/ -│ │ │ └── i18n/ -│ │ ├── css/ -│ │ └── content.config.ts -│ ├── astro.config.mjs -│ └── wrangler.jsonc -├── messages/ +│ ├── docs/ +│ │ ├── public -> ../web/public +│ │ ├── src/ +│ │ │ ├── components/ +│ │ │ ├── content/ +│ │ │ │ └── docs/ +│ │ │ ├── css/ +│ │ │ └── content.config.ts +│ │ ├── astro.config.mjs +│ │ └── wrangler.jsonc +│ └── translation-worker/ ├── scripts/ └── package.json ``` @@ -130,24 +129,19 @@ All commands are run from the repo root: | `bun run preview:docs` | Preview the docs locally | | `bun run deploy:web` | Deploy the website worker | | `bun run deploy:docs` | Deploy the docs worker | -| `bun run deploy` | Fails intentionally to prevent deploying both workers together | +| `bun run deploy:translation` | Deploy the localized edge translation worker | +| `bun run deploy` | Fails intentionally; use `deploy:web`, `deploy:docs`, or `deploy:translation` for one worker at a time | | `bun run astro -- --help` | Get help using the Astro CLI | -## Automatic i18n - -The repo uses scripts in `scripts/` to maintain translated content. - -When adding a new locale, the relevant content now lives in: +## Localized Pages -- Website pages and blog content under `apps/web/src` -- Documentation content under `apps/docs/src` +English source content is kept in the web and docs apps. Localized URL paths, canonical metadata, alternates, and sitemap entries are still generated by Astro, while non-English page content is translated at the edge by the translation Worker. -Useful commands: +When updating localized behavior, check: -- `bun run setup:new:locale` -- `bun run docs:translate_all` -- `bun run blogs:translate_all` -- `bun run plugins:translate_all` +- Website source content under `apps/web/src` +- Documentation source content under `apps/docs/src` +- Edge translation logic under `apps/translation-worker/src` ## License diff --git a/_typos.toml b/_typos.toml index 3830579ec..7b4d1dc7a 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,57 +1,5 @@ [files] -# Exclude translated content - these are not English typos extend-exclude = [ - "messages/*.json", - "src/content/i18n/*.json", - "apps/docs/src/content/i18n/*.json", - "src/content/docs/es/**", - "src/content/docs/it/**", - "src/content/docs/ja/**", - "src/content/docs/de/**", - "src/content/docs/fr/**", - "src/content/docs/pt/**", - "src/content/docs/zh/**", - "src/content/docs/ko/**", - "src/content/docs/ru/**", - "src/content/docs/id/**", - "apps/docs/src/content/docs/es/**", - "apps/docs/src/content/docs/it/**", - "apps/docs/src/content/docs/ja/**", - "apps/docs/src/content/docs/de/**", - "apps/docs/src/content/docs/fr/**", - "apps/docs/src/content/docs/pt/**", - "apps/docs/src/content/docs/zh/**", - "apps/docs/src/content/docs/ko/**", - "apps/docs/src/content/docs/ru/**", - "apps/docs/src/content/docs/id/**", - "src/content/plugins-tutorials/es/**", - "src/content/plugins-tutorials/it/**", - "src/content/plugins-tutorials/fr/**", - "src/content/plugins-tutorials/id/**", - "src/content/plugins-tutorials/de/**", - "apps/web/src/content/plugins-tutorials/es/**", - "apps/web/src/content/plugins-tutorials/it/**", - "apps/web/src/content/plugins-tutorials/fr/**", - "apps/web/src/content/plugins-tutorials/id/**", - "apps/web/src/content/plugins-tutorials/de/**", - "src/content/blog/es/**", - "src/content/blog/fr/**", - "src/content/blog/it/**", - "src/content/blog/ja/**", - "src/content/blog/de/**", - "src/content/blog/pt/**", - "src/content/blog/id/**", - "src/content/blog/ko/**", - "src/content/blog/zh-cn/**", - "apps/web/src/content/blog/es/**", - "apps/web/src/content/blog/fr/**", - "apps/web/src/content/blog/it/**", - "apps/web/src/content/blog/ja/**", - "apps/web/src/content/blog/de/**", - "apps/web/src/content/blog/pt/**", - "apps/web/src/content/blog/id/**", - "apps/web/src/content/blog/ko/**", - "apps/web/src/content/blog/zh-cn/**", "public/**/*_tye.png", "apps/web/public/**/*_tye.png", "apps/docs/public/**/*_tye.png", @@ -88,6 +36,9 @@ formatable = "formatable" # NFC event literal from upstream API udid = "udid" # iOS device UDID (technical term) UDID = "UDID" # iOS device UDID (technical term) RTO = "RTO" # Recovery Time Objective +Sanbox = "Sanbox" # Legacy copy key kept for runtime compatibility +framworks = "framworks" # Legacy copy key kept for runtime compatibility +youre = "youre" # Legacy copy key kept for runtime compatibility # OpenSSL command flags passin = "passin" # OpenSSL password input flag diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index 20c4ec89b..7a9763826 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -2,12 +2,14 @@ import starlight from '@astrojs/starlight' import starlightDocSearch from '@astrojs/starlight-docsearch' import { defineConfig } from 'astro/config' import { fileURLToPath } from 'node:url' +import starlightLlmsTxt from 'starlight-llms-txt' import { viteStaticCopy } from 'vite-plugin-static-copy' import config from '../../configs.json' import { buildSharedAstroBaseConfig, buildSharedIntegrations, buildSharedViteConfig } from '../shared/astro-config.mjs' import { buildPluginIcons, getBuildConcurrency, getPageLastModDates, normalizeDirectoryPath } from '../shared/astro-utils.mjs' +import { docsLlmsCustomSets } from './src/config/llmsCustomSets' import { docsSidebar } from './src/config/sidebar.mjs' -import { defaultLocale, localeNames, locales } from './src/services/locale' +import { defaultLocale } from './src/services/locale' const CPU_COUNT = getBuildConcurrency() const SRC_DIR = `${normalizeDirectoryPath(fileURLToPath(new URL('./src/', import.meta.url)))}/` @@ -21,7 +23,6 @@ const SITE_DOMAIN = process.env.BRANCH === 'development' ? config.base_domain.de export default defineConfig({ ...buildSharedAstroBaseConfig({ siteDomain: SITE_DOMAIN, - locales, defaultLocale, cpuCount: CPU_COUNT, build: { @@ -46,8 +47,6 @@ export default defineConfig({ integrations: [ ...buildSharedIntegrations({ pluginIcons, - defaultLocale, - localeNames, pageLastModDates, }), starlight({ @@ -60,6 +59,10 @@ export default defineConfig({ apiKey: '039b8d50eaa068b9ff8726d912c6f388', indexName: 'capgo', }), + starlightLlmsTxt({ + customSets: docsLlmsCustomSets, + details: 'The canonical source documentation is English. Translated language paths are served at request time by the Capgo edge translation worker.', + }), ], disable404Route: true, logo: { diff --git a/apps/docs/package.json b/apps/docs/package.json index bbedb3cd1..13a2d0010 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -8,12 +8,11 @@ "@astrojs/sitemap": "3.7.2", "@astrojs/starlight": "0.38.2", "@astrojs/starlight-docsearch": "0.7.0", - "@inlang/paraglide-js": "2.13.2", "@tailwindcss/vite": "^4.2.1", "astro": "6.1.3", - "astro-i18n-aut": "^0.7.3", "astro-icon": "1.1.5", "sharp": "^0.34.5", + "starlight-llms-txt": "^0.6.1", "starlight-package-managers": "^0.11.1", "tailwindcss": "^4.2.1" }, @@ -26,13 +25,12 @@ }, "scripts": { "sync:public": "bun run ./scripts/sync_public.ts", - "paraglide:compile": "bunx paraglide-js compile --project ../../project.inlang --outdir ./src/paraglide --emit-ts-declarations --silent", "astro": "astro", - "dev": "bun run sync:public && bun run paraglide:compile && astro dev", - "start": "bun run sync:public && bun run paraglide:compile && astro dev", + "dev": "bun run sync:public && astro dev", + "start": "bun run sync:public && astro dev", "clean:build-cache": "rm -rf dist .astro/content-modules.mjs .astro/content-assets.mjs .wrangler/deploy", - "build": "bun run sync:public && bun run paraglide:compile && bun run clean:build-cache && NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=16384} BUILD_CONCURRENCY=${BUILD_CONCURRENCY:-1} UV_THREADPOOL_SIZE=${UV_THREADPOOL_SIZE:-16} astro build", - "check": "bun run sync:public && bun run paraglide:compile && astro check", + "build": "bun run sync:public && bun run clean:build-cache && NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=16384} BUILD_CONCURRENCY=${BUILD_CONCURRENCY:-1} UV_THREADPOOL_SIZE=${UV_THREADPOOL_SIZE:-16} astro build", + "check": "bun run sync:public && astro check", "preview": "astro preview", "preview:worker": "rm -rf .wrangler/deploy && wrangler dev", "deploy": "rm -rf .wrangler/deploy && wrangler deploy" diff --git a/apps/docs/src/components/FrameworkSelector.astro b/apps/docs/src/components/FrameworkSelector.astro index c89bbec7f..506ed3fa7 100644 --- a/apps/docs/src/components/FrameworkSelector.astro +++ b/apps/docs/src/components/FrameworkSelector.astro @@ -1,6 +1,6 @@ --- // FrameworkSelector.astro - Framework selection buttons for conditional steps -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' import { resolveMessageLocale } from '@/services/message-locale' interface FrameworkOption { @@ -19,7 +19,15 @@ const frameworks: FrameworkOption[] = [ { id: 'qwik', nameKey: 'framework_qwik', icon: '/icons/qwik.svg' }, ] -const messageLocale = resolveMessageLocale(Astro.locals.starlightRoute.locale, Astro.currentLocale) +const getRouteLocale = (): string | undefined => { + try { + return Astro.locals.starlightRoute?.locale + } catch { + return undefined + } +} + +const messageLocale = resolveMessageLocale(getRouteLocale(), Astro.currentLocale) const getFrameworkName = (key: string) => { const translations: Record = { diff --git a/apps/docs/src/components/doc/CopyPage.astro b/apps/docs/src/components/doc/CopyPage.astro index 110bbd6e2..b89aeb8dc 100644 --- a/apps/docs/src/components/doc/CopyPage.astro +++ b/apps/docs/src/components/doc/CopyPage.astro @@ -1,6 +1,6 @@ --- import type { StarlightRouteData } from '@astrojs/starlight/route-data' -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' import { resolveMessageLocale } from '@/services/message-locale' const route = Astro.locals.starlightRoute as StarlightRouteData @@ -8,8 +8,6 @@ const { editUrl, id: pageSlug } = route const messageLocale = resolveMessageLocale(route.locale, Astro.currentLocale) const repoSourcePath = route.entry?.filePath ? `apps/docs/${route.entry.filePath}` : '' const markdownSourceUrl = repoSourcePath ? `https://raw.githubusercontent.com/Cap-go/website/refs/heads/main/${repoSourcePath}` : '' -const githubSourceUrl = repoSourcePath ? `https://github.com/Cap-go/website/blob/main/${repoSourcePath}` : '' -const githubRawUrl = githubSourceUrl ? `${githubSourceUrl}?raw=1` : '' const translations = { copyPage: m.copy_page({}, { locale: messageLocale }), @@ -17,8 +15,6 @@ const translations = { copyPageAsMarkdown: m.copy_page_as_markdown({}, { locale: messageLocale }), viewAsMarkdown: m.view_as_markdown({}, { locale: messageLocale }), viewPageAsPlainText: m.view_page_as_plain_text({}, { locale: messageLocale }), - viewRawOnGithub: m.view_raw_on_github({}, { locale: messageLocale }), - openRawOnGithub: m.open_raw_on_github({}, { locale: messageLocale }), openInChatgpt: m.open_in_chatgpt({}, { locale: messageLocale }), openInClaude: m.open_in_claude({}, { locale: messageLocale }), openInPerplexity: m.open_in_perplexity({}, { locale: messageLocale }), @@ -235,7 +231,7 @@ const translations = { -` +} + +function rewriteMetadataAndLinks(html: string, requestUrl: URL, locale: Locale): string { + const basePath = stripLocalePrefix(requestUrl.pathname) + const localizedUrl = localizedAbsoluteUrl(requestUrl, locale, basePath) + let rewritten = updateHtmlLang(html, locale) + + rewritten = removeTagsByAttribute(rewritten, 'link', 'rel', 'alternate') + rewritten = setLinkRel(rewritten, 'canonical', ``) + rewritten = insertAfterFirstTagByAttribute(rewritten, 'link', 'rel', 'canonical', alternateLinks(requestUrl, basePath)) + rewritten = setMetaContent(rewritten, 'property', 'og:url', localizedUrl) + rewritten = setMetaContent(rewritten, 'property', 'twitter:url', localizedUrl) + rewritten = setMetaContent(rewritten, 'name', 'twitter:url', localizedUrl) + rewritten = localizeUrlAttributes(rewritten, locale) + + if (!rewritten.includes('capgo-edge-language-selector')) { + rewritten = insertBeforeClosingTag(rewritten, 'body', languageSelectorScript(locale)) + } + + return rewritten +} + +async function buildTranslatedResponse(request: Request, env: Env, requestUrl: URL, locale: Locale): Promise { + const originResponse = await fetchEnglishOrigin(request, env, requestUrl) + if (isRedirect(originResponse)) return localizeRedirect(originResponse, requestUrl, locale) + if (!isHtmlResponse(originResponse) || !originResponse.ok) return originResponse + + const contentLength = Number.parseInt(originResponse.headers.get('Content-Length') || '0', 10) + if (Number.isFinite(contentLength) && contentLength > MAX_HTML_BYTES) { + return withResponseHeaders(originResponse, 'BYPASS') + } + + const sourceHtml = await originResponse.text() + if (new TextEncoder().encode(sourceHtml).length > MAX_HTML_BYTES) { + return withResponseHeaders(new Response(sourceHtml, originResponse), 'BYPASS') + } + + const translatedHtml = await translateHtml(env, locale, sourceHtml) + const localizedHtml = rewriteMetadataAndLinks(translatedHtml, requestUrl, locale) + const headers = new Headers(originResponse.headers) + headers.set('Content-Type', 'text/html; charset=utf-8') + headers.delete('Content-Length') + return new Response(localizedHtml, { + status: originResponse.status, + statusText: originResponse.statusText, + headers, + }) +} + +async function refreshCache(request: Request, env: Env, requestUrl: URL, locale: Locale, cacheKey: Request): Promise { + const response = await buildTranslatedResponse(request, env, requestUrl, locale) + if (response.ok && isHtmlResponse(response)) { + const cachedResponse = toCachedResponse(response.clone()) + await caches.default.put(cacheKey, cachedResponse) + } + return response +} + +async function serveTranslated(request: Request, env: Env, ctx: ExecutionContext, requestUrl: URL, locale: Locale): Promise { + const cacheKey = cacheKeyFor(requestUrl, locale) + const cachedResponse = await caches.default.match(cacheKey) + const isHead = request.method === 'HEAD' + + if (cachedResponse) { + const translatedAt = readTranslatedAt(cachedResponse) + const isStale = Date.now() - translatedAt > FRESH_MS + if (isStale) { + ctx.waitUntil( + refreshCache(request, env, requestUrl, locale, cacheKey).catch((error) => { + console.error('Failed to refresh translated page', { pathname: requestUrl.pathname, locale, error }) + }), + ) + } + return withResponseHeaders(cachedResponse, isStale ? 'STALE' : 'HIT', isHead) + } + + const translatedResponse = await refreshCache(request, env, requestUrl, locale, cacheKey) + return withResponseHeaders(translatedResponse, 'MISS', isHead) +} + +export default { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { + const requestUrl = new URL(request.url) + const locale = extractLocale(requestUrl.pathname) + + if (!locale) return await fetchEnglishOrigin(request, env, requestUrl) + + if (request.method !== 'GET' && request.method !== 'HEAD') { + return await fetchEnglishOrigin(request, env, requestUrl) + } + + if (shouldBypassTranslation(requestUrl.pathname)) { + return await fetchEnglishOrigin(request, env, requestUrl) + } + + try { + return await serveTranslated(request, env, ctx, requestUrl, locale) + } catch (error) { + console.error('Translation worker failed', { pathname: requestUrl.pathname, locale, error }) + const fallback = await fetchEnglishOrigin(request, env, requestUrl) + if (isHtmlResponse(fallback)) { + const localizedHtml = rewriteMetadataAndLinks(await fallback.text(), requestUrl, locale) + return withResponseHeaders(new Response(localizedHtml, fallback), 'BYPASS', request.method === 'HEAD') + } + return fallback + } + }, +} diff --git a/apps/translation-worker/tsconfig.json b/apps/translation-worker/tsconfig.json new file mode 100644 index 000000000..c3a7570ad --- /dev/null +++ b/apps/translation-worker/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "allowJs": false, + "lib": ["ES2022", "WebWorker"], + "module": "ESNext", + "moduleResolution": "Bundler", + "noEmit": true, + "strict": true, + "target": "ES2022", + "types": [] + }, + "include": ["src/**/*.ts"] +} diff --git a/apps/translation-worker/wrangler.jsonc b/apps/translation-worker/wrangler.jsonc new file mode 100644 index 000000000..0d62c5078 --- /dev/null +++ b/apps/translation-worker/wrangler.jsonc @@ -0,0 +1,78 @@ +{ + "$schema": "../../node_modules/wrangler/config-schema.json", + "name": "capgo-translation", + "main": "./src/index.ts", + "compatibility_date": "2026-05-01", + "compatibility_flags": ["nodejs_compat"], + "workers_dev": false, + "ai": { + "binding": "AI", + }, + "vars": { + "TRANSLATION_MODEL": "@cf/meta/llama-3.1-8b-instruct", + }, + "services": [ + { + "binding": "WEB", + "service": "capgo-website", + }, + { + "binding": "DOCS", + "service": "capgo-docs", + }, + ], + "observability": { + "enabled": true, + "head_sampling_rate": 1, + }, + "routes": [ + { "pattern": "capgo.app/de", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/de/*", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/es", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/es/*", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/fr", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/fr/*", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/id", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/id/*", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/it", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/it/*", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/ja", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/ja/*", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/ko", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/ko/*", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/zh", "zone_name": "capgo.app" }, + { "pattern": "capgo.app/zh/*", "zone_name": "capgo.app" }, + ], + "env": { + "development": { + "services": [ + { + "binding": "WEB", + "service": "capgo-website-development", + }, + { + "binding": "DOCS", + "service": "capgo-docs-development", + }, + ], + "routes": [ + { "pattern": "development.capgo.app/de", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/de/*", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/es", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/es/*", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/fr", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/fr/*", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/id", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/id/*", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/it", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/it/*", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/ja", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/ja/*", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/ko", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/ko/*", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/zh", "zone_name": "capgo.app" }, + { "pattern": "development.capgo.app/zh/*", "zone_name": "capgo.app" }, + ], + }, + }, +} diff --git a/apps/web/astro.config.mjs b/apps/web/astro.config.mjs index bcab798ed..7fa88cee3 100644 --- a/apps/web/astro.config.mjs +++ b/apps/web/astro.config.mjs @@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url' import config from '../../configs.json' import { buildSharedAstroBaseConfig, buildSharedIntegrations, buildSharedViteConfig } from '../shared/astro-config.mjs' import { buildPluginIcons, getBuildConcurrency, getPageLastModDates, normalizeDirectoryPath } from '../shared/astro-utils.mjs' -import { defaultLocale, localeNames, locales } from './src/services/locale' +import { defaultLocale } from './src/services/locale' const CPU_COUNT = getBuildConcurrency() const SRC_DIR = `${normalizeDirectoryPath(fileURLToPath(new URL('./src/', import.meta.url)))}/` @@ -11,23 +11,15 @@ const PUBLIC_DIR = normalizeDirectoryPath(fileURLToPath(new URL('./public/', imp const pageLastModDates = getPageLastModDates() const pluginIcons = buildPluginIcons('src/config/plugins.ts') const SITE_DOMAIN = process.env.BRANCH === 'development' ? config.base_domain.development : config.base_domain.prod -const I18N_MIDDLEWARE_SHIM = fileURLToPath(new URL('./src/lib/astro-i18n-aut-middleware.ts', import.meta.url)) const viteConfig = buildSharedViteConfig({ srcDir: SRC_DIR, publicDir: PUBLIC_DIR, cpuCount: CPU_COUNT, - ssrNoExternal: ['astro-i18n-aut', 'astro-i18n-aut/middleware'], -}) - -viteConfig.resolve.alias.push({ - find: 'astro-i18n-aut/middleware', - replacement: I18N_MIDDLEWARE_SHIM, }) export default defineConfig({ ...buildSharedAstroBaseConfig({ siteDomain: SITE_DOMAIN, - locales, defaultLocale, cpuCount: CPU_COUNT, build: { @@ -36,8 +28,6 @@ export default defineConfig({ }), integrations: buildSharedIntegrations({ pluginIcons, - defaultLocale, - localeNames, pageLastModDates, }), vite: viteConfig, diff --git a/apps/web/package.json b/apps/web/package.json index 6a1f65fe7..59289a0b1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,13 +11,12 @@ }, "scripts": { "validate:agent-skills": "bun ../../scripts/validate-agent-skills-digests.mjs", - "paraglide:compile": "bunx paraglide-js compile --project ../../project.inlang --outdir ./src/paraglide --emit-ts-declarations --silent", "astro": "astro", - "dev": "bun run paraglide:compile && NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=16384} astro dev", - "start": "bun run paraglide:compile && NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=16384} astro dev", + "dev": "NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=16384} astro dev", + "start": "NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=16384} astro dev", "clean:build-cache": "rm -rf dist .astro/content-modules.mjs .astro/content-assets.mjs", - "build": "bun run paraglide:compile && bun run clean:build-cache && NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=16384} BUILD_CONCURRENCY=${BUILD_CONCURRENCY:-1} UV_THREADPOOL_SIZE=${UV_THREADPOOL_SIZE:-16} astro build && rm -rf dist/.prerender", - "check": "bun run validate:agent-skills && bun run paraglide:compile && astro check", + "build": "bun run clean:build-cache && NODE_OPTIONS=${NODE_OPTIONS:---max-old-space-size=16384} BUILD_CONCURRENCY=${BUILD_CONCURRENCY:-1} UV_THREADPOOL_SIZE=${UV_THREADPOOL_SIZE:-16} astro build && rm -rf dist/.prerender", + "check": "bun run validate:agent-skills && astro check", "preview": "bun run build && bun run preview:worker", "preview:astro": "astro preview", "preview:worker": "wrangler dev", diff --git a/apps/web/src/components/AppflowShutdown.astro b/apps/web/src/components/AppflowShutdown.astro index 6b9277c34..bbd84b496 100644 --- a/apps/web/src/components/AppflowShutdown.astro +++ b/apps/web/src/components/AppflowShutdown.astro @@ -1,24 +1,27 @@ --- -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' import { getRelativeLocaleUrl } from 'astro:i18n' ---- +--- + -
-
+
+
-
+
{m.home_important_update_badge({}, { locale: Astro.locals.locale })}
-

+

{m.home_appflow_shutdown_heading({}, { locale: Astro.locals.locale })},
{m.home_capgo_here_to_stay({}, { locale: Astro.locals.locale })}

-
+

{m.home_appflow_notice_intro({}, { locale: Astro.locals.locale })}

@@ -36,25 +39,31 @@ import { getRelativeLocaleUrl } from 'astro:i18n'
-
+
-
-
+
+
-
-
- +
+
+
Fully Bootstrapped
-
- No VCs - Self funded - Growing - Own resources - Happy customers +
+ No VCs + Self funded + Growing + Own resources + Happy customers
@@ -65,26 +74,27 @@ import { getRelativeLocaleUrl } from 'astro:i18n'
-
-
- +
+
+ - - - - + + + + - - + + - + Revenue - + - + - +
@@ -95,38 +105,42 @@ import { getRelativeLocaleUrl } from 'astro:i18n'
-
-
+
+
-
- +
+
+
- + - + - - + + - + - - + + - + - + - - + + - +
@@ -140,15 +154,15 @@ import { getRelativeLocaleUrl } from 'astro:i18n'
-
-
- Appflow - - - - +
+
+ Appflow + + + + - Capgo + Capgo

@@ -159,27 +173,45 @@ import { getRelativeLocaleUrl } from 'astro:i18n'

-
- Capgo user avatar - Capgo user avatar - Capgo user avatar - Capgo user avatar - Capgo user avatar -
- 1k+ -
+
+ Capgo user avatar + Capgo user avatar + Capgo user avatar + Capgo user avatar + Capgo user avatar +
1k+
-
+
{m.cta_start_migration({}, { locale: Astro.locals.locale })} {m.migration_guide({}, { locale: Astro.locals.locale })} diff --git a/apps/web/src/components/BlogListing.astro b/apps/web/src/components/BlogListing.astro index 49422c709..01686fadd 100644 --- a/apps/web/src/components/BlogListing.astro +++ b/apps/web/src/components/BlogListing.astro @@ -1,7 +1,7 @@ --- import Blog from '@/components/Blog.astro' import Layout from '@/layouts/Layout.astro' -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' import type { CollectionEntry } from 'astro:content' import { getRelativeLocaleUrl } from 'astro:i18n' import { slug as slugify } from 'github-slugger' @@ -23,16 +23,16 @@ if (Astro.locals.runtimeConfig.public.blog_description) content['description'] = --- -
-
-

{m.latest_from_the_blog({}, { locale: Astro.locals.locale })}

+
+
+

{m.latest_from_the_blog({}, { locale: Astro.locals.locale })}

{config.public.blog_description}

-
+
All @@ -40,14 +40,14 @@ if (Astro.locals.runtimeConfig.public.blog_description) content['description'] = uniqueTags.map((val) => ( {val} )) }
-
+
{ posts.map((j, i) => ( diff --git a/apps/web/src/components/BuiltForDevelopers.astro b/apps/web/src/components/BuiltForDevelopers.astro index 929e96830..2869bf4f1 100644 --- a/apps/web/src/components/BuiltForDevelopers.astro +++ b/apps/web/src/components/BuiltForDevelopers.astro @@ -1,12 +1,12 @@ --- -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' import { getRelativeLocaleUrl } from 'astro:i18n' --- -
-
+
+
-

{m.built_for_devs_badge({}, { locale: Astro.locals.locale })}

+

{m.built_for_devs_badge({}, { locale: Astro.locals.locale })}

{m.built_for_devs_title({}, { locale: Astro.locals.locale })}

{m.built_for_devs_subtitle({}, { locale: Astro.locals.locale })} @@ -14,259 +14,339 @@ import { getRelativeLocaleUrl } from 'astro:i18n'

-
- -
-
-
-
-
-
-
-
- 1 - import {'{'} CapacitorUpdater {'}'} from '@capgo/capacitor-updater' -
-
- 2 -
-
- 3 - CapacitorUpdater.notifyAppReady() -
-
- 4 -
-
+
+ +
+
+
+
+
+
+
+ 1 + import + {'{'} + CapacitorUpdater + {'}'} + from + '@capgo/capacitor-updater' +
+
+ 2 +
+
+ 3 + CapacitorUpdater.notifyAppReady() +
+
+ 4 +
+
+
+ + + - - + +
+ +
+ +
- -
- -
- -
- - -
- - -
{m.built_for_devs_code_comment({}, { locale: Astro.locals.locale })}
+ +
+ +
{m.built_for_devs_code_comment({}, { locale: Astro.locals.locale })}
-
-
{m.built_for_devs_phone_update({}, { locale: Astro.locals.locale })}
-
{m.built_for_devs_phone_time({}, { locale: Astro.locals.locale })}
- - -
-
- App Icon -
-
-
+
+
{m.built_for_devs_phone_update({}, { locale: Astro.locals.locale })}
+
{m.built_for_devs_phone_time({}, { locale: Astro.locals.locale })}
- -
+ +
+
+ App Icon
+
+ + +
+
+
-
- +
-
-
-
- TS -
+
+
+
TS
{m.built_for_devs_feature_ts({}, { locale: Astro.locals.locale })}
-
+

{m.built_for_devs_feature_ts_desc({}, { locale: Astro.locals.locale })}

-
-
-
- - - +
+
+
+ + +
{m.built_for_devs_feature_cli({}, { locale: Astro.locals.locale })}
-
+

{m.built_for_devs_feature_cli_desc({}, { locale: Astro.locals.locale })}

-
-
-
- - - +
+
+
+ + +
{m.built_for_devs_feature_channels({}, { locale: Astro.locals.locale })}
-
+

{m.built_for_devs_feature_channels_desc({}, { locale: Astro.locals.locale })}

-
-
-
- - - - +
+
+
+ + + + +
{m.built_for_devs_feature_rollback({}, { locale: Astro.locals.locale })}
-
+

{m.built_for_devs_feature_rollback_desc({}, { locale: Astro.locals.locale })}

-
-
-
- - - +
+
+
+ + +
{m.built_for_devs_feature_analytics({}, { locale: Astro.locals.locale })}
-
+

{m.built_for_devs_feature_analytics_desc({}, { locale: Astro.locals.locale })}

-
-
-
- - - +
+
+
+ + +
{m.built_for_devs_feature_opensource({}, { locale: Astro.locals.locale })}
-
+

{m.built_for_devs_feature_opensource_desc({}, { locale: Astro.locals.locale })}

-
-
-
- - - +
+
+
+ + +
{m.built_for_devs_feature_capacitor({}, { locale: Astro.locals.locale })}
-
+

{m.built_for_devs_feature_capacitor_desc({}, { locale: Astro.locals.locale })}

-
-
-
- - - - +
+
+
+ + + +
{m.built_for_devs_feature_api({}, { locale: Astro.locals.locale })}
-
-

{m.built_for_devs_feature_api_desc_part1({}, { locale: Astro.locals.locale })} {m.built_for_devs_feature_api_docs({}, { locale: Astro.locals.locale })}

+
+

+ {m.built_for_devs_feature_api_desc_part1({}, { locale: Astro.locals.locale })} + {m.built_for_devs_feature_api_docs({}, { locale: Astro.locals.locale })} +

-
-
-
- - - +
+
+
+ + +
{m.built_for_devs_feature_differential({}, { locale: Astro.locals.locale })}
-
-

{m.built_for_devs_feature_differential_desc_part1({}, { locale: Astro.locals.locale })} {m.built_for_devs_feature_differential_learn({}, { locale: Astro.locals.locale })}

+
+

+ {m.built_for_devs_feature_differential_desc_part1({}, { locale: Astro.locals.locale })} + {m.built_for_devs_feature_differential_learn({}, { locale: Astro.locals.locale })} +

-
-
-
- - - - +
+
+
+ + + +
{m.built_for_devs_feature_distribution({}, { locale: Astro.locals.locale })}
-
-

{m.built_for_devs_feature_distribution_desc_part1({}, { locale: Astro.locals.locale })} {m.built_for_devs_feature_distribution_learn({}, { locale: Astro.locals.locale })}

+
+

+ {m.built_for_devs_feature_distribution_desc_part1({}, { locale: Astro.locals.locale })} + {m.built_for_devs_feature_distribution_learn({}, { locale: Astro.locals.locale })} +

-
-
-
- - - +
+
+
+ + +
{m.built_for_devs_feature_soc2({}, { locale: Astro.locals.locale })}
-
-

{m.built_for_devs_feature_soc2_desc_part1({}, { locale: Astro.locals.locale })} {m.built_for_devs_feature_soc2_trust({}, { locale: Astro.locals.locale })}

+
+

+ {m.built_for_devs_feature_soc2_desc_part1({}, { locale: Astro.locals.locale })} + {m.built_for_devs_feature_soc2_trust({}, { locale: Astro.locals.locale })} +

-
-
-
- - - +
+
+
+ + +
{m.built_for_devs_feature_support({}, { locale: Astro.locals.locale })}
-
+

{m.built_for_devs_feature_support_desc({}, { locale: Astro.locals.locale })}

-
-
+
{m.built_for_devs_cta({}, { locale: Astro.locals.locale })} diff --git a/apps/web/src/components/CIExpert.astro b/apps/web/src/components/CIExpert.astro index 60f2d492b..9ca14959e 100644 --- a/apps/web/src/components/CIExpert.astro +++ b/apps/web/src/components/CIExpert.astro @@ -1,18 +1,18 @@ --- -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' --- -
+
-
-
-
+
+
+
-
+
-
- +
+ -
+
-
-
-
- +
+
+
+
@@ -60,10 +60,10 @@ import * as m from '@/paraglide/messages'
-
-
-
- +
+
+
+ -
-
+
+
-
+
{m.most_popular({}, { locale: Astro.locals.locale })}
-
- +
+ @@ -106,10 +106,10 @@ import * as m from '@/paraglide/messages'
-
-
-
- +
+
+
+ -
-
- +
+
+
@@ -144,10 +144,10 @@ import * as m from '@/paraglide/messages'
-
+
-
- +
+ {m.smart_investment_massive_savings({}, { locale: Astro.locals.locale })}

-
+
-
+
{m.managed_solutions({}, { locale: Astro.locals.locale })}
$499/month
{m.appflow_or_similar({}, { locale: Astro.locals.locale })}
-
-
- {m.best_value({}, { locale: Astro.locals.locale })} +
+
+ {m.best_value({}, { locale: Astro.locals.locale })}
{m.our_setup_you_own_it({}, { locale: Astro.locals.locale })}
$2,600 + $30/mo
@@ -178,14 +178,14 @@ import * as m from '@/paraglide/messages'
-
+
{m.five_year_savings({}, { locale: Astro.locals.locale })}
$25,340
{m.massive_cost_reduction({}, { locale: Astro.locals.locale })}
-
+

{m.setup_fee_savings_comparison({}, { locale: Astro.locals.locale })}

{m.own_setup_zero_vendor_lockin({}, { locale: Astro.locals.locale })}

{m.break_even_timeline({}, { locale: Astro.locals.locale })}

@@ -195,10 +195,10 @@ import * as m from '@/paraglide/messages'
{m.setup_ci_cd_now({}, { locale: Astro.locals.locale })} - + diff --git a/apps/web/src/components/Footer.astro b/apps/web/src/components/Footer.astro index 2de6fcf0d..53e7807c0 100644 --- a/apps/web/src/components/Footer.astro +++ b/apps/web/src/components/Footer.astro @@ -1,5 +1,6 @@ --- -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' +import { getLocalizedPath } from '@/services/locale-path' import { locales } from '@/services/locale' import { getRelativeLocaleUrl } from 'astro:i18n' @@ -334,7 +335,9 @@ const navigation: Record = { { locales.map((item) => ( {item.toUpperCase()} diff --git a/apps/web/src/components/GetStarted.astro b/apps/web/src/components/GetStarted.astro index 874f1b8d4..9c789beaf 100644 --- a/apps/web/src/components/GetStarted.astro +++ b/apps/web/src/components/GetStarted.astro @@ -1,25 +1,25 @@ --- -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' import { getRelativeLocaleUrl } from 'astro:i18n' ---
-
+
-
+
- + {m.instant_updates_for_capacitor_apps({}, { locale: Astro.locals.locale })} -

+

{m.instant_updates_for_capacitor_apps_description({}, { locale: Astro.locals.locale })}

{m.get_started_now({}, { locale: Astro.locals.locale })} diff --git a/apps/web/src/components/GlobalInfrastructure.astro b/apps/web/src/components/GlobalInfrastructure.astro index b9bac27ef..0dc2ba2d7 100644 --- a/apps/web/src/components/GlobalInfrastructure.astro +++ b/apps/web/src/components/GlobalInfrastructure.astro @@ -1,25 +1,26 @@ --- -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' import { getRelativeLocaleUrl } from 'astro:i18n' const ctaSubtext = `${m.days_free_trial({}, { locale: Astro.locals.locale })}. ${m.no_credit_card_required({}, { locale: Astro.locals.locale })}` --- -
+
-
+
-
+
{m.global_infra_badge_instant({}, { locale: Astro.locals.locale })}
-

- {m.instant_updates({}, { locale: Astro.locals.locale })}
+

+ {m.instant_updates({}, { locale: Astro.locals.locale })} +
{m.home_worldwide_heading({}, { locale: Astro.locals.locale })}

@@ -29,102 +30,132 @@ const ctaSubtext = `${m.days_free_trial({}, { locale: Astro.locals.locale })}. $
-
- -
-

{m.home_global_network_label({}, { locale: Astro.locals.locale })}

-
- {m.home_locations_active({}, { locale: Astro.locals.locale })} -
-
- {m.home_multi_provider_resilience({}, { locale: Astro.locals.locale })} -
+
+ +
+

{m.home_global_network_label({}, { locale: Astro.locals.locale })}

+
+ + {m.home_locations_active({}, { locale: Astro.locals.locale })}
+
+ + {m.home_multi_provider_resilience({}, { locale: Astro.locals.locale })} +
+
- -
-

{m.global_infra_performance_badge({}, { locale: Astro.locals.locale })}

-
-
- {m.home_avg_latency({}, { locale: Astro.locals.locale })} -
-
- {m.home_uptime({}, { locale: Astro.locals.locale })} -
-
+ +
+

{m.global_infra_performance_badge({}, { locale: Astro.locals.locale })}

+
+
+ + {m.home_avg_latency({}, { locale: Astro.locals.locale })} +
+
+ + {m.home_uptime({}, { locale: Astro.locals.locale })} +
+
-
- -
- Global Network Map -
+
+ +
+ Global Network Map +
+ + - - - -
-
-
- -
-
-
300+
-
{m.home_cities_worldwide({}, { locale: Astro.locals.locale })}
-
{m.home_cities_desc({}, { locale: Astro.locals.locale })}
-
-
+ +
+
+
+ +
+
+
300+
+
{m.home_cities_worldwide({}, { locale: Astro.locals.locale })}
+
{m.home_cities_desc({}, { locale: Astro.locals.locale })}
+
+
- -
-
-
- -
-
-
13,000+
-
{m.home_network_connections({}, { locale: Astro.locals.locale })}
-
{m.home_network_desc({}, { locale: Astro.locals.locale })}
-
-
+ +
+
+
+ +
+
+
13,000+
+
{m.home_network_connections({}, { locale: Astro.locals.locale })}
+
{m.home_network_desc({}, { locale: Astro.locals.locale })}
+
+
- -
-
-
- -
-
-
122+
-
{m.home_countries_covered({}, { locale: Astro.locals.locale })}
-
{m.home_countries_desc({}, { locale: Astro.locals.locale })}
-
-
+ +
+
+
+ +
+
+
122+
+
{m.home_countries_covered({}, { locale: Astro.locals.locale })}
+
{m.home_countries_desc({}, { locale: Astro.locals.locale })}
+
+
- -
-
-
- -
-
-
50ms
-
{m.home_latency_from_users({}, { locale: Astro.locals.locale })}
-
{m.global_infra_latency_ultra_low({}, { locale: Astro.locals.locale })}
-
-
+ +
+
+
+ +
+
+
50ms
+
{m.home_latency_from_users({}, { locale: Astro.locals.locale })}
+
{m.global_infra_latency_ultra_low({}, { locale: Astro.locals.locale })}
+
+
-
+
{m.cta_start_free({}, { locale: Astro.locals.locale })} diff --git a/apps/web/src/components/Header.astro b/apps/web/src/components/Header.astro index 0dd8c1870..0a344e42c 100644 --- a/apps/web/src/components/Header.astro +++ b/apps/web/src/components/Header.astro @@ -1,5 +1,5 @@ --- -import * as m from '@/paraglide/messages' +import m from '@/copy/messages' import { getRelativeLocaleUrl } from 'astro:i18n' import { getTotalStars } from '@/services/github' @@ -9,44 +9,48 @@ const locale = Astro.locals.locale ---
-
+
-
- - {`${brand} +
-