diff --git a/apps/extension/lib/i18n.tsx b/apps/extension/lib/i18n.tsx
index c100412..628b906 100644
--- a/apps/extension/lib/i18n.tsx
+++ b/apps/extension/lib/i18n.tsx
@@ -1,4 +1,4 @@
-import { formatLocale, messagesFor } from "@pluck/i18n";
+import { formatLocale, extensionMessages } from "@pluck/i18n/extension";
import type { ReactNode } from "react";
import { IntlProvider } from "use-intl";
import { locale } from "./t";
@@ -6,5 +6,5 @@ import { locale } from "./t";
export { locale };
export function I18n({ children }: { children: ReactNode }) {
- return {children};
+ return {children};
}
diff --git a/apps/extension/lib/t.ts b/apps/extension/lib/t.ts
index e6d4b9b..fdd18d8 100644
--- a/apps/extension/lib/t.ts
+++ b/apps/extension/lib/t.ts
@@ -1,7 +1,7 @@
-import { formatLocale, matchLocale, messagesFor } from "@pluck/i18n";
+import { formatLocale, matchLocale, extensionMessages } from "@pluck/i18n/extension";
import { createTranslator } from "use-intl/core";
// The browser's language, for the popup, options and the service worker (which has no React).
// Chrome's own UI strings (the manifest) come from public/_locales instead.
export const locale = matchLocale(navigator.languages);
-export const t = createTranslator({ locale: formatLocale(locale), messages: { extension: messagesFor("extension", locale) }, namespace: "extension" });
+export const t = createTranslator({ locale: formatLocale(locale), messages: { extension: extensionMessages(locale) }, namespace: "extension" });
diff --git a/apps/extension/package.json b/apps/extension/package.json
index fce37d8..ce46761 100644
--- a/apps/extension/package.json
+++ b/apps/extension/package.json
@@ -2,7 +2,7 @@
"name": "@pluck/extension",
"description": "The Pluck browser extension: pluck the page you are on, a link, an image or a selection.",
"repository": { "type": "git", "url": "https://github.com/codu-code/pluck.git", "directory": "apps/extension" },
- "version": "0.1.0",
+ "version": "0.2.0",
"private": true,
"license": "MIT",
"type": "module",
diff --git a/apps/extension/store/listing.md b/apps/extension/store/listing.md
index 880c4b6..7c64e1b 100644
--- a/apps/extension/store/listing.md
+++ b/apps/extension/store/listing.md
@@ -17,7 +17,7 @@ What to paste into each field of the developer dashboard. The zip is `pnpm --fil
```
Pluck the page you are on. One click keeps it in your Pluck library, with the text as you see it, so you can read it later, find it again, or ask your AI about it.
-Click Pluck, or press Alt+Shift+P. "Plucked" appears with the title, and the popup lets you add a tag, change the Kind, put it on a List, write a note, or send it straight to Later or the Archive.
+Click Pluck, or press Alt+Shift+P (⌥⇧P on a Mac). "Plucked" appears with the title, and the popup lets you add a tag, change the Kind, put it on a List, write a note, or send it straight to Later or the Archive.
Right-click a link, an image or the page to pluck it without opening it. Select a sentence and press Alt+Shift+H to keep it as a highlight. Pluck all your tabs into a new List at once.
@@ -27,6 +27,8 @@ The Pluck icon fills in on pages already in your library, so you can see at a gl
No connection? Plucks wait on your computer and go when you are back online.
+Pluck speaks English, Spanish, Brazilian Portuguese, French, German, Japanese, Italian, Dutch, Korean and Simplified Chinese, and follows your browser's language.
+
Pluck is open source. Use getpluck.io, or build the extension for a server you run yourself. There is no AI inside Pluck: you connect the AI tools you already use, if you want to.
Needs a free Pluck account at https://getpluck.io.
diff --git a/apps/web/app/(app)/items/[id]/page.tsx b/apps/web/app/(app)/items/[id]/page.tsx
index 84de609..0bf40b7 100644
--- a/apps/web/app/(app)/items/[id]/page.tsx
+++ b/apps/web/app/(app)/items/[id]/page.tsx
@@ -1,7 +1,7 @@
import { isTextKind, withReferral, type Item } from "@pluck/shared-schemas";
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
-import Link from "next/link";
+import { CHROME_STORE_URL } from "@/lib/siteUrls";
import { cache } from "react";
import { notFound } from "next/navigation";
import { DeleteButton, ItemBar } from "@/components/ItemBar";
@@ -68,7 +68,7 @@ export default async function ItemPage({ params }: { params: Promise<{ id: strin
diff --git a/apps/web/lib/site.ts b/apps/web/lib/site.ts
index 450ee75..bdf0119 100644
--- a/apps/web/lib/site.ts
+++ b/apps/web/lib/site.ts
@@ -6,8 +6,6 @@ import { siteAlternates } from "./siteUrls";
// Links the marketing site needs that only exist at launch. Placeholders stay [LIKE THIS] until then.
export const REPO_URL = "https://github.com/codu-code/pluck";
export const CLONE_URL = "https://github.com/codu-code/pluck";
-// shortcut: a "coming soon" page while the listing is in review; swap in the store URL once it is live.
-export const CHROME_STORE_URL = "/chrome";
// shortcut: the TestFlight beta page while the app is in public beta; swap in the App Store URL once it is live.
export const APP_STORE_URL = "/iphone";
// The public beta: anyone with the link can join (up to the limit set in App Store Connect, TestFlight, Public beta).
diff --git a/apps/web/lib/siteUrls.ts b/apps/web/lib/siteUrls.ts
index 56d3b9b..07333bc 100644
--- a/apps/web/lib/siteUrls.ts
+++ b/apps/web/lib/siteUrls.ts
@@ -3,7 +3,9 @@ import { DEFAULT_LOCALE, LOCALES, type Locale } from "@pluck/i18n";
// The marketing site has one URL per language so search engines can list each: English at the root, the others
// under a prefix (/de/connect, /pt-br/blog/pocket-is-gone). proxy.ts reads the prefix; the signed-in app keeps one
// URL and goes by the cookie (i18n/request.ts).
-export const SITE_PAGES = ["/", "/connect", "/open-source", "/iphone", "/chrome", "/support", "/privacy", "/terms", "/blog"];
+export const CHROME_STORE_URL = "https://chromewebstore.google.com/detail/pluck/plcknnahikagnmlliangabeofipkhecc";
+
+export const SITE_PAGES = ["/", "/connect", "/open-source", "/iphone", "/support", "/privacy", "/terms", "/blog"];
export const isSitePath = (path: string) => SITE_PAGES.includes(path) || /^\/blog\/[a-z0-9-]+$/.test(path);
const prefix = (l: Locale) => l.toLowerCase();
diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
index a419cbe..ce4e94a 100644
--- a/apps/web/next-env.d.ts
+++ b/apps/web/next-env.d.ts
@@ -1,7 +1,7 @@
///
///
-import "./.next/dev/types/routes.d.ts";
-import "./.next/dev/types/root-params.d.ts";
+import "./.next/types/routes.d.ts";
+import "./.next/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts
index 5ac9ec6..f3bbbc0 100644
--- a/apps/web/proxy.ts
+++ b/apps/web/proxy.ts
@@ -1,6 +1,6 @@
import { NextResponse, type NextRequest } from "next/server";
import { LOCALE_COOKIE, LOCALE_HEADER, localeFrom } from "@/i18n/request";
-import { cardPage, isSitePath, localePath, splitLocale } from "@/lib/siteUrls";
+import { CHROME_STORE_URL, cardPage, isSitePath, localePath, splitLocale } from "@/lib/siteUrls";
// The marketing site's language URLs (lib/siteUrls.ts). /de/connect is served by the /connect page in German.
// An English URL sends someone who picked another language, or whose browser asks for one, to that language's URL;
@@ -9,6 +9,7 @@ export function proxy(req: NextRequest) {
const { pathname, search } = req.nextUrl;
if (pathname === "/en" || pathname.startsWith("/en/")) return NextResponse.redirect(new URL((pathname.slice(3) || "/") + search, req.url), 308);
const { locale, path } = splitLocale(pathname);
+ if (path === "/chrome") return NextResponse.redirect(CHROME_STORE_URL, 308); // the coming-soon page, before the listing
const headers = new Headers(req.headers);
headers.delete(LOCALE_HEADER); // only a site URL says which language it is
const card = cardPage(path);
diff --git a/apps/web/public/BingSiteAuth.xml b/apps/web/public/BingSiteAuth.xml
new file mode 100644
index 0000000..914a911
--- /dev/null
+++ b/apps/web/public/BingSiteAuth.xml
@@ -0,0 +1,4 @@
+
+
+ B9B28C6BDED698D74C50526F4ADE328F
+
\ No newline at end of file
diff --git a/docs/launch-checklist.md b/docs/launch-checklist.md
index 194c2a3..5dfc814 100644
--- a/docs/launch-checklist.md
+++ b/docs/launch-checklist.md
@@ -22,7 +22,7 @@ Everything between the code in this repo and Pluck being live, in order. Each li
- [ ] Load it unpacked once and go through `docs/loop/BLOCKED.md` (slice 14).
- [ ] `pnpm --filter @pluck/extension zip`. (Firefox is not planned for launch; the build still supports it.)
- [ ] Chrome Web Store listing: description, screenshots, a 30-second video, privacy policy `https://getpluck.io/privacy`, data-use answers (the page you pluck and its address go to your own Pluck server; nothing else, no selling, no ads).
-- [ ] Once the listings are live, put the store URL in `CHROME_STORE_URL` (it points at the `/chrome` coming-soon page until then).
+- [x] Once the listings are live, put the store URL in `CHROME_STORE_URL` (`apps/web/lib/siteUrls.ts`; `/chrome` redirects there).
## iPhone
diff --git a/e2e/design.spec.ts b/e2e/design.spec.ts
index 11ed1ba..a3569e9 100644
--- a/e2e/design.spec.ts
+++ b/e2e/design.spec.ts
@@ -8,7 +8,7 @@ test("snapshot app pages for the design detector", async ({ page }) => {
await signIn(page);
const snap = (name: string) => snapshot(page, name);
- for (const route of ["/", "/chrome", "/iphone", "/connect", "/open-source", "/support", "/privacy", "/terms", "/blog", "/blog/ask-your-ai-what-you-plucked"]) {
+ for (const route of ["/", "/iphone", "/connect", "/open-source", "/support", "/privacy", "/terms", "/blog", "/blog/ask-your-ai-what-you-plucked"]) {
await page.goto(route);
await snap(`site-${route.slice(1).replaceAll("/", "-") || "home"}`);
}
diff --git a/e2e/site.spec.ts b/e2e/site.spec.ts
index c52b34b..6412214 100644
--- a/e2e/site.spec.ts
+++ b/e2e/site.spec.ts
@@ -2,6 +2,7 @@ import { expect, test } from "@playwright/test";
import { linkFromEmail, openSignInLink, signIn, uniqueEmail } from "./helpers";
const FIX = "http://127.0.0.1:4180";
+const CHROME_STORE_URL = "https://chromewebstore.google.com/detail/pluck/plcknnahikagnmlliangabeofipkhecc";
test("a visitor pastes links on the home page, signs up, and finds them in the Inbox", async ({ page }) => {
await page.goto("/");
@@ -61,12 +62,14 @@ test("the site: connect your AI with tabs, open source, privacy and terms", asyn
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Pluck it.");
});
-test("Add to Chrome: a coming-soon page until the store listing is live", async ({ page }) => {
+test("Add to Chrome goes to the store listing, and so does the old coming-soon page", async ({ page, request }) => {
await page.goto("/");
- await page.getByRole("link", { name: "Add to Chrome" }).first().click();
- await expect(page).toHaveURL(/\/chrome$/);
- await expect(page.getByRole("heading", { level: 1 })).toHaveText("Soon.");
- await expect(page.getByRole("link", { name: "Use it on the web" })).toHaveAttribute("href", "/signin");
+ await expect(page.getByRole("link", { name: "Add to Chrome" }).first()).toHaveAttribute("href", CHROME_STORE_URL);
+ for (const old of ["/chrome", "/de/chrome"]) {
+ const res = await request.get(old, { maxRedirects: 0 });
+ expect(res.status(), old).toBe(308);
+ expect(res.headers().location, old).toBe(CHROME_STORE_URL);
+ }
});
test("home: drawn pieces below the fold play once in view; reduced motion leaves them drawn", async ({ page, browser }) => {
diff --git a/packages/i18n/messages/site/de.json b/packages/i18n/messages/site/de.json
index dbd6e79..ca20965 100644
--- a/packages/i18n/messages/site/de.json
+++ b/packages/i18n/messages/site/de.json
@@ -152,19 +152,6 @@
"web": "Im Web nutzen"
}
},
- "soon": {
- "title": "Bald.",
- "web": "Im Web nutzen",
- "connect": "Deine KI verbinden",
- "chrome": {
- "meta": {
- "title": "Pluck für Chrome · Pluck",
- "description": "Pluck für Chrome wartet auf die Prüfung im Chrome Web Store. Es kann jeden Tag so weit sein."
- },
- "para1": "Pluck für Chrome wartet auf die Prüfung im Chrome Web Store, irgendwo hinter ein paar Tausend Gutschein-Findern. Es kann jeden Tag so weit sein.",
- "para2": "Bis dahin funktioniert Pluck im Web: Füg einen Link ein, und er ist gepflückt. Auch deine KI kann für dich pflücken."
- }
- },
"openSource": {
"meta": {
"title": "Open Source · Pluck",
diff --git a/packages/i18n/messages/site/en.json b/packages/i18n/messages/site/en.json
index 30d2f9f..0c69c96 100644
--- a/packages/i18n/messages/site/en.json
+++ b/packages/i18n/messages/site/en.json
@@ -152,19 +152,6 @@
"web": "Use it on the web"
}
},
- "soon": {
- "title": "Soon.",
- "web": "Use it on the web",
- "connect": "Connect your AI",
- "chrome": {
- "meta": {
- "title": "Pluck for Chrome · Pluck",
- "description": "Pluck for Chrome is in the Chrome Web Store's review queue. Out any day now."
- },
- "para1": "Pluck for Chrome is in the Chrome Web Store's review queue, somewhere behind a few thousand coupon finders. Out any day now.",
- "para2": "Until then, Pluck works on the web: paste a link and it is plucked. Your AI can pluck for you too."
- }
- },
"openSource": {
"meta": {
"title": "Open source · Pluck",
diff --git a/packages/i18n/messages/site/es.json b/packages/i18n/messages/site/es.json
index f982496..21d17a8 100644
--- a/packages/i18n/messages/site/es.json
+++ b/packages/i18n/messages/site/es.json
@@ -152,19 +152,6 @@
"web": "Úsalo en la web"
}
},
- "soon": {
- "title": "Pronto.",
- "web": "Úsalo en la web",
- "connect": "Conecta tu IA",
- "chrome": {
- "meta": {
- "title": "Pluck para Chrome · Pluck",
- "description": "Pluck para Chrome está en la cola de revisión de Chrome Web Store. Saldrá cualquier día de estos."
- },
- "para1": "Pluck para Chrome está en la cola de revisión de Chrome Web Store, en algún lugar detrás de unos cuantos miles de buscadores de cupones. Saldrá cualquier día de estos.",
- "para2": "Mientras tanto, Pluck funciona en la web: pega un enlace y queda recogido. Tu IA también puede recoger por ti."
- }
- },
"openSource": {
"meta": {
"title": "Código abierto · Pluck",
diff --git a/packages/i18n/messages/site/fr.json b/packages/i18n/messages/site/fr.json
index 234abd2..868a0f4 100644
--- a/packages/i18n/messages/site/fr.json
+++ b/packages/i18n/messages/site/fr.json
@@ -152,19 +152,6 @@
"web": "L’utiliser sur le web"
}
},
- "soon": {
- "title": "Bientôt.",
- "web": "L’utiliser sur le web",
- "connect": "Connecter votre IA",
- "chrome": {
- "meta": {
- "title": "Pluck pour Chrome · Pluck",
- "description": "Pluck pour Chrome est dans la file d’examen du Chrome Web Store. Il sort d’un jour à l’autre."
- },
- "para1": "Pluck pour Chrome est dans la file d’examen du Chrome Web Store, quelque part derrière quelques milliers de chasseurs de codes promo. Il sort d’un jour à l’autre.",
- "para2": "En attendant, Pluck marche sur le web : collez un lien et il est cueilli. Votre IA peut aussi cueillir pour vous."
- }
- },
"openSource": {
"meta": {
"title": "Open source · Pluck",
diff --git a/packages/i18n/messages/site/it.json b/packages/i18n/messages/site/it.json
index 83bebd2..6320144 100644
--- a/packages/i18n/messages/site/it.json
+++ b/packages/i18n/messages/site/it.json
@@ -152,19 +152,6 @@
"web": "Usalo sul web"
}
},
- "soon": {
- "title": "Presto.",
- "web": "Usalo sul web",
- "connect": "Collega la tua IA",
- "chrome": {
- "meta": {
- "title": "Pluck per Chrome · Pluck",
- "description": "Pluck per Chrome è in coda per la revisione del Chrome Web Store. Esce a giorni."
- },
- "para1": "Pluck per Chrome è in coda per la revisione del Chrome Web Store, da qualche parte dietro qualche migliaio di estensioni per i coupon. Esce a giorni.",
- "para2": "Nel frattempo, Pluck funziona sul web: incolla un link ed è preso. Anche la tua IA può prenderli per te."
- }
- },
"openSource": {
"meta": {
"title": "Open source · Pluck",
diff --git a/packages/i18n/messages/site/ja.json b/packages/i18n/messages/site/ja.json
index c528d74..ab09ac0 100644
--- a/packages/i18n/messages/site/ja.json
+++ b/packages/i18n/messages/site/ja.json
@@ -152,19 +152,6 @@
"web": "Webで使う"
}
},
- "soon": {
- "title": "もうすぐ。",
- "web": "Webで使う",
- "connect": "AIと接続",
- "chrome": {
- "meta": {
- "title": "Chrome版Pluck · Pluck",
- "description": "Chrome版PluckはChromeウェブストアの審査待ちです。もうすぐ公開です。"
- },
- "para1": "Chrome版Pluckは、Chromeウェブストアの審査待ちです。数千のクーポン拡張機能の後ろに並んでいます。もうすぐ公開です。",
- "para2": "それまでは、Web版のPluckをお使いください。リンクを貼り付ければPluckできます。お使いのAIにPluckしてもらうこともできます。"
- }
- },
"openSource": {
"meta": {
"title": "オープンソース · Pluck",
diff --git a/packages/i18n/messages/site/ko.json b/packages/i18n/messages/site/ko.json
index 3acbc5a..4e2e45b 100644
--- a/packages/i18n/messages/site/ko.json
+++ b/packages/i18n/messages/site/ko.json
@@ -152,19 +152,6 @@
"web": "웹에서 쓰기"
}
},
- "soon": {
- "title": "곧 나와요.",
- "web": "웹에서 쓰기",
- "connect": "내 AI 연결",
- "chrome": {
- "meta": {
- "title": "Chrome용 Pluck · Pluck",
- "description": "Chrome용 Pluck은 Chrome 웹 스토어 심사를 기다리고 있어요. 곧 나와요."
- },
- "para1": "Chrome용 Pluck은 Chrome 웹 스토어 심사 대기열에 있어요. 쿠폰 찾기 확장 프로그램 몇천 개 뒤 어딘가예요. 곧 나와요.",
- "para2": "그때까지는 웹에서 Pluck을 쓸 수 있어요. 링크를 붙여 넣으면 담겨요. 내 AI가 대신 담아 줄 수도 있어요."
- }
- },
"openSource": {
"meta": {
"title": "오픈소스 · Pluck",
diff --git a/packages/i18n/messages/site/nl.json b/packages/i18n/messages/site/nl.json
index 510e183..c98be7d 100644
--- a/packages/i18n/messages/site/nl.json
+++ b/packages/i18n/messages/site/nl.json
@@ -152,19 +152,6 @@
"web": "Gebruik het op het web"
}
},
- "soon": {
- "title": "Binnenkort.",
- "web": "Gebruik het op het web",
- "connect": "Verbind je AI",
- "chrome": {
- "meta": {
- "title": "Pluck voor Chrome · Pluck",
- "description": "Pluck voor Chrome wacht op beoordeling door de Chrome Web Store. Komt elk moment uit."
- },
- "para1": "Pluck voor Chrome wacht op beoordeling door de Chrome Web Store, ergens achter een paar duizend kortingscodezoekers. Komt elk moment uit.",
- "para2": "Tot die tijd werkt Pluck op het web: plak een link en hij is geplukt. Je AI kan ook voor je plukken."
- }
- },
"openSource": {
"meta": {
"title": "Open source · Pluck",
diff --git a/packages/i18n/messages/site/pt-BR.json b/packages/i18n/messages/site/pt-BR.json
index c91252e..8179cb3 100644
--- a/packages/i18n/messages/site/pt-BR.json
+++ b/packages/i18n/messages/site/pt-BR.json
@@ -152,19 +152,6 @@
"web": "Usar na web"
}
},
- "soon": {
- "title": "Em breve.",
- "web": "Usar na web",
- "connect": "Conecte sua IA",
- "chrome": {
- "meta": {
- "title": "Pluck para Chrome · Pluck",
- "description": "O Pluck para Chrome está na fila de revisão da Chrome Web Store. Sai a qualquer momento."
- },
- "para1": "O Pluck para Chrome está na fila de revisão da Chrome Web Store, em algum lugar atrás de alguns milhares de caçadores de cupons. Sai a qualquer momento.",
- "para2": "Até lá, o Pluck funciona na web: cole um link e ele é colhido. A sua IA também pode colher por você."
- }
- },
"openSource": {
"meta": {
"title": "Código aberto · Pluck",
diff --git a/packages/i18n/messages/site/zh-Hans.json b/packages/i18n/messages/site/zh-Hans.json
index ce79a32..1bad3a4 100644
--- a/packages/i18n/messages/site/zh-Hans.json
+++ b/packages/i18n/messages/site/zh-Hans.json
@@ -152,19 +152,6 @@
"web": "在网页上使用"
}
},
- "soon": {
- "title": "快了。",
- "web": "在网页上使用",
- "connect": "连接你的 AI",
- "chrome": {
- "meta": {
- "title": "Pluck for Chrome · Pluck",
- "description": "Pluck for Chrome 正在 Chrome 网上应用店排队审核,随时可能上线。"
- },
- "para1": "Pluck for Chrome 正在 Chrome 网上应用店排队审核,前面大概还有几千个优惠券插件。随时可能上线。",
- "para2": "在那之前,Pluck 可以在网页上用:粘贴一个链接,它就摘下了。你的 AI 也能替你摘。"
- }
- },
"openSource": {
"meta": {
"title": "开源 · Pluck",
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index 369d2dd..1fedce8 100644
--- a/packages/i18n/package.json
+++ b/packages/i18n/package.json
@@ -6,7 +6,7 @@
"private": true,
"license": "MIT",
"type": "module",
- "exports": { ".": "./src/index.ts", "./messages/*": "./messages/*" },
+ "exports": { ".": "./src/index.ts", "./extension": "./src/extension.ts", "./messages/*": "./messages/*" },
"scripts": { "typecheck": "tsc -p ." },
"devDependencies": { "intl-messageformat": "12.1.4" }
}
diff --git a/packages/i18n/src/catalog.ts b/packages/i18n/src/catalog.ts
new file mode 100644
index 0000000..3960529
--- /dev/null
+++ b/packages/i18n/src/catalog.ts
@@ -0,0 +1,15 @@
+import { DEFAULT_LOCALE, type Locale } from "./locales.ts";
+
+export type Messages = { [key: string]: string | Messages };
+
+function merge(base: Messages, over: Messages): Messages {
+ const out: Messages = { ...base };
+ for (const [k, v] of Object.entries(over)) {
+ const b = out[k];
+ out[k] = typeof v === "object" && typeof b === "object" ? merge(b, v) : v;
+ }
+ return out;
+}
+
+// A catalog in a language, with English filling any key not translated yet.
+export const inLocale = (all: Record, locale: Locale): Messages => (locale === DEFAULT_LOCALE ? all.en : merge(all.en, all[locale]));
diff --git a/packages/i18n/src/extension.ts b/packages/i18n/src/extension.ts
new file mode 100644
index 0000000..e86b5b6
--- /dev/null
+++ b/packages/i18n/src/extension.ts
@@ -0,0 +1,7 @@
+import extension from "../messages/extension/index.ts";
+import { inLocale, type Messages } from "./catalog.ts";
+import type { Locale } from "./locales.ts";
+
+// The extension's entry: its own catalog only, so the other surfaces' ten languages stay out of its bundle.
+export * from "./locales.ts";
+export const extensionMessages = (locale: Locale) => inLocale(extension as Record, locale) as typeof extension.en;
diff --git a/packages/i18n/src/index.ts b/packages/i18n/src/index.ts
index e9a76d1..4c656c0 100644
--- a/packages/i18n/src/index.ts
+++ b/packages/i18n/src/index.ts
@@ -4,30 +4,21 @@ import extension from "../messages/extension/index.ts";
import ios from "../messages/ios/index.ts";
import share from "../messages/share/index.ts";
import site from "../messages/site/index.ts";
-import { DEFAULT_LOCALE, type Locale } from "./locales.ts";
+import { inLocale, type Messages } from "./catalog.ts";
+import type { Locale } from "./locales.ts";
export * from "./locales.ts";
+export type { Messages };
// Where each catalog is used: site (marketing pages), app (the signed-in web app and sign-in), email (every
// email the server sends), ios (the iPhone app), extension (the Chrome extension), share (the iPhone share sheet).
export const CATALOGS = { site, app, email, ios, extension, share } as const;
export type Surface = keyof typeof CATALOGS;
-export type Messages = { [key: string]: string | Messages };
-
-function merge(base: Messages, over: Messages): Messages {
- const out: Messages = { ...base };
- for (const [k, v] of Object.entries(over)) {
- const b = out[k];
- out[k] = typeof v === "object" && typeof b === "object" ? merge(b, v) : v;
- }
- return out;
-}
// The shape every language shares: English's, so a key that does not exist fails the typecheck.
export type CatalogOf = (typeof CATALOGS)[S]["en"];
// A surface's messages in a language, with English filling any key not translated yet.
export function messagesFor(surface: S, locale: Locale): CatalogOf {
- const all = CATALOGS[surface] as Record;
- return (locale === DEFAULT_LOCALE ? all.en : merge(all.en, all[locale])) as CatalogOf;
+ return inLocale(CATALOGS[surface] as Record, locale) as CatalogOf;
}