@astrolicious/i18n
Version:
Yet another i18n integration for Astro with server and client utilities, type safety and translations built-in.
29 lines (28 loc) • 16.1 kB
JavaScript
import{readFileSync as v}from"node:fs";import{addIntegration as ae,addVirtualImports as ie,createResolver as se,defineIntegration as ce}from"astro-integration-kit";import{join as Rt,relative as St}from"node:path";import{fileURLToPath as M}from"node:url";import{defineUtility as jt,watchDirectory as bt}from"astro-integration-kit";import{normalizePath as J}from"vite";import{existsSync as lt,readdirSync as pt}from"node:fs";import{basename as mt,extname as ut}from"node:path";var z=(t,e,r)=>{let a=[];if(lt(t)){let o=pt(t).filter(d=>d.endsWith(".json"));for(let d of o)a.push({namespaceName:mt(d,ut(d)),fileName:d})}let u=a.map(o=>o.namespaceName);return r.info(`Detected namespaces: ${u.map(o=>`"${o}"`).join(",")}`),u.includes(e)||r.warn(`Default namespace "${e}" is not detected`),{namespaces:u}};import{existsSync as ft,readFileSync as dt,readdirSync as gt}from"node:fs";import{basename as ht,extname as yt,join as W}from"node:path";import{normalizePath as F}from"vite";var q=(t,{locales:e},r)=>{let a={},u=e.map(o=>({locale:o,dir:F(W(r,o))})).filter(o=>ft(o.dir));for(let{locale:o,dir:d}of u){let c=gt(d).filter(h=>h.endsWith(".json"));for(let h of c){let s=F(W(d,h));try{let p=JSON.parse(dt(s,"utf-8"));a[o]??={},a[o][ht(h,yt(h))]=p}catch{t.warn(`Can't parse "${s}", skipping.`)}}}return t.info(`${Object.keys(Object.values(a)).length} resources registered`),a};var xt=(t,e)=>{let r=J(M(new URL(e.localesDir,t))),a=Rt(r,e.defaultLocale);return{localesDir:r,defaultLocalesDir:a}},Ot="astro-i18n/i18next",G=jt("astro:config:setup")((t,e)=>{let r=t.logger.fork(Ot),a=xt(t.config.root,e);bt(t,a.localesDir),r.info(`Registered watcher for "${J(St(M(t.config.root),a.localesDir))}" directory`);let{namespaces:u}=z(a.defaultLocalesDir,e.defaultNamespace,r),o=q(r,e,a.localesDir),d=`
type Resources = ${JSON.stringify(o[e.defaultLocale]??{})}
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "${e.defaultNamespace}";
resources: Resources;
}
}
export {}
`;return{namespaces:u,resources:o,dtsContent:d}});import{z as g}from"astro/zod";import{withLeadingSlash as Dt,withoutTrailingSlash as At}from"ufo";import{z as R}from"astro/zod";import{EnumChangefreq as Lt}from"sitemap";var D=R.object({customPages:R.array(R.string().url()).optional(),entryLimit:R.number().min(1).optional().default(45e3),changefreq:R.nativeEnum(Lt).optional(),lastmod:R.date().optional(),priority:R.number().min(0).max(1).optional()}),Pt=R.object({internal:R.object({i18n:R.object({defaultLocale:R.string(),locales:R.array(R.string())}),routes:R.array(R.object({locale:R.string(),params:R.array(R.string()),pattern:R.string(),injectedRoute:R.object({pattern:R.string(),entrypoint:R.string(),prerender:R.boolean().optional()})}))})}),H=D.and(Pt);var Z=g.string().regex(/^[a-zA-Z0-9_/[\]-]+$/),It=g.literal(300).or(g.literal(301)).or(g.literal(302)).or(g.literal(303)).or(g.literal(304)).or(g.literal(307)).or(g.literal(308)),B=g.object({defaultLocale:g.string(),locales:g.array(g.string()),strategy:g.enum(["prefix","prefixExceptDefault"]).optional().default("prefixExceptDefault"),pages:g.record(Z,g.record(g.string(),Z.optional())).optional().default({}).transform(t=>Object.fromEntries(Object.entries(t).map(([e,r])=>[Dt(At(e)),r]))),localesDir:g.string().optional().default("./src/locales").refine(t=>t.startsWith("./")||t.startsWith("../"),{message:"Must be a relative path (ie. start with `./` or `../`)"}),defaultNamespace:g.string().optional().default("common"),client:g.literal(!1).or(g.object({translations:g.boolean().optional().default(!1),data:g.boolean().optional().default(!1),paths:g.boolean().optional().default(!1)})).optional().default(!1).transform(t=>typeof t=="boolean"?{data:t,translations:t,paths:t}:t),rootRedirect:g.object({status:It,destination:g.string()}).optional(),sitemap:g.union([g.boolean(),D]).optional().default(!1).transform(t=>t===!1?void 0:t===!0?{}:t)}).refine(({locales:t,defaultLocale:e})=>t.includes(e),{message:"`locales` must include the `defaultLocale`",path:["locales"]}).refine(({pages:t,locales:e})=>Object.values(t).every(r=>Object.keys(r).every(a=>e.includes(a))),{message:"`pages` locale keys must be included in `locales`",path:["pages"]}).refine(({strategy:t,rootRedirect:e})=>t==="prefix"?!0:e===void 0,{message:"`rootRedirect` should only be used with `strategy: 'prefix'`",path:["rootRedirect"]});import{defineUtility as Ht}from"astro-integration-kit";import{join as $t,relative as wt}from"node:path";import{fileURLToPath as V}from"node:url";import{defineUtility as Ct,watchDirectory as _t}from"astro-integration-kit";import{normalizePath as X}from"vite";var K=Ct("astro:config:setup")((t,e)=>{let{config:r}=t,a=X($t(V(r.srcDir),A));_t(t,a),e.info(`Registered watcher for "${X(wt(V(t.config.root),a))}" directory`)});import{mkdirSync as kt,readFileSync as Et,rmSync as Nt,writeFileSync as Tt}from"node:fs";import{dirname as C,join as vt,relative as _,resolve as Q}from"node:path";import{fileURLToPath as k}from"node:url";import{defineUtility as Ut}from"astro-integration-kit";import{addPageDir as zt}from"astro-pages";import{AstroError as Wt}from"astro/errors";import{withLeadingSlash as Ft}from"ufo";import{normalizePath as E}from"vite";var qt=t=>{let e=t.match(/export const prerender = (\w+)/);if(e)return e[1]==="true"},Mt=t=>Object.entries(zt({...t,dir:A}).pages).map(([e,r])=>({pattern:e,entrypoint:r})),Jt=Ut("astro:config:setup")(({config:t})=>{let e=k(new URL(A,t.srcDir)),r=Q(k(t.root),"./.astro/astro-i18n/entrypoints");return{routesDir:e,entrypointsDir:r}}),Gt=({strategy:t,defaultLocale:e,locales:r,pages:a},u,o,d,c)=>{let h=()=>{let i=u===e,l=i&&t==="prefixExceptDefault"?"":`/${u}`,n=Ft(i?o.pattern:a?.[o.pattern]?.[u]??o.pattern);return l+n},s=i=>{let l=(m,b,f)=>{let x=Q(C(b),m),L=_(C(f),x);return E(L)};kt(C(i),{recursive:!0});let n=Et(o.entrypoint,"utf-8");if(o.entrypoint.endsWith(".astro"))try{n=n.replaceAll("getLocalePlaceholder()",`"${u}"`).replaceAll("getLocalesPlaceholder()",`[${r.map(f=>`"${f}"`).join(", ")}]`).replaceAll("getDefaultLocalePlaceholder()",`"${e}"`);let[,m,...b]=n.split("---");if(!m)throw new Error("No frontmatter found");m=m.replace(/import\s+([\s\S]*?)\s+from\s+['"](.+?)['"]/g,(f,x,L)=>{let ct=L.startsWith("./")||L.startsWith("../")?l(L,o.entrypoint,i):L;return`import ${x} from '${ct}'`}),m=m.replace(/import\s*\(\s*['"](.+?)['"]\s*\)/g,(f,x)=>`import('${x.startsWith("./")||x.startsWith("../")?l(x,o.entrypoint,i):x}')`),n=`---${m}---${b.join("---")}`}catch{throw new Wt(`An error occured while transforming "${o.entrypoint}".`,"Make sure it has a valid frontmatter, even empty")}return Tt(i,n,"utf-8"),{prerender:qt(n)}},p=i=>{let l=[],n=i.match(/\[([^\]]+)]/g);if(n)for(let m of n)l.push(m.slice(1,-1));return l},y=h(),S=vt(d.entrypointsDir,u,E(_(d.routesDir,o.entrypoint))),{prerender:j}=s(S);return c.info(`Injecting "${y}" route`),{locale:u,params:p(y),pattern:o.pattern,injectedRoute:{pattern:y,entrypoint:S,...j?{prerender:j}:{}}}},Y=(t,e,r)=>{let{config:a,injectRoute:u}=t,{locales:o}=e;r.info("Starting routes injection...");let d=Jt(t);Nt(d.entrypointsDir,{recursive:!0,force:!0}),r.info(`Cleaned "${E(_(k(a.root),d.entrypointsDir))}" directory`);let c=[],h=Mt(t);for(let s of o)for(let p of h)c.push(Gt(e,s,p,d,r));for(let{injectedRoute:s}of c)u(s);return{routes:c}};var A="routes",Zt="astro-i18n/routing",tt=Ht("astro:config:setup")((t,e)=>{let r=t.logger.fork(Zt);K(t,r);let{routes:a}=Y(t,e,r);return{routes:a}});import{relative as ot}from"node:path";import{fileURLToPath as Xt}from"node:url";import Kt from"@inox-tools/aik-route-config";import{defineIntegration as Qt,hasIntegration as Yt,withPlugins as te}from"astro-integration-kit";import{AstroError as nt}from"astro/errors";import{z as ee}from"astro/zod";import{simpleSitemapAndIndex as re}from"sitemap";import{withoutTrailingSlash as oe}from"ufo";import{normalizePath as ne}from"vite";import{AstroError as Bt}from"astro/errors";var Vt=new Set(["404","500"]),N=t=>{let e=t;e.endsWith("/")&&(e=e.slice(0,-1));let r=e.split("/").pop()??"";return Vt.has(r)},T=t=>t.issues.map(r=>` ${r.path.join(".")} ${`${r.message}.`}`).join(`
`),$=t=>new Bt(t,"Please open an issue on GitHub at https://github.com/astrolicious/i18n/issues"),I=({segments:t})=>`/${t.map(r=>r.map(a=>a.dynamic?`[${a.content}]`:a.content).join("")).join("/")}`,w=t=>t?Array.isArray(t)?t:Object.entries(t).map(([e,r])=>({locale:e,params:r})):[],P=(t,e)=>e.trailingSlash==="never"?t:e.build.format==="directory"&&!t.endsWith("/")?`${t}/`:t;function et(t,e,r,a){let{changefreq:u,priority:o,lastmod:d}=r,c=d?.toISOString(),h=(p,y)=>{if(!p.route)return[];let S=[],j=t.filter(n=>n.route&&n.route.pattern===p.route.pattern&&n.route.locale!==p.route.locale);if(S.push({lang:p.route.locale,url:y}),p.routeData.params.length===0){for(let n of j)S.push({lang:n.route.locale,url:P(`${new URL(y).origin}${n.route.injectedRoute.pattern}`,a)});return[...S].sort((n,m)=>n.lang.localeCompare(m.lang,"en",{numeric:!0}))}let i=p.pages.indexOf(y),l=p.sitemapOptions.filter(n=>n.dynamicParams&&(Array.isArray(n.dynamicParams)?n.dynamicParams:Object.entries(n.dynamicParams)).length>0)[i];if(!l||!l.dynamicParams)return[];for(let n of j){let m=w(l.dynamicParams).find(f=>f.locale===n.route.locale);if(!m)continue;let b=n.route.injectedRoute.pattern;for(let[f,x]of Object.entries(m.params)){if(!x)throw $("This situation should never occur (value is not set)");b=b.replace(`[${f}]`,x)}b=P(`${new URL(y).origin}${b}`,a),S.push({lang:n.route.locale,url:b})}return[...S].sort((n,m)=>n.lang.localeCompare(m.lang,"en",{numeric:!0}))},s=[];for(let p of t)for(let y of p.pages){let S=[];if(p.route){let i=h({...p,route:p.route},y);S.push(...i)}let j={url:y,links:S};u&&Object.assign(j,{changefreq:u}),c&&Object.assign(j,{lastmod:c}),o&&Object.assign(j,{priority:o}),s.push(j)}return[...s].sort((p,y)=>p.url.localeCompare(y.url,"en",{numeric:!0}))}import{z as O}from"astro/zod";var rt=O.union([O.literal(!1),O.object({dynamicParams:O.union([O.record(O.record(O.string().optional())),O.array(O.object({locale:O.string(),params:O.record(O.string())}))]).optional()}).and(D.pick({lastmod:!0,priority:!0,changefreq:!0}).partial())]).optional().default({});var at="sitemap-index.xml",it=Qt({name:"astro-i18n/sitemap",optionsSchema:H,setup({options:t,name:e}){let r=t.internal.routes.map(u=>({pages:[],route:u,routeData:void 0,sitemapOptions:[],include:!0})),a;return te({name:e,plugins:[Kt],hooks:{"astro:config:setup":({defineRouteConfig:u,...o})=>{let{logger:d}=o;if(Yt(o,{name:"@astrojs/sitemap"}))throw new nt("Cannot use both `@astrolicious/i18n` sitemap and `@astrojs/sitemap` integrations at the same time.","Remove the `@astrojs/sitemap` integration from your project.");a=o.config,u({importName:"i18n:astro/sitemap",callbackHandler:({routeData:c},h)=>{let s=rt.safeParse(h);if(!s.success)throw new nt(T(s.error),"Check your usage of `astro:i18n/sitemap`");for(let p of c){let y=r.find(S=>S.route?.injectedRoute.pattern===I(p));if(y&&(y.routeData=p,y.include=s.data!==!1,s.data!==!1&&((s.data.changefreq||s.data.lastmod||s.data.priority)&&d.warn(`Setting \`changefreq\`, \`lastmod\` or \`priority\` on a route basis is not implemented yet (eg. on "${p.component}")`),y.sitemapOptions.push(s.data),y.route))){let{locale:S,injectedRoute:j}=y.route,i=w(s.data.dynamicParams)?.find(l=>l.locale===S);if(i){let l=j.pattern;for(let[n,m]of Object.entries(i.params))m&&(l=l.replace(`[${n}]`,m));y.pages.push(l)}}}}})},"astro:build:done":async u=>{let{logger:o}=u;for(let c of r)c.pages.length===0&&c.route&&c.pages.push(c.route.injectedRoute.pattern);for(let c of r.filter(h=>!h.routeData)){let h=u.routes.find(s=>oe(c.route?.injectedRoute.pattern)===I(s));if(!h)throw $("This situation should never occur (a corresponding routeData should always be found)");c.routeData=h,c.include=h.type==="page"}let d=[...r,...u.routes.filter(c=>!r.map(h=>I(h.routeData)).includes(I(c))).map(c=>({include:!0,routeData:c,pages:[],route:void 0,sitemapOptions:[]}))];try{if(!a.site){o.warn("The Sitemap integration requires the `site` astro.config option. Skipping.");return}let{customPages:c,entryLimit:h}=t;if(!a.site){o.warn("The `site` astro.config option is required. Skipping.");return}let s=new URL(a.base,a.site),p=u.pages.filter(i=>!N(i.pathname)).map(i=>{i.pathname!==""&&!s.pathname.endsWith("/")&&(s.pathname+="/"),i.pathname.startsWith("/")&&(i.pathname=i.pathname.slice(1));let l=s.pathname+i.pathname;return new URL(l,s).href}),y=d.reduce((i,l)=>{let n=l.routeData;if(!n||n.type!=="page")return i;if(n.pathname){if(N(n.pathname??n.route))return i;let m=s.pathname;m.endsWith("/")?m+=n.generate(n.pathname).substring(1):m+=n.generate(n.pathname);let b=new URL(m,s).href;i.push(P(b,a))}return i},[]);if(p=Array.from(new Set([...p,...y,...c??[]])),p=p.filter(i=>{let l=ne(`/${ot(a.base,new URL(i).pathname)}`),n=d.filter(m=>!m.include);for(let{routeData:m}of n)if(m.pattern.test(l))return!1;return!0}),p.length===0){o.warn(`No pages found!
\`${at}\` not created.`);return}for(let i of d.filter(l=>l.include))i.pages=i.pages.map(l=>l.startsWith("/")?P(new URL(l,s).href,a):l);let S=et(d.filter(i=>i.include),s.href,t,a),j=Xt(u.dir);await re({hostname:s.href,destinationDir:j,sourceData:S,limit:h,gzip:!1}),o.info(`\`${at}\` created at \`${ot(process.cwd(),j)}\``)}catch(c){if(c instanceof ee.ZodError)o.warn(T(c));else throw c}}}})}});var U="i18n:astro",st="__INTERNAL_ASTRO_I18N_CONFIG__",Or=ce({name:"astro-i18n",optionsSchema:B,setup({options:t,name:e}){let{resolve:r}=se(import.meta.url),a,u;return{hooks:{"astro:config:setup":o=>{let{addMiddleware:d,logger:c,updateConfig:h}=o,{routes:s}=tt(o,t),{namespaces:p,resources:y,dtsContent:S}=G(o,t);u=S,d({entrypoint:r("../assets/middleware.ts"),order:"pre"});let j=s.filter(f=>f.locale===t.defaultLocale),i=v(r("../assets/stubs/virtual.d.ts"),"utf-8"),l={id:"@@_ID_@@",locale:'"@@_LOCALE_@@"',localePathParams:'"@@_LOCALE_PATH_PARAMS_@@"',locales:'"@@_LOCALES_@@"'};if(a=i.replace(l.id,U).replace(l.locale,t.locales.map(f=>`"${f}"`).join(" | ")).replace(l.localePathParams,`{${j.map(f=>`"${f.pattern}": ${f.params.length===0?"never":`{
${f.params.map(x=>`"${x}": string;`).join(`
`)}
}`}`).join(`;
`)}}`).replace(l.locales,JSON.stringify(t.locales)),t.sitemap){ae(o,{integration:it({...t.sitemap,internal:{i18n:{defaultLocale:t.defaultLocale,locales:t.locales},routes:s}})});let f=v(r("../assets/stubs/sitemap.d.ts"),"utf-8");a+=f}let n=Object.entries(t.client).map(([f,x])=>({name:f,enabled:x})).filter(f=>f.enabled);n.length>0&&c.info(`Client features enabled: ${n.map(f=>`"${f.name}"`).join(", ")}. Make sure to use the \`<I18nClient />\` component`);let m=v(r("../assets/stubs/virtual.mjs"),"utf-8"),b={config:'"@@_CONFIG_@@"',i18next:'"@@_I18NEXT_@@"'};ie(o,{name:e,imports:[{id:"virtual:astro-i18n/internal",content:`
export const options = ${JSON.stringify(t)};
export const routes = ${JSON.stringify(s)};
export const i18nextConfig = ${JSON.stringify({namespaces:p,defaultNamespace:t.defaultNamespace,resources:y})};
export const clientId = ${JSON.stringify(st)};
`},{id:"virtual:astro-i18n/als",content:`
import { AsyncLocalStorage } from "node:async_hooks";
export const als = new AsyncLocalStorage;
`},{id:U,content:`
import { als } from "virtual:astro-i18n/als";
import _i18next from "i18next";
${m.replaceAll(b.config,"als.getStore()").replaceAll(b.i18next,"_i18next")}`,context:"server"},{id:U,content:(()=>{let f="";return t.client.translations&&(f+='import _i18next from "i18next"; '),f+=m.replaceAll(b.config,`JSON.parse(document.getElementById(${JSON.stringify(st)}).textContent)`),t.client.translations&&(f=f.replaceAll(b.i18next,"_i18next")),f})(),context:"client"}]}),c.info("Types injected"),t.strategy==="prefix"&&t.rootRedirect&&h({redirects:{"/":t.rootRedirect}})},"astro:config:done":o=>{o.injectTypes({filename:"astro-i18n.d.ts",content:a}),o.injectTypes({filename:"i18next.d.ts",content:u})}}}}});export{Or as integration};
//# sourceMappingURL=integration.js.map