rosetta-i18n 1.0.0-beta.1 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,6 +25,7 @@ npx rosetta check # CI gate: fails if anything is stale or broken (no API ke
25
25
  ## Contents
26
26
 
27
27
  - [Quick start](#quick-start)
28
+ - [Set up with your AI agent](#set-up-with-your-ai-agent)
28
29
  - [How it works](#how-it-works)
29
30
  - [Configuration](#configuration)
30
31
  - [Commands](#commands)
@@ -33,6 +34,7 @@ npx rosetta check # CI gate: fails if anything is stale or broken (no API ke
33
34
  - [Coming from Lingo.dev](#coming-from-lingodev)
34
35
  - [Library API](#library-api)
35
36
  - [React & Next.js](#react--nextjs)
37
+ - [Locale routing & cookies](#locale-routing--cookies)
36
38
  - [Development](#development)
37
39
 
38
40
  ## Quick start
@@ -68,6 +70,26 @@ Already have translations? The first `push` **adopts** them — it records them
68
70
  the lockfile without retranslating, so it's free. Only strings that are missing,
69
71
  changed, or broken go to the model.
70
72
 
73
+ ## Set up with your AI agent
74
+
75
+ Copy this into your coding agent (Claude Code, Cursor, Codex, opencode, …) from your
76
+ project root:
77
+
78
+ ```text
79
+ Set up Rosetta (npm: rosetta-i18n) for translations in this repo. Follow https://raw.githubusercontent.com/ian/rosetta/main/skills/rosetta/SKILL.md step by step: discover our locale files and current translation setup and report back, configure .rosetta/config.json, adopt our existing translations with `rosetta status` + `rosetta push` (list any translations it flags as broken), add tests and CI, remove the old translation tooling, document the workflow in AGENTS.md, and open one PR. Ask me before running `push --force`, before any large translation run, and before choosing how translations land if it's unclear.
80
+ ```
81
+
82
+ - For agents that can't fetch URLs, use the full, self-contained prompt in
83
+ [`docs/agent-prompt.md`](docs/agent-prompt.md).
84
+ - To give your agent Rosetta know-how for everyday requests ("add a German string",
85
+ "why is the i18n job red?"), install the skill:
86
+
87
+ ```bash
88
+ mkdir -p .claude/skills/rosetta && curl -fsSL https://raw.githubusercontent.com/ian/rosetta/main/skills/rosetta/SKILL.md -o .claude/skills/rosetta/SKILL.md
89
+ ```
90
+
91
+ Other agents: [`docs/agent-prompt.md#install-the-skill`](docs/agent-prompt.md#install-the-skill).
92
+
71
93
  ## How it works
72
94
 
73
95
  Rosetta keeps a lockfile, `.rosetta/lock.json`, that records — for every file,
@@ -534,6 +556,119 @@ the route handler or Server Action above.
534
556
  > Node-only workflow (`push`, `check`, …, which read and write files), so prefer the
535
557
  > `next` entrypoint in edge code.
536
558
 
559
+ ## Locale routing & cookies
560
+
561
+ Translating strings is half the job. Knowing *which* locale a request is in — and
562
+ switching cleanly — is the other half, and it's where most apps hand-roll fragile
563
+ middleware, server actions, and `router.refresh()` races. Rosetta ships the runtime
564
+ layer too: `rosetta-i18n/locale` (core), `/react`, `/next`, `/astro`.
565
+
566
+ The model is one-directional and needs no client-side guessing:
567
+
568
+ 1. a **locale-prefixed URL** (`/ja/pricing`) is authoritative,
569
+ 2. then a **forced header** (`x-rosetta-locale`, set by middleware),
570
+ 3. then the **cookie**,
571
+ 4. then the **default locale**.
572
+
573
+ **Switching writes the cookie and navigates to the localized URL**, so the URL and the
574
+ cookie always agree. A full-page navigation makes it a single request with no
575
+ refresh-after-push race.
576
+
577
+ ### Config (plain data)
578
+
579
+ ```ts
580
+ // i18n/config.ts
581
+ import type { LocaleConfig } from "rosetta-i18n/locale";
582
+
583
+ export const localeConfig: LocaleConfig = {
584
+ locales: ["en", "ja", "de", "es"],
585
+ defaultLocale: "en",
586
+ // Only these paths have localized variants; everything else stays bare.
587
+ localizedPaths: ["/", "/pricing", "/agents"],
588
+ localizedPathPrefixes: ["/blog/", "/help/", "/vs/"],
589
+ };
590
+ ```
591
+
592
+ The core also exports the pure helpers (`resolveLocale`, `stripLocale`, `localizePath`,
593
+ `planSwitch`, `serializeLocaleCookie`, …) if you're wiring your own framework.
594
+
595
+ ### React
596
+
597
+ ```tsx
598
+ "use client";
599
+ import { LocaleProvider, LocaleSwitcher } from "rosetta-i18n/react";
600
+
601
+ export function App({ locale, children }: { locale: string; children: React.ReactNode }) {
602
+ return (
603
+ <LocaleProvider config={localeConfig} locale={locale}>
604
+ {/* or render={({ locales, locale, switchTo, labelFor }) => …} */}
605
+ <LocaleSwitcher />
606
+ {children}
607
+ </LocaleProvider>
608
+ );
609
+ }
610
+ ```
611
+
612
+ Hooks: `useLocale()`, `useLocales()`, `useLocaleSwitch()`, `useLocaleContext()`.
613
+ `switchTo(locale)` writes the cookie and navigates (full page by default; pass
614
+ `navigate` to hook a router, `persist` to save server-side).
615
+
616
+ ### Next.js (App Router)
617
+
618
+ ```ts
619
+ // middleware.ts
620
+ import { createLocaleMiddleware } from "rosetta-i18n/next";
621
+ import { localeConfig } from "./i18n/config";
622
+
623
+ export const middleware = createLocaleMiddleware(localeConfig);
624
+ export const config = { matcher: ["/((?!_next|favicon.ico|.*\\..*).*)"] };
625
+ ```
626
+
627
+ ```tsx
628
+ // app/layout.tsx
629
+ import { RosettaProvider } from "rosetta-i18n/next";
630
+ import { localeConfig } from "@/i18n/config";
631
+
632
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
633
+ return (
634
+ <html>
635
+ <body>
636
+ <RosettaProvider config={localeConfig}>{children}</RosettaProvider>
637
+ </body>
638
+ </html>
639
+ );
640
+ }
641
+ ```
642
+
643
+ `RosettaProvider` reads the locale the middleware forced (`getServerLocale(config)`)
644
+ and provides it to the client. Persist the choice for signed-in users with a server
645
+ action:
646
+
647
+ ```ts
648
+ // app/locale-actions.ts
649
+ "use server";
650
+ import { createSetLocaleAction } from "rosetta-i18n/next";
651
+ import { localeConfig } from "@/i18n/config";
652
+
653
+ export const setLocale = createSetLocaleAction(localeConfig, {
654
+ persist: (locale) => updateUserProfile(userId, { locale }),
655
+ });
656
+ ```
657
+
658
+ ### Astro
659
+
660
+ ```ts
661
+ // src/middleware.ts
662
+ import { createAstroLocaleMiddleware } from "rosetta-i18n/astro";
663
+ import { localeConfig } from "../i18n/config";
664
+
665
+ export const onRequest = createAstroLocaleMiddleware(localeConfig);
666
+ ```
667
+
668
+ The middleware sets `Astro.locals.locale` (read it with `getAstroLocale(Astro, config)`)
669
+ and rewrites prefixed URLs. For a switcher, render the React `<LocaleSwitcher>` as an
670
+ island wrapped in `<LocaleProvider locale={Astro.locals.locale} config={localeConfig}>`.
671
+
537
672
  ## Legacy `translate-catalog`
538
673
 
539
674
  The 0.x one-shot command still works but is deprecated and will be removed in v2:
@@ -593,6 +728,9 @@ Source lives in `src/`: the translation engine in `src/rosetta.ts`, validation i
593
728
  in `src/cli.ts`. `rollup` produces `dist/esm`; `tsc` emits declarations to
594
729
  `dist/types`.
595
730
 
731
+ The website lives in `site/` (Astro, deployed to Cloudflare Pages); see
732
+ [`site/README.md`](site/README.md).
733
+
596
734
  ## License
597
735
 
598
736
  MIT © Ian Hunter
@@ -0,0 +1,103 @@
1
+ const LOCALE_HEADER = "x-rosetta-locale";
2
+ const DEFAULT_LOCALE_COOKIE = "locale";
3
+ const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
4
+ function localeCookieName(config) {
5
+ return config.cookieName ?? DEFAULT_LOCALE_COOKIE;
6
+ }
7
+ function localeCookieOptions(config) {
8
+ return {
9
+ path: "/",
10
+ maxAge: config.cookieMaxAgeSeconds ?? ONE_YEAR_SECONDS,
11
+ sameSite: "lax"
12
+ };
13
+ }
14
+ function isLocale(config, value) {
15
+ return typeof value === "string" && config.locales.includes(value);
16
+ }
17
+ function localeFromPath(config, pathname) {
18
+ const segment = pathname.split("/")[1];
19
+ return segment && isLocale(config, segment) ? segment : null;
20
+ }
21
+ function stripLocale(config, pathname) {
22
+ const locale = localeFromPath(config, pathname);
23
+ if (!locale) return { locale: null, path: pathname || "/" };
24
+ const rest = pathname.slice(locale.length + 1);
25
+ return { locale, path: rest || "/" };
26
+ }
27
+ function resolveLocale(config, input) {
28
+ const fromPath = localeFromPath(config, input.pathname);
29
+ if (fromPath) {
30
+ return {
31
+ locale: fromPath,
32
+ path: stripLocale(config, input.pathname).path,
33
+ source: "url"
34
+ };
35
+ }
36
+ if (isLocale(config, input.header)) {
37
+ return { locale: input.header, path: input.pathname, source: "header" };
38
+ }
39
+ if (isLocale(config, input.cookie)) {
40
+ return { locale: input.cookie, path: input.pathname, source: "cookie" };
41
+ }
42
+ return {
43
+ locale: config.defaultLocale,
44
+ path: input.pathname,
45
+ source: "default"
46
+ };
47
+ }
48
+ function serializeLocaleCookie(config, locale) {
49
+ const options = localeCookieOptions(config);
50
+ return `${localeCookieName(config)}=${encodeURIComponent(locale)}; Path=${options.path}; Max-Age=${options.maxAge}; SameSite=${capitalize(options.sameSite)}`;
51
+ }
52
+ function parseCookie(header, name) {
53
+ if (!header) return null;
54
+ for (const part of header.split(";")) {
55
+ const [key, ...rest] = part.trim().split("=");
56
+ if (key === name) return decodeURIComponent(rest.join("="));
57
+ }
58
+ return null;
59
+ }
60
+ function capitalize(value) {
61
+ return value.charAt(0).toUpperCase() + value.slice(1);
62
+ }
63
+
64
+ function createAstroLocaleMiddleware(config) {
65
+ return async function onRequest(context, next) {
66
+ const url = new URL(context.request.url);
67
+ const resolved = resolveLocale(config, {
68
+ pathname: url.pathname,
69
+ header: context.request.headers.get(LOCALE_HEADER),
70
+ cookie: parseCookie(
71
+ context.request.headers.get("cookie"),
72
+ localeCookieName(config)
73
+ )
74
+ });
75
+ context.locals.locale = resolved.locale;
76
+ if (resolved.source === "url") {
77
+ const target = new URL(url);
78
+ target.pathname = resolved.path;
79
+ const response2 = await context.rewrite(target);
80
+ response2.headers.set(LOCALE_HEADER, resolved.locale);
81
+ response2.headers.append(
82
+ "Set-Cookie",
83
+ serializeLocaleCookie(config, resolved.locale)
84
+ );
85
+ return response2;
86
+ }
87
+ const response = await next();
88
+ if (resolved.source === "cookie" || resolved.source === "default") {
89
+ response.headers.append(
90
+ "Set-Cookie",
91
+ serializeLocaleCookie(config, resolved.locale)
92
+ );
93
+ }
94
+ return response;
95
+ };
96
+ }
97
+ function getAstroLocale(context, config) {
98
+ const locale = context.locals.locale;
99
+ return isLocale(config, locale) ? locale : config.defaultLocale;
100
+ }
101
+
102
+ export { createAstroLocaleMiddleware, getAstroLocale };
103
+ //# sourceMappingURL=astro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"astro.js","sources":["../../src/locale/core.ts","../../src/locale/astro.ts"],"sourcesContent":["/**\n * Runtime locale layer — framework-agnostic core.\n *\n * The translation engine answers \"how do I translate these strings?\". This\n * module answers \"which locale is this request in, and how do I switch?\" — the\n * routing + cookie model that the React, Next, and Astro adapters are thin\n * wrappers around.\n *\n * The config is deliberately plain data (no functions) so the exact same object\n * can cross the server → client boundary. Resolution is deterministic and\n * one-directional:\n *\n * 1. a locale-prefixed URL (`/ja/pricing`) is authoritative,\n * 2. then a forced header (`x-rosetta-locale`, set by middleware),\n * 3. then the persisted cookie,\n * 4. then the default locale.\n *\n * Switching sets the cookie *and* navigates to the localized URL, so the URL and\n * the cookie always agree. There is no client-side guessing, no\n * refresh-after-push race, and no way for a stale request to re-pin the locale.\n */\n\nexport const LOCALE_HEADER = \"x-rosetta-locale\";\nexport const DEFAULT_LOCALE_COOKIE = \"locale\";\nexport const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;\n\nexport interface LocaleConfig {\n\t/** Every locale, including the default. Order is the switcher order. */\n\tlocales: readonly string[];\n\t/** The locale served without a URL prefix. */\n\tdefaultLocale: string;\n\t/** Cookie that persists the choice. Default `\"locale\"`. */\n\tcookieName?: string;\n\t/** Cookie max-age in seconds. Default one year. */\n\tcookieMaxAgeSeconds?: number;\n\t/** Prefix the default locale too (`/en/...`). Default `false`. */\n\tprefixDefaultLocale?: boolean;\n\t/**\n\t * Canonical (unprefixed) paths that have localized variants — exact matches.\n\t * Omit both this and `localizedPathPrefixes` to localize every path.\n\t */\n\tlocalizedPaths?: readonly string[];\n\t/** Canonical paths that have localized variants — prefix matches (e.g. `/blog/`). */\n\tlocalizedPathPrefixes?: readonly string[];\n}\n\nexport type LocaleSource = \"url\" | \"header\" | \"cookie\" | \"default\";\n\nexport interface ResolvedLocale {\n\tlocale: string;\n\t/** The canonical, unprefixed path for this request. */\n\tpath: string;\n\tsource: LocaleSource;\n}\n\nexport interface LocaleCookieOptions {\n\tpath: string;\n\tmaxAge: number;\n\tsameSite: \"lax\";\n}\n\nexport interface LocaleSwitchPlan {\n\t/** Localized URL to navigate to. */\n\thref: string;\n\t/** Cookie to persist the choice. */\n\tcookie: { name: string; value: string; options: LocaleCookieOptions };\n}\n\nexport function localeCookieName(config: LocaleConfig): string {\n\treturn config.cookieName ?? DEFAULT_LOCALE_COOKIE;\n}\n\nexport function localeCookieOptions(config: LocaleConfig): LocaleCookieOptions {\n\treturn {\n\t\tpath: \"/\",\n\t\tmaxAge: config.cookieMaxAgeSeconds ?? ONE_YEAR_SECONDS,\n\t\tsameSite: \"lax\",\n\t};\n}\n\nexport function isLocale(\n\tconfig: LocaleConfig,\n\tvalue: unknown,\n): value is string {\n\treturn typeof value === \"string\" && config.locales.includes(value);\n}\n\nexport function isLocalizedPath(\n\tconfig: LocaleConfig,\n\tpathname: string,\n): boolean {\n\tconst hasRules =\n\t\t(config.localizedPaths?.length ?? 0) > 0 ||\n\t\t(config.localizedPathPrefixes?.length ?? 0) > 0;\n\tif (!hasRules) return true;\n\tif (config.localizedPaths?.includes(pathname)) return true;\n\treturn (\n\t\tconfig.localizedPathPrefixes?.some(\n\t\t\t(prefix) => pathname === prefix || pathname.startsWith(prefix),\n\t\t) ?? false\n\t);\n}\n\n/** The leading locale segment of a path, if it is one we know. */\nexport function localeFromPath(\n\tconfig: LocaleConfig,\n\tpathname: string,\n): string | null {\n\tconst segment = pathname.split(\"/\")[1];\n\treturn segment && isLocale(config, segment) ? segment : null;\n}\n\n/** Strip a leading locale segment, returning the canonical path. */\nexport function stripLocale(\n\tconfig: LocaleConfig,\n\tpathname: string,\n): { locale: string | null; path: string } {\n\tconst locale = localeFromPath(config, pathname);\n\tif (!locale) return { locale: null, path: pathname || \"/\" };\n\tconst rest = pathname.slice(locale.length + 1);\n\treturn { locale, path: rest || \"/\" };\n}\n\n/**\n * Prefix a canonical path for a locale. The default locale stays bare unless\n * `prefixDefaultLocale` is set.\n */\nexport function localizePath(\n\tconfig: LocaleConfig,\n\tpathname: string,\n\tlocale: string,\n): string {\n\tif (locale === config.defaultLocale && !config.prefixDefaultLocale) {\n\t\treturn pathname;\n\t}\n\treturn pathname === \"/\" ? `/${locale}` : `/${locale}${pathname}`;\n}\n\n/** The URL to navigate to when switching `pathname` to `locale`. */\nexport function switchHref(\n\tconfig: LocaleConfig,\n\tpathname: string,\n\tlocale: string,\n): string {\n\tconst { path } = stripLocale(config, pathname);\n\treturn localizePath(config, path, locale);\n}\n\n/**\n * Resolve the active locale for a request. Precedence: URL prefix → forced\n * header → cookie → default. `path` is always the canonical (unprefixed) path.\n */\nexport function resolveLocale(\n\tconfig: LocaleConfig,\n\tinput: { pathname: string; header?: string | null; cookie?: string | null },\n): ResolvedLocale {\n\tconst fromPath = localeFromPath(config, input.pathname);\n\tif (fromPath) {\n\t\treturn {\n\t\t\tlocale: fromPath,\n\t\t\tpath: stripLocale(config, input.pathname).path,\n\t\t\tsource: \"url\",\n\t\t};\n\t}\n\tif (isLocale(config, input.header)) {\n\t\treturn { locale: input.header, path: input.pathname, source: \"header\" };\n\t}\n\tif (isLocale(config, input.cookie)) {\n\t\treturn { locale: input.cookie, path: input.pathname, source: \"cookie\" };\n\t}\n\treturn {\n\t\tlocale: config.defaultLocale,\n\t\tpath: input.pathname,\n\t\tsource: \"default\",\n\t};\n}\n\n/** The `Set-Cookie` value that persists `locale`. */\nexport function serializeLocaleCookie(\n\tconfig: LocaleConfig,\n\tlocale: string,\n): string {\n\tconst options = localeCookieOptions(config);\n\treturn `${localeCookieName(config)}=${encodeURIComponent(locale)}; Path=${\n\t\toptions.path\n\t}; Max-Age=${options.maxAge}; SameSite=${capitalize(options.sameSite)}`;\n}\n\n/** Read a single cookie value from a `Cookie:` header. */\nexport function parseCookie(\n\theader: string | null | undefined,\n\tname: string,\n): string | null {\n\tif (!header) return null;\n\tfor (const part of header.split(\";\")) {\n\t\tconst [key, ...rest] = part.trim().split(\"=\");\n\t\tif (key === name) return decodeURIComponent(rest.join(\"=\"));\n\t}\n\treturn null;\n}\n\n/**\n * Everything a client needs to switch locale: set `cookie`, navigate to `href`.\n * Returns `null` when the current path has no localized variant (legal pages,\n * API routes, …), so the switcher can no-op instead of linking to a 404.\n */\nexport function planSwitch(\n\tconfig: LocaleConfig,\n\tpathname: string,\n\tlocale: string,\n): LocaleSwitchPlan | null {\n\tconst { path } = stripLocale(config, pathname);\n\tif (!isLocalizedPath(config, path)) return null;\n\treturn {\n\t\thref: localizePath(config, path, locale),\n\t\tcookie: {\n\t\t\tname: localeCookieName(config),\n\t\t\tvalue: locale,\n\t\t\toptions: localeCookieOptions(config),\n\t\t},\n\t};\n}\n\nfunction capitalize(value: string): string {\n\treturn value.charAt(0).toUpperCase() + value.slice(1);\n}\n","/**\n * Astro adapter.\n *\n * Astro's middleware runs on every request, so it's the natural place to\n * resolve the locale, force it onto `Astro.locals.locale`, rewrite\n * locale-prefixed URLs to their canonical path, and persist the cookie.\n *\n * ```ts\n * // src/middleware.ts\n * import { defineMiddleware } from \"astro:middleware\";\n * import { createAstroLocaleMiddleware } from \"rosetta-i18n/astro\";\n * import { localeConfig } from \"../i18n/config\";\n *\n * export const onRequest = createAstroLocaleMiddleware(localeConfig);\n * ```\n *\n * For a client switch, render the React `<LocaleSwitcher>` as an island and\n * wrap it in `<LocaleProvider locale={Astro.locals.locale} config={localeConfig}>`.\n * Types are structural, so this module never depends on `astro`.\n */\n\nimport {\n\tisLocale,\n\tLOCALE_HEADER,\n\ttype LocaleConfig,\n\tlocaleCookieName,\n\tparseCookie,\n\tresolveLocale,\n\tserializeLocaleCookie,\n} from \"./core\";\n\nexport interface AstroLocaleContext {\n\trequest: Request;\n\tlocals: Record<string, unknown>;\n\t/** Astro's `context.rewrite()`. */\n\trewrite: (url: string | URL) => Promise<Response>;\n}\n\nexport type AstroMiddlewareNext = (rewrite?: URL) => Promise<Response>;\n\nexport function createAstroLocaleMiddleware(config: LocaleConfig) {\n\treturn async function onRequest(\n\t\tcontext: AstroLocaleContext,\n\t\tnext: AstroMiddlewareNext,\n\t): Promise<Response> {\n\t\tconst url = new URL(context.request.url);\n\t\tconst resolved = resolveLocale(config, {\n\t\t\tpathname: url.pathname,\n\t\t\theader: context.request.headers.get(LOCALE_HEADER),\n\t\t\tcookie: parseCookie(\n\t\t\t\tcontext.request.headers.get(\"cookie\"),\n\t\t\t\tlocaleCookieName(config),\n\t\t\t),\n\t\t});\n\n\t\tcontext.locals.locale = resolved.locale;\n\n\t\tif (resolved.source === \"url\") {\n\t\t\tconst target = new URL(url);\n\t\t\ttarget.pathname = resolved.path;\n\t\t\tconst response = await context.rewrite(target);\n\t\t\tresponse.headers.set(LOCALE_HEADER, resolved.locale);\n\t\t\tresponse.headers.append(\n\t\t\t\t\"Set-Cookie\",\n\t\t\t\tserializeLocaleCookie(config, resolved.locale),\n\t\t\t);\n\t\t\treturn response;\n\t\t}\n\n\t\tconst response = await next();\n\t\t// Persist only when the URL can't decide it (bare path) and the cookie\n\t\t// disagrees — never on a rewrite, where the URL is authoritative.\n\t\tif (resolved.source === \"cookie\" || resolved.source === \"default\") {\n\t\t\tresponse.headers.append(\n\t\t\t\t\"Set-Cookie\",\n\t\t\t\tserializeLocaleCookie(config, resolved.locale),\n\t\t\t);\n\t\t}\n\t\treturn response;\n\t};\n}\n\n/** Read the locale the middleware stored on `Astro.locals`. */\nexport function getAstroLocale(\n\tcontext: { locals: Record<string, unknown> },\n\tconfig: LocaleConfig,\n): string {\n\tconst locale = context.locals.locale;\n\treturn isLocale(config, locale) ? locale : config.defaultLocale;\n}\n"],"names":["response"],"mappings":"AAsBO,MAAM,aAAA,GAAgB,kBAAA;AACtB,MAAM,qBAAA,GAAwB,QAAA;AAC9B,MAAM,gBAAA,GAAmB,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,GAAA;AA4CxC,SAAS,iBAAiB,MAAA,EAA8B;AAC9D,EAAA,OAAO,OAAO,UAAA,IAAc,qBAAA;AAC7B;AAEO,SAAS,oBAAoB,MAAA,EAA2C;AAC9E,EAAA,OAAO;AAAA,IACN,IAAA,EAAM,GAAA;AAAA,IACN,MAAA,EAAQ,OAAO,mBAAA,IAAuB,gBAAA;AAAA,IACtC,QAAA,EAAU;AAAA,GACX;AACD;AAEO,SAAS,QAAA,CACf,QACA,KAAA,EACkB;AAClB,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,MAAA,CAAO,OAAA,CAAQ,SAAS,KAAK,CAAA;AAClE;AAmBO,SAAS,cAAA,CACf,QACA,QAAA,EACgB;AAChB,EAAA,MAAM,OAAA,GAAU,QAAA,CAAS,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AACrC,EAAA,OAAO,OAAA,IAAW,QAAA,CAAS,MAAA,EAAQ,OAAO,IAAI,OAAA,GAAU,IAAA;AACzD;AAGO,SAAS,WAAA,CACf,QACA,QAAA,EAC0C;AAC1C,EAAA,MAAM,MAAA,GAAS,cAAA,CAAe,MAAA,EAAQ,QAAQ,CAAA;AAC9C,EAAA,IAAI,CAAC,QAAQ,OAAO,EAAE,QAAQ,IAAA,EAAM,IAAA,EAAM,YAAY,GAAA,EAAI;AAC1D,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,SAAS,CAAC,CAAA;AAC7C,EAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,IAAA,IAAQ,GAAA,EAAI;AACpC;AA+BO,SAAS,aAAA,CACf,QACA,KAAA,EACiB;AACjB,EAAA,MAAM,QAAA,GAAW,cAAA,CAAe,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA;AACtD,EAAA,IAAI,QAAA,EAAU;AACb,IAAA,OAAO;AAAA,MACN,MAAA,EAAQ,QAAA;AAAA,MACR,IAAA,EAAM,WAAA,CAAY,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA,CAAE,IAAA;AAAA,MAC1C,MAAA,EAAQ;AAAA,KACT;AAAA,EACD;AACA,EAAA,IAAI,QAAA,CAAS,MAAA,EAAQ,KAAA,CAAM,MAAM,CAAA,EAAG;AACnC,IAAA,OAAO,EAAE,QAAQ,KAAA,CAAM,MAAA,EAAQ,MAAM,KAAA,CAAM,QAAA,EAAU,QAAQ,QAAA,EAAS;AAAA,EACvE;AACA,EAAA,IAAI,QAAA,CAAS,MAAA,EAAQ,KAAA,CAAM,MAAM,CAAA,EAAG;AACnC,IAAA,OAAO,EAAE,QAAQ,KAAA,CAAM,MAAA,EAAQ,MAAM,KAAA,CAAM,QAAA,EAAU,QAAQ,QAAA,EAAS;AAAA,EACvE;AACA,EAAA,OAAO;AAAA,IACN,QAAQ,MAAA,CAAO,aAAA;AAAA,IACf,MAAM,KAAA,CAAM,QAAA;AAAA,IACZ,MAAA,EAAQ;AAAA,GACT;AACD;AAGO,SAAS,qBAAA,CACf,QACA,MAAA,EACS;AACT,EAAA,MAAM,OAAA,GAAU,oBAAoB,MAAM,CAAA;AAC1C,EAAA,OAAO,GAAG,gBAAA,CAAiB,MAAM,CAAC,CAAA,CAAA,EAAI,kBAAA,CAAmB,MAAM,CAAC,CAAA,OAAA,EAC/D,OAAA,CAAQ,IACT,aAAa,OAAA,CAAQ,MAAM,cAAc,UAAA,CAAW,OAAA,CAAQ,QAAQ,CAAC,CAAA,CAAA;AACtE;AAGO,SAAS,WAAA,CACf,QACA,IAAA,EACgB;AAChB,EAAA,IAAI,CAAC,QAAQ,OAAO,IAAA;AACpB,EAAA,KAAA,MAAW,IAAA,IAAQ,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA,EAAG;AACrC,IAAA,MAAM,CAAC,KAAK,GAAG,IAAI,IAAI,IAAA,CAAK,IAAA,EAAK,CAAE,KAAA,CAAM,GAAG,CAAA;AAC5C,IAAA,IAAI,QAAQ,IAAA,EAAM,OAAO,mBAAmB,IAAA,CAAK,IAAA,CAAK,GAAG,CAAC,CAAA;AAAA,EAC3D;AACA,EAAA,OAAO,IAAA;AACR;AAwBA,SAAS,WAAW,KAAA,EAAuB;AAC1C,EAAA,OAAO,KAAA,CAAM,OAAO,CAAC,CAAA,CAAE,aAAY,GAAI,KAAA,CAAM,MAAM,CAAC,CAAA;AACrD;;ACzLO,SAAS,4BAA4B,MAAA,EAAsB;AACjE,EAAA,OAAO,eAAe,SAAA,CACrB,OAAA,EACA,IAAA,EACoB;AACpB,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,QAAQ,GAAG,CAAA;AACvC,IAAA,MAAM,QAAA,GAAW,cAAc,MAAA,EAAQ;AAAA,MACtC,UAAU,GAAA,CAAI,QAAA;AAAA,MACd,MAAA,EAAQ,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,IAAI,aAAa,CAAA;AAAA,MACjD,MAAA,EAAQ,WAAA;AAAA,QACP,OAAA,CAAQ,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,QAAQ,CAAA;AAAA,QACpC,iBAAiB,MAAM;AAAA;AACxB,KACA,CAAA;AAED,IAAA,OAAA,CAAQ,MAAA,CAAO,SAAS,QAAA,CAAS,MAAA;AAEjC,IAAA,IAAI,QAAA,CAAS,WAAW,KAAA,EAAO;AAC9B,MAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,MAAA,MAAA,CAAO,WAAW,QAAA,CAAS,IAAA;AAC3B,MAAA,MAAMA,SAAAA,GAAW,MAAM,OAAA,CAAQ,OAAA,CAAQ,MAAM,CAAA;AAC7C,MAAAA,SAAAA,CAAS,OAAA,CAAQ,GAAA,CAAI,aAAA,EAAe,SAAS,MAAM,CAAA;AACnD,MAAAA,UAAS,OAAA,CAAQ,MAAA;AAAA,QAChB,YAAA;AAAA,QACA,qBAAA,CAAsB,MAAA,EAAQ,QAAA,CAAS,MAAM;AAAA,OAC9C;AACA,MAAA,OAAOA,SAAAA;AAAA,IACR;AAEA,IAAA,MAAM,QAAA,GAAW,MAAM,IAAA,EAAK;AAG5B,IAAA,IAAI,QAAA,CAAS,MAAA,KAAW,QAAA,IAAY,QAAA,CAAS,WAAW,SAAA,EAAW;AAClE,MAAA,QAAA,CAAS,OAAA,CAAQ,MAAA;AAAA,QAChB,YAAA;AAAA,QACA,qBAAA,CAAsB,MAAA,EAAQ,QAAA,CAAS,MAAM;AAAA,OAC9C;AAAA,IACD;AACA,IAAA,OAAO,QAAA;AAAA,EACR,CAAA;AACD;AAGO,SAAS,cAAA,CACf,SACA,MAAA,EACS;AACT,EAAA,MAAM,MAAA,GAAS,QAAQ,MAAA,CAAO,MAAA;AAC9B,EAAA,OAAO,QAAA,CAAS,MAAA,EAAQ,MAAM,CAAA,GAAI,SAAS,MAAA,CAAO,aAAA;AACnD;;"}
package/dist/esm/cli.js CHANGED
@@ -2539,6 +2539,7 @@ async function cmdPush(ctx) {
2539
2539
  translator: io.translator,
2540
2540
  onLocale: (locale) => printPushLocale(ctx, locale)
2541
2541
  });
2542
+ warnNoMatches(ctx, result.locales.length);
2542
2543
  if (ctx.json) {
2543
2544
  ctx.out(JSON.stringify(result, null, 2));
2544
2545
  } else if (estimate && result.estimate) {
@@ -2574,6 +2575,13 @@ async function cmdPush(ctx) {
2574
2575
  }
2575
2576
  return result.ok ? EXIT.ok : EXIT.failed;
2576
2577
  }
2578
+ function warnNoMatches(ctx, jobs) {
2579
+ if (jobs === 0 && ctx.args.positionals.length > 0 && !ctx.json) {
2580
+ ctx.io.stderr(
2581
+ `warning: no configured source files match ${ctx.args.positionals.map((p) => `"${p}"`).join(", ")} (patterns match source paths like those in .rosetta/config.json files[].pattern).`
2582
+ );
2583
+ }
2584
+ }
2577
2585
  function printCheck(ctx, result) {
2578
2586
  for (const locale of result.locales) {
2579
2587
  if (locale.ok) {
@@ -2603,6 +2611,7 @@ function cmdCheck(ctx) {
2603
2611
  patterns: ctx.args.positionals,
2604
2612
  locales: list(ctx.args.flags.locale)
2605
2613
  });
2614
+ warnNoMatches(ctx, result.locales.length);
2606
2615
  if (ctx.json) ctx.out(JSON.stringify(result, null, 2));
2607
2616
  else printCheck(ctx, result);
2608
2617
  return result.ok ? EXIT.ok : EXIT.stale;