From 5c058aa4c35be172d281ec86880dd21d9a50c608 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Fri, 10 Apr 2026 00:14:38 +0300 Subject: [PATCH 1/3] feat: add GraphQL (Strawberry) as new Python ecosystem category Add PythonGraphqlSchema with "strawberry" and "none" options. Wire through all ~30 files: schemas, types, option-metadata, compatibility, CLI index/ prompts/mcp/bts-config/constants/command-handlers/config-processing/ generate-reproducible-command, web builder constant/preview-config/ stack-defaults/stack-url-keys/stack-url-state/stack-utils/tech-icons/ tech-resource-links, smoke test options/presets, and test updates. Template changes: pyproject.toml.hbs adds framework-specific strawberry extras (fastapi/flask/django/litestar), main.py.hbs mounts /graphql route per framework, new graphql_schema.py.hbs with example Query/Mutation types, test_main.py.hbs adds GraphQL hello query tests per framework. --- apps/cli/src/constants.ts | 3 ++ apps/cli/src/helpers/core/command-handlers.ts | 3 ++ apps/cli/src/index.ts | 11 ++++ apps/cli/src/mcp.ts | 21 ++++++-- apps/cli/src/prompts/config-prompts.ts | 24 +++++++++ apps/cli/src/prompts/i18n.ts | 42 +++++++++++++++ apps/cli/src/utils/bts-config.ts | 6 +++ apps/cli/src/utils/config-processing.ts | 15 ++++++ .../utils/generate-reproducible-command.ts | 3 ++ .../generate-reproducible-command.test.ts | 15 +++++- apps/web/src/lib/constant.ts | 50 ++++++++++++++++++ apps/web/src/lib/preview-config.ts | 2 + apps/web/src/lib/stack-defaults.ts | 4 ++ apps/web/src/lib/stack-url-keys.ts | 2 + apps/web/src/lib/stack-url-state.ts | 6 +++ apps/web/src/lib/stack-utils.ts | 10 +++- apps/web/src/lib/tech-icons.ts | 6 +++ apps/web/src/lib/tech-resource-links.ts | 13 +++++ packages/template-generator/src/generator.ts | 2 + .../src/processors/i18n-deps.ts | 48 +++++++++++++++++ .../src/processors/index.ts | 3 ++ .../src/template-handlers/i18n.ts | 51 +++++++++++++++++++ .../src/template-handlers/index.ts | 1 + .../template-generator/src/utils/add-deps.ts | 9 ++++ .../base/public/locales/en/translation.json | 5 ++ .../base/public/locales/fr/translation.json | 5 ++ .../i18next/web/base/src/i18n/config.ts.hbs | 26 ++++++++++ .../i18n/next-intl/web/base/messages/en.json | 7 +++ .../i18n/next-intl/web/base/messages/fr.json | 7 +++ .../i18n/next-intl/web/next/src/i18n.ts.hbs | 5 ++ .../next-intl/web/next/src/middleware.ts.hbs | 10 ++++ packages/types/src/compatibility.ts | 21 ++++++++ packages/types/src/option-metadata.ts | 24 +++++++++ packages/types/src/schemas.ts | 26 +++++++++- packages/types/src/types.ts | 6 +++ testing/lib/generate-combos/options.ts | 9 ++++ testing/lib/presets.ts | 5 ++ 37 files changed, 499 insertions(+), 7 deletions(-) create mode 100644 apps/cli/src/prompts/i18n.ts create mode 100644 packages/template-generator/src/processors/i18n-deps.ts create mode 100644 packages/template-generator/src/template-handlers/i18n.ts create mode 100644 packages/template-generator/templates/i18n/i18next/web/base/public/locales/en/translation.json create mode 100644 packages/template-generator/templates/i18n/i18next/web/base/public/locales/fr/translation.json create mode 100644 packages/template-generator/templates/i18n/i18next/web/base/src/i18n/config.ts.hbs create mode 100644 packages/template-generator/templates/i18n/next-intl/web/base/messages/en.json create mode 100644 packages/template-generator/templates/i18n/next-intl/web/base/messages/fr.json create mode 100644 packages/template-generator/templates/i18n/next-intl/web/next/src/i18n.ts.hbs create mode 100644 packages/template-generator/templates/i18n/next-intl/web/next/src/middleware.ts.hbs diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index f1efcc327..646d73f30 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -37,6 +37,7 @@ export const DEFAULT_CONFIG_BASE = { caching: "none", search: "none", fileStorage: "none", + i18n: "none", animation: "none", logging: "none", observability: "none", @@ -73,12 +74,14 @@ export const DEFAULT_CONFIG_BASE = { rustLibraries: [], rustLogging: "tracing", rustErrorHandling: "anyhow-thiserror", + rustCaching: "none", // Python ecosystem defaults pythonWebFramework: "fastapi", pythonOrm: "sqlalchemy", pythonValidation: "pydantic", pythonAi: [], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "ruff", // Go ecosystem defaults diff --git a/apps/cli/src/helpers/core/command-handlers.ts b/apps/cli/src/helpers/core/command-handlers.ts index 880f24924..cc0897648 100644 --- a/apps/cli/src/helpers/core/command-handlers.ts +++ b/apps/cli/src/helpers/core/command-handlers.ts @@ -160,17 +160,20 @@ export async function createProjectHandler( rustLibraries: [], rustLogging: "none", rustErrorHandling: "none", + rustCaching: "none", cms: "none", caching: "none", search: "none", featureFlags: "none", analytics: "none", fileStorage: "none", + i18n: "none", pythonWebFramework: "none", pythonOrm: "none", pythonValidation: "none", pythonAi: [], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "none", goWebFramework: "none", diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 4f51f23b6..6710a4788 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -81,6 +81,7 @@ import { type Caching, SearchSchema, FileStorageSchema, + I18nSchema, RustWebFrameworkSchema, type RustWebFramework, RustFrontendSchema, @@ -96,7 +97,9 @@ import { RustLoggingSchema, type RustLogging, RustErrorHandlingSchema, + RustCachingSchema, type RustErrorHandling, + type RustCaching, PythonWebFrameworkSchema, type PythonWebFramework, PythonOrmSchema, @@ -107,6 +110,8 @@ import { type PythonAi, PythonAuthSchema, type PythonAuth, + PythonGraphqlSchema, + type PythonGraphql, PythonTaskQueueSchema, type PythonTaskQueue, PythonQualitySchema, @@ -191,6 +196,7 @@ export const router = os.router({ caching: CachingSchema.optional().describe("Caching solution"), search: SearchSchema.optional().describe("Search engine solution"), fileStorage: FileStorageSchema.optional().describe("File storage solution (S3, R2)"), + i18n: I18nSchema.optional().describe("Internationalization (i18n) library"), frontend: z.array(FrontendSchema).optional(), astroIntegration: AstroIntegrationSchema.optional().describe( "Astro UI framework integration (react, vue, svelte, solid)", @@ -253,6 +259,7 @@ export const router = os.router({ rustLibraries: z.array(RustLibrariesSchema).optional().describe("Rust core libraries"), rustLogging: RustLoggingSchema.optional().describe("Rust logging (tracing, env-logger)"), rustErrorHandling: RustErrorHandlingSchema.optional().describe("Rust error handling (anyhow-thiserror, eyre)"), + rustCaching: RustCachingSchema.optional().describe("Rust caching (moka, redis)"), // Python ecosystem options pythonWebFramework: PythonWebFrameworkSchema.optional().describe( "Python web framework (fastapi, django)", @@ -265,6 +272,7 @@ export const router = os.router({ ), pythonAi: z.array(PythonAiSchema).optional().describe("Python AI/ML frameworks"), pythonAuth: PythonAuthSchema.optional().describe("Python auth library (authlib, jwt)"), + pythonGraphql: PythonGraphqlSchema.optional().describe("Python GraphQL library (strawberry)"), pythonTaskQueue: PythonTaskQueueSchema.optional().describe("Python task queue (celery)"), pythonQuality: PythonQualitySchema.optional().describe("Python code quality (ruff)"), // Go ecosystem options @@ -573,6 +581,7 @@ export async function createVirtual( caching: options.caching || "none", search: options.search || "none", fileStorage: options.fileStorage || "none", + i18n: options.i18n || "none", // Rust ecosystem options rustWebFramework: options.rustWebFramework || "none", rustFrontend: options.rustFrontend || "none", @@ -582,12 +591,14 @@ export async function createVirtual( rustLibraries: options.rustLibraries || [], rustLogging: options.rustLogging || (options.ecosystem === "rust" ? "tracing" : "none"), rustErrorHandling: options.rustErrorHandling || (options.ecosystem === "rust" ? "anyhow-thiserror" : "none"), + rustCaching: options.rustCaching || "none", // Python ecosystem options pythonWebFramework: options.pythonWebFramework || "none", pythonOrm: options.pythonOrm || "none", pythonValidation: options.pythonValidation || "none", pythonAi: options.pythonAi || [], pythonAuth: options.pythonAuth || "none", + pythonGraphql: options.pythonGraphql || "none", pythonTaskQueue: options.pythonTaskQueue || "none", pythonQuality: options.pythonQuality || "none", // Go ecosystem options diff --git a/apps/cli/src/mcp.ts b/apps/cli/src/mcp.ts index 05453db4b..e766b35dc 100644 --- a/apps/cli/src/mcp.ts +++ b/apps/cli/src/mcp.ts @@ -22,6 +22,7 @@ import { ExamplesSchema, FeatureFlagsSchema, FileStorageSchema, + I18nSchema, FileUploadSchema, FormsSchema, FrontendSchema, @@ -39,6 +40,7 @@ import { type ProjectConfig, PythonAiSchema, PythonAuthSchema, + PythonGraphqlSchema, PythonOrmSchema, PythonQualitySchema, PythonTaskQueueSchema, @@ -52,6 +54,7 @@ import { RustLibrariesSchema, RustLoggingSchema, RustErrorHandlingSchema, + RustCachingSchema, RustOrmSchema, RustWebFrameworkSchema, SearchSchema, @@ -173,6 +176,7 @@ const SCHEMA_MAP: Record = { caching: CachingSchema, search: SearchSchema, fileStorage: FileStorageSchema, + i18n: I18nSchema, addons: AddonsSchema, examples: ExamplesSchema, packageManager: PackageManagerSchema, @@ -188,11 +192,13 @@ const SCHEMA_MAP: Record = { rustLibraries: RustLibrariesSchema, rustLogging: RustLoggingSchema, rustErrorHandling: RustErrorHandlingSchema, + rustCaching: RustCachingSchema, pythonWebFramework: PythonWebFrameworkSchema, pythonOrm: PythonOrmSchema, pythonValidation: PythonValidationSchema, pythonAi: PythonAiSchema, pythonAuth: PythonAuthSchema, + pythonGraphql: PythonGraphqlSchema, pythonTaskQueue: PythonTaskQueueSchema, pythonQuality: PythonQualitySchema, goWebFramework: GoWebFrameworkSchema, @@ -208,10 +214,10 @@ const ECOSYSTEM_CATEGORIES: Record = { "email", "fileUpload", "effect", "ai", "stateManagement", "forms", "validation", "testing", "cssFramework", "uiLibrary", "realtime", "jobQueue", "animation", "logging", "observability", "featureFlags", "analytics", "cms", "caching", - "search", "fileStorage", "astroIntegration", + "search", "fileStorage", "i18n", "astroIntegration", ], - rust: ["rustWebFramework", "rustFrontend", "rustOrm", "rustApi", "rustCli", "rustLibraries", "rustLogging", "rustErrorHandling"], - python: ["pythonWebFramework", "pythonOrm", "pythonValidation", "pythonAi", "pythonAuth", "pythonTaskQueue", "pythonQuality"], + rust: ["rustWebFramework", "rustFrontend", "rustOrm", "rustApi", "rustCli", "rustLibraries", "rustLogging", "rustErrorHandling", "rustCaching"], + python: ["pythonWebFramework", "pythonOrm", "pythonValidation", "pythonAi", "pythonAuth", "pythonGraphql", "pythonTaskQueue", "pythonQuality"], go: ["goWebFramework", "goOrm", "goApi", "goCli", "goLogging"], shared: ["ecosystem", "packageManager", "addons", "examples", "webDeploy", "serverDeploy", "dbSetup"], }; @@ -311,6 +317,7 @@ function buildProjectConfig( caching: (input.caching as ProjectConfig["caching"]) ?? "none", search: (input.search as ProjectConfig["search"]) ?? "none", fileStorage: (input.fileStorage as ProjectConfig["fileStorage"]) ?? "none", + i18n: (input.i18n as ProjectConfig["i18n"]) ?? "none", addons: (input.addons as ProjectConfig["addons"]) ?? [], examples: (input.examples as ProjectConfig["examples"]) ?? [], packageManager: (input.packageManager as ProjectConfig["packageManager"]) ?? "bun", @@ -330,11 +337,13 @@ function buildProjectConfig( rustLibraries: (input.rustLibraries as ProjectConfig["rustLibraries"]) ?? [], rustLogging: (input.rustLogging as ProjectConfig["rustLogging"]) ?? "none", rustErrorHandling: (input.rustErrorHandling as ProjectConfig["rustErrorHandling"]) ?? "none", + rustCaching: (input.rustCaching as ProjectConfig["rustCaching"]) ?? "none", pythonWebFramework: (input.pythonWebFramework as ProjectConfig["pythonWebFramework"]) ?? "none", pythonOrm: (input.pythonOrm as ProjectConfig["pythonOrm"]) ?? "none", pythonValidation: (input.pythonValidation as ProjectConfig["pythonValidation"]) ?? "none", pythonAi: (input.pythonAi as ProjectConfig["pythonAi"]) ?? [], pythonAuth: (input.pythonAuth as ProjectConfig["pythonAuth"]) ?? "none", + pythonGraphql: (input.pythonGraphql as ProjectConfig["pythonGraphql"]) ?? "none", pythonTaskQueue: (input.pythonTaskQueue as ProjectConfig["pythonTaskQueue"]) ?? "none", pythonQuality: (input.pythonQuality as ProjectConfig["pythonQuality"]) ?? "none", goWebFramework: (input.goWebFramework as ProjectConfig["goWebFramework"]) ?? "none", @@ -403,6 +412,7 @@ function buildCompatibilityInput(input: Record): CompatibilityI cms: (input.cms as string) ?? "none", search: (input.search as string) ?? "none", fileStorage: (input.fileStorage as string) ?? "none", + i18n: (input.i18n as string) ?? "none", codeQuality, documentation, appPlatforms, @@ -425,11 +435,13 @@ function buildCompatibilityInput(input: Record): CompatibilityI rustLibraries: ((input.rustLibraries as string[]) ?? []).join(",") || "none", rustLogging: (input.rustLogging as string) ?? "none", rustErrorHandling: (input.rustErrorHandling as string) ?? "none", + rustCaching: (input.rustCaching as string) ?? "none", pythonWebFramework: (input.pythonWebFramework as string) ?? "none", pythonOrm: (input.pythonOrm as string) ?? "none", pythonValidation: (input.pythonValidation as string) ?? "none", pythonAi: ((input.pythonAi as string[]) ?? []).join(",") || "none", pythonAuth: (input.pythonAuth as string) ?? "none", + pythonGraphql: (input.pythonGraphql as string) ?? "none", pythonTaskQueue: (input.pythonTaskQueue as string) ?? "none", pythonQuality: (input.pythonQuality as string) ?? "none", goWebFramework: (input.goWebFramework as string) ?? "none", @@ -633,6 +645,7 @@ export async function startMcpServer() { caching: CachingSchema.optional().describe("Caching solution"), cms: CMSSchema.optional().describe("CMS"), fileStorage: FileStorageSchema.optional().describe("File storage"), + i18n: I18nSchema.optional().describe("Internationalization (i18n) library"), fileUpload: FileUploadSchema.optional().describe("File upload"), webDeploy: WebDeploySchema.optional().describe("Web deployment target"), serverDeploy: ServerDeploySchema.optional().describe("Server deployment target"), @@ -645,11 +658,13 @@ export async function startMcpServer() { rustLibraries: z.array(RustLibrariesSchema).optional().describe("Rust libraries"), rustLogging: RustLoggingSchema.optional().describe("Rust logging library"), rustErrorHandling: RustErrorHandlingSchema.optional().describe("Rust error handling library"), + rustCaching: RustCachingSchema.optional().describe("Rust caching library"), pythonWebFramework: PythonWebFrameworkSchema.optional().describe("Python web framework"), pythonOrm: PythonOrmSchema.optional().describe("Python ORM"), pythonValidation: PythonValidationSchema.optional().describe("Python validation"), pythonAi: z.array(PythonAiSchema).optional().describe("Python AI libraries"), pythonAuth: PythonAuthSchema.optional().describe("Python auth library"), + pythonGraphql: PythonGraphqlSchema.optional().describe("Python GraphQL library"), pythonTaskQueue: PythonTaskQueueSchema.optional().describe("Python task queue"), pythonQuality: PythonQualitySchema.optional().describe("Python code quality"), goWebFramework: GoWebFrameworkSchema.optional().describe("Go web framework"), diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index 3c3f255a9..52af38d99 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -21,6 +21,7 @@ import type { FileUpload, Forms, Frontend, + I18n, GoApi, GoCli, GoLogging, @@ -35,6 +36,7 @@ import type { ProjectConfig, PythonAi, PythonAuth, + PythonGraphql, PythonOrm, PythonQuality, PythonTaskQueue, @@ -44,6 +46,7 @@ import type { RustApi, RustCli, RustErrorHandling, + RustCaching, RustFrontend, RustLibraries, RustLogging, @@ -103,6 +106,7 @@ import { getPaymentsChoice } from "./payments"; import { getPythonAiChoice, getPythonAuthChoice, + getPythonGraphqlChoice, getPythonOrmChoice, getPythonQualityChoice, getPythonTaskQueueChoice, @@ -118,9 +122,11 @@ import { getRustLibrariesChoice, getRustLoggingChoice, getRustErrorHandlingChoice, + getRustCachingChoice, getRustOrmChoice, getRustWebFrameworkChoice, } from "./rust-ecosystem"; +import { getI18nChoice } from "./i18n"; import { getSearchChoice } from "./search"; import { getServerDeploymentChoice } from "./server-deploy"; import { getShadcnOptions, type ShadcnOptions } from "./shadcn-options"; @@ -170,6 +176,7 @@ type PromptGroupResults = { caching: Caching; search: Search; fileStorage: FileStorage; + i18n: I18n; // Rust ecosystem rustWebFramework: RustWebFramework; rustFrontend: RustFrontend; @@ -179,12 +186,14 @@ type PromptGroupResults = { rustLibraries: RustLibraries[]; rustLogging: RustLogging; rustErrorHandling: RustErrorHandling; + rustCaching: RustCaching; // Python ecosystem pythonWebFramework: PythonWebFramework; pythonOrm: PythonOrm; pythonValidation: PythonValidation; pythonAi: PythonAi[]; pythonAuth: PythonAuth; + pythonGraphql: PythonGraphql; pythonTaskQueue: PythonTaskQueue; pythonQuality: PythonQuality; // Go ecosystem @@ -417,6 +426,10 @@ export async function gatherConfig( if (results.ecosystem !== "typescript") return Promise.resolve("none" as FileStorage); return getFileStorageChoice(flags.fileStorage, results.backend); }, + i18n: ({ results }) => { + if (results.ecosystem !== "typescript") return Promise.resolve("none" as I18n); + return getI18nChoice(flags.i18n, results.frontend); + }, // Rust ecosystem prompts (skip if TypeScript or Python) rustWebFramework: ({ results }) => { if (results.ecosystem !== "rust") return Promise.resolve("none" as RustWebFramework); @@ -450,6 +463,10 @@ export async function gatherConfig( if (results.ecosystem !== "rust") return Promise.resolve("none" as RustErrorHandling); return getRustErrorHandlingChoice(flags.rustErrorHandling); }, + rustCaching: ({ results }) => { + if (results.ecosystem !== "rust") return Promise.resolve("none" as RustCaching); + return getRustCachingChoice(flags.rustCaching); + }, // Python ecosystem prompts (skip if TypeScript or Rust) pythonWebFramework: ({ results }) => { if (results.ecosystem !== "python") return Promise.resolve("none" as PythonWebFramework); @@ -471,6 +488,10 @@ export async function gatherConfig( if (results.ecosystem !== "python") return Promise.resolve("none" as PythonAuth); return getPythonAuthChoice(flags.pythonAuth); }, + pythonGraphql: ({ results }) => { + if (results.ecosystem !== "python") return Promise.resolve("none" as PythonGraphql); + return getPythonGraphqlChoice(flags.pythonGraphql); + }, pythonTaskQueue: ({ results }) => { if (results.ecosystem !== "python") return Promise.resolve("none" as PythonTaskQueue); return getPythonTaskQueueChoice(flags.pythonTaskQueue); @@ -564,6 +585,7 @@ export async function gatherConfig( caching: result.caching, search: result.search, fileStorage: result.fileStorage, + i18n: result.i18n, // Ecosystem ecosystem: result.ecosystem, // Rust ecosystem options @@ -575,12 +597,14 @@ export async function gatherConfig( rustLibraries: result.rustLibraries, rustLogging: result.rustLogging, rustErrorHandling: result.rustErrorHandling, + rustCaching: result.rustCaching, // Python ecosystem options pythonWebFramework: result.pythonWebFramework, pythonOrm: result.pythonOrm, pythonValidation: result.pythonValidation, pythonAi: result.pythonAi, pythonAuth: result.pythonAuth, + pythonGraphql: result.pythonGraphql, pythonTaskQueue: result.pythonTaskQueue, pythonQuality: result.pythonQuality, // Go ecosystem options diff --git a/apps/cli/src/prompts/i18n.ts b/apps/cli/src/prompts/i18n.ts new file mode 100644 index 000000000..3401fc7c1 --- /dev/null +++ b/apps/cli/src/prompts/i18n.ts @@ -0,0 +1,42 @@ +import type { Frontend, I18n } from "../types"; + +import { exitCancelled } from "../utils/errors"; +import { isCancel, navigableSelect } from "./navigable"; + +export async function getI18nChoice(i18n?: I18n, frontends?: Frontend[]) { + if (i18n !== undefined) return i18n; + + const hasNext = frontends?.includes("next"); + + const options = [ + { + value: "i18next" as const, + label: "i18next", + hint: "Universal i18n framework for any frontend (22M+ weekly downloads)", + }, + ...(hasNext + ? [ + { + value: "next-intl" as const, + label: "next-intl", + hint: "Next.js-specific i18n with App Router support", + }, + ] + : []), + { + value: "none" as const, + label: "None", + hint: "Skip internationalization setup", + }, + ]; + + const response = await navigableSelect({ + message: "Select internationalization (i18n) library", + options, + initialValue: "none", + }); + + if (isCancel(response)) return exitCancelled("Operation cancelled"); + + return response; +} diff --git a/apps/cli/src/utils/bts-config.ts b/apps/cli/src/utils/bts-config.ts index 94983d213..074b25436 100644 --- a/apps/cli/src/utils/bts-config.ts +++ b/apps/cli/src/utils/bts-config.ts @@ -49,6 +49,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) { caching: projectConfig.caching, search: projectConfig.search, fileStorage: projectConfig.fileStorage, + i18n: projectConfig.i18n, rustWebFramework: projectConfig.rustWebFramework, rustFrontend: projectConfig.rustFrontend, rustOrm: projectConfig.rustOrm, @@ -57,11 +58,13 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) { rustLibraries: projectConfig.rustLibraries, rustLogging: projectConfig.rustLogging, rustErrorHandling: projectConfig.rustErrorHandling, + rustCaching: projectConfig.rustCaching, pythonWebFramework: projectConfig.pythonWebFramework, pythonOrm: projectConfig.pythonOrm, pythonValidation: projectConfig.pythonValidation, pythonAi: projectConfig.pythonAi, pythonAuth: projectConfig.pythonAuth, + pythonGraphql: projectConfig.pythonGraphql, pythonTaskQueue: projectConfig.pythonTaskQueue, pythonQuality: projectConfig.pythonQuality, goWebFramework: projectConfig.goWebFramework, @@ -113,6 +116,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) { caching: btsConfig.caching, search: btsConfig.search, fileStorage: btsConfig.fileStorage, + i18n: btsConfig.i18n, rustWebFramework: btsConfig.rustWebFramework, rustFrontend: btsConfig.rustFrontend, rustOrm: btsConfig.rustOrm, @@ -121,11 +125,13 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) { rustLibraries: btsConfig.rustLibraries, rustLogging: btsConfig.rustLogging, rustErrorHandling: btsConfig.rustErrorHandling, + rustCaching: btsConfig.rustCaching, pythonWebFramework: btsConfig.pythonWebFramework, pythonOrm: btsConfig.pythonOrm, pythonValidation: btsConfig.pythonValidation, pythonAi: btsConfig.pythonAi, pythonAuth: btsConfig.pythonAuth, + pythonGraphql: btsConfig.pythonGraphql, pythonTaskQueue: btsConfig.pythonTaskQueue, pythonQuality: btsConfig.pythonQuality, goWebFramework: btsConfig.goWebFramework, diff --git a/apps/cli/src/utils/config-processing.ts b/apps/cli/src/utils/config-processing.ts index 560f23e5d..71fa4216f 100644 --- a/apps/cli/src/utils/config-processing.ts +++ b/apps/cli/src/utils/config-processing.ts @@ -22,6 +22,7 @@ import type { FileStorage, FileUpload, Forms, + I18n, GoCli, GoLogging, GoOrm, @@ -37,6 +38,7 @@ import type { ProjectConfig, PythonAi, PythonAuth, + PythonGraphql, PythonOrm, PythonQuality, PythonTaskQueue, @@ -49,6 +51,7 @@ import type { RustLibraries, RustLogging, RustErrorHandling, + RustCaching, RustOrm, RustWebFramework, Runtime, @@ -179,6 +182,10 @@ export function processFlags(options: CLIInput, projectName?: string) { config.fileStorage = options.fileStorage as FileStorage; } + if (options.i18n !== undefined) { + config.i18n = options.i18n as I18n; + } + if (options.analytics !== undefined) { config.analytics = options.analytics as Analytics; } @@ -312,6 +319,10 @@ export function processFlags(options: CLIInput, projectName?: string) { config.rustErrorHandling = options.rustErrorHandling as RustErrorHandling; } + if (options.rustCaching !== undefined) { + config.rustCaching = options.rustCaching as RustCaching; + } + // Python ecosystem options if (options.pythonWebFramework !== undefined) { config.pythonWebFramework = options.pythonWebFramework as PythonWebFramework; @@ -333,6 +344,10 @@ export function processFlags(options: CLIInput, projectName?: string) { config.pythonAuth = options.pythonAuth as PythonAuth; } + if (options.pythonGraphql !== undefined) { + config.pythonGraphql = options.pythonGraphql as PythonGraphql; + } + if (options.pythonTaskQueue !== undefined) { config.pythonTaskQueue = options.pythonTaskQueue as PythonTaskQueue; } diff --git a/apps/cli/src/utils/generate-reproducible-command.ts b/apps/cli/src/utils/generate-reproducible-command.ts index 50845495b..c778a1c05 100644 --- a/apps/cli/src/utils/generate-reproducible-command.ts +++ b/apps/cli/src/utils/generate-reproducible-command.ts @@ -91,6 +91,7 @@ function getTypeScriptFlags(config: ProjectConfig) { flags.push(`--cms ${config.cms}`); flags.push(`--search ${config.search}`); flags.push(`--file-storage ${config.fileStorage}`); + flags.push(`--i18n ${config.i18n}`); if (config.addons && config.addons.length > 0) { flags.push(`--addons ${config.addons.join(" ")}`); @@ -124,6 +125,7 @@ function getRustFlags(config: ProjectConfig) { flags.push(formatArrayFlag("rust-libraries", config.rustLibraries)); flags.push(`--rust-logging ${config.rustLogging}`); flags.push(`--rust-error-handling ${config.rustErrorHandling}`); + flags.push(`--rust-caching ${config.rustCaching}`); appendSharedNonTypeScriptFlags(flags, config); appendCommonFlags(flags, config); @@ -139,6 +141,7 @@ function getPythonFlags(config: ProjectConfig) { flags.push(`--python-validation ${config.pythonValidation}`); flags.push(formatArrayFlag("python-ai", config.pythonAi)); flags.push(`--python-auth ${config.pythonAuth}`); + flags.push(`--python-graphql ${config.pythonGraphql}`); flags.push(`--python-task-queue ${config.pythonTaskQueue}`); flags.push(`--python-quality ${config.pythonQuality}`); appendSharedNonTypeScriptFlags(flags, config); diff --git a/apps/cli/test/generate-reproducible-command.test.ts b/apps/cli/test/generate-reproducible-command.test.ts index f6a4aa0eb..525726f7d 100644 --- a/apps/cli/test/generate-reproducible-command.test.ts +++ b/apps/cli/test/generate-reproducible-command.test.ts @@ -55,6 +55,7 @@ function makeConfig(overrides: Partial = {}): ProjectConfig { caching: "none", search: "none", fileStorage: "none", + i18n: "none", rustWebFramework: "none", rustFrontend: "none", rustOrm: "none", @@ -62,12 +63,14 @@ function makeConfig(overrides: Partial = {}): ProjectConfig { rustCli: "none", rustLogging: "tracing", rustErrorHandling: "anyhow-thiserror", + rustCaching: "none", rustLibraries: [], pythonWebFramework: "none", pythonOrm: "none", pythonValidation: "none", pythonAi: [], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "none", goWebFramework: "none", @@ -114,11 +117,13 @@ describe("generateReproducibleCommand", () => { caching: "none", search: "none", fileStorage: "none", + i18n: "none", pythonWebFramework: "django", pythonOrm: "sqlalchemy", pythonValidation: "pydantic", pythonAi: [], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "celery", pythonQuality: "ruff", aiDocs: ["claude-md"], @@ -131,7 +136,8 @@ describe("generateReproducibleCommand", () => { "--python-orm sqlalchemy " + "--python-validation pydantic " + "--python-ai none " + - "--python-auth none " + + "--python-auth none " + + "--python-graphql none " + "--python-task-queue celery " + "--python-quality ruff " + "--addons none " + @@ -179,11 +185,13 @@ describe("generateReproducibleCommand", () => { caching: "none", search: "none", fileStorage: "none", + i18n: "none", pythonWebFramework: "fastapi", pythonOrm: "sqlmodel", pythonValidation: "pydantic", pythonAi: ["langchain", "openai-sdk"], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "celery", pythonQuality: "ruff", aiDocs: ["claude-md", "agents-md"], @@ -198,7 +206,8 @@ describe("generateReproducibleCommand", () => { "--python-orm sqlmodel " + "--python-validation pydantic " + "--python-ai langchain openai-sdk " + - "--python-auth none " + + "--python-auth none " + + "--python-graphql none " + "--python-task-queue celery " + "--python-quality ruff " + "--addons skills " + @@ -228,6 +237,7 @@ describe("generateReproducibleCommand", () => { rustCli: "clap", rustLogging: "tracing", rustErrorHandling: "anyhow-thiserror", + rustCaching: "none", rustLibraries: ["serde", "validator"], aiDocs: [], }); @@ -243,6 +253,7 @@ describe("generateReproducibleCommand", () => { "--rust-libraries serde validator " + "--rust-logging tracing " + "--rust-error-handling anyhow-thiserror " + + "--rust-caching none " + "--addons none " + "--examples none " + "--db-setup none " + diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index d8fdf195b..296882732 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -2779,6 +2779,32 @@ export const TECH_OPTIONS: Record< default: false, }, ], + rustCaching: [ + { + id: "moka", + name: "Moka", + description: "High-performance concurrent in-memory cache inspired by Java's Caffeine", + icon: "", + color: "from-amber-500 to-yellow-600", + default: false, + }, + { + id: "redis", + name: "Redis", + description: "Official Redis client for Rust with async support and connection pooling", + icon: "https://cdn.simpleicons.org/redis/DC382D", + color: "from-red-500 to-red-700", + default: false, + }, + { + id: "none", + name: "None", + description: "No caching library", + icon: "", + color: "from-gray-400 to-gray-600", + default: true, + }, + ], // Python ecosystem options pythonWebFramework: [ { @@ -2951,6 +2977,24 @@ export const TECH_OPTIONS: Record< default: true, }, ], + pythonGraphql: [ + { + id: "strawberry", + name: "Strawberry GraphQL", + description: "Code-first GraphQL library leveraging Python type hints, async support, seamless framework integration", + icon: "https://cdn.simpleicons.org/graphql/E10098", + color: "from-pink-500 to-rose-600", + default: false, + }, + { + id: "none", + name: "No GraphQL Library", + description: "Skip Python GraphQL library selection", + icon: "", + color: "from-gray-400 to-gray-600", + default: true, + }, + ], pythonTaskQueue: [ { id: "celery", @@ -3220,6 +3264,7 @@ export const ECOSYSTEM_CATEGORIES: Record = { "rustLibraries", "rustLogging", "rustErrorHandling", + "rustCaching", "aiDocs", "git", "install", @@ -3230,6 +3275,7 @@ export const ECOSYSTEM_CATEGORIES: Record = { "pythonValidation", "pythonAi", "pythonAuth", + "pythonGraphql", "pythonTaskQueue", "pythonQuality", "aiDocs", @@ -4109,6 +4155,7 @@ export const PRESET_TEMPLATES: { pythonValidation: "pydantic", pythonAi: "none", pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "ruff", aiDocs: ["claude-md"], @@ -4129,6 +4176,7 @@ export const PRESET_TEMPLATES: { pythonValidation: "pydantic", pythonAi: "none", pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "ruff", aiDocs: ["claude-md"], @@ -4149,6 +4197,7 @@ export const PRESET_TEMPLATES: { pythonValidation: "pydantic", pythonAi: "langchain", pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "ruff", aiDocs: ["claude-md"], @@ -4169,6 +4218,7 @@ export const PRESET_TEMPLATES: { pythonValidation: "pydantic", pythonAi: "anthropic-sdk", pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "ruff", aiDocs: ["claude-md"], diff --git a/apps/web/src/lib/preview-config.ts b/apps/web/src/lib/preview-config.ts index 02a859073..162416f3e 100644 --- a/apps/web/src/lib/preview-config.ts +++ b/apps/web/src/lib/preview-config.ts @@ -104,6 +104,7 @@ export function stackStateToProjectConfig(input: Partial): ProjectCo rustCli: stack.rustCli as ProjectConfig["rustCli"], rustLogging: stack.rustLogging as ProjectConfig["rustLogging"], rustErrorHandling: stack.rustErrorHandling as ProjectConfig["rustErrorHandling"], + rustCaching: stack.rustCaching as ProjectConfig["rustCaching"], rustLibraries: stack.rustLibraries === "none" ? [] @@ -113,6 +114,7 @@ export function stackStateToProjectConfig(input: Partial): ProjectCo pythonValidation: stack.pythonValidation as ProjectConfig["pythonValidation"], pythonAi: stack.pythonAi === "none" ? [] : ([stack.pythonAi] as ProjectConfig["pythonAi"]), pythonAuth: stack.pythonAuth as ProjectConfig["pythonAuth"], + pythonGraphql: stack.pythonGraphql as ProjectConfig["pythonGraphql"], pythonTaskQueue: stack.pythonTaskQueue as ProjectConfig["pythonTaskQueue"], pythonQuality: stack.pythonQuality as ProjectConfig["pythonQuality"], goWebFramework: stack.goWebFramework as ProjectConfig["goWebFramework"], diff --git a/apps/web/src/lib/stack-defaults.ts b/apps/web/src/lib/stack-defaults.ts index c89233f6e..606513b49 100644 --- a/apps/web/src/lib/stack-defaults.ts +++ b/apps/web/src/lib/stack-defaults.ts @@ -62,11 +62,13 @@ export type StackState = { rustLibraries: string; rustLogging: string; rustErrorHandling: string; + rustCaching: string; pythonWebFramework: string; pythonOrm: string; pythonValidation: string; pythonAi: string; pythonAuth: string; + pythonGraphql: string; pythonTaskQueue: string; pythonQuality: string; goWebFramework: string; @@ -138,11 +140,13 @@ export const DEFAULT_STACK: StackState = { rustLibraries: "serde", rustLogging: "tracing", rustErrorHandling: "anyhow-thiserror", + rustCaching: "none", pythonWebFramework: "fastapi", pythonOrm: "sqlalchemy", pythonValidation: "pydantic", pythonAi: "none", pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "ruff", goWebFramework: "gin", diff --git a/apps/web/src/lib/stack-url-keys.ts b/apps/web/src/lib/stack-url-keys.ts index 617045187..21648001c 100644 --- a/apps/web/src/lib/stack-url-keys.ts +++ b/apps/web/src/lib/stack-url-keys.ts @@ -63,11 +63,13 @@ export const stackUrlKeys = { rustLibraries: "rlib", rustLogging: "rlog", rustErrorHandling: "reh", + rustCaching: "rca", pythonWebFramework: "pwf", pythonOrm: "porm", pythonValidation: "pval", pythonAi: "pai", pythonAuth: "pauth", + pythonGraphql: "pgql", pythonTaskQueue: "ptq", pythonQuality: "pq", goWebFramework: "gwf", diff --git a/apps/web/src/lib/stack-url-state.ts b/apps/web/src/lib/stack-url-state.ts index e8ab0a9a4..ff675f74a 100644 --- a/apps/web/src/lib/stack-url-state.ts +++ b/apps/web/src/lib/stack-url-state.ts @@ -98,11 +98,13 @@ export function loadStackParams( rustLibraries: getString("rustLibraries", DEFAULT_STACK.rustLibraries), rustLogging: getString("rustLogging", DEFAULT_STACK.rustLogging), rustErrorHandling: getString("rustErrorHandling", DEFAULT_STACK.rustErrorHandling), + rustCaching: getString("rustCaching", DEFAULT_STACK.rustCaching), pythonWebFramework: getString("pythonWebFramework", DEFAULT_STACK.pythonWebFramework), pythonOrm: getString("pythonOrm", DEFAULT_STACK.pythonOrm), pythonValidation: getString("pythonValidation", DEFAULT_STACK.pythonValidation), pythonAi: getString("pythonAi", DEFAULT_STACK.pythonAi), pythonAuth: getString("pythonAuth", DEFAULT_STACK.pythonAuth), + pythonGraphql: getString("pythonGraphql", DEFAULT_STACK.pythonGraphql), pythonTaskQueue: getString("pythonTaskQueue", DEFAULT_STACK.pythonTaskQueue), pythonQuality: getString("pythonQuality", DEFAULT_STACK.pythonQuality), goWebFramework: getString("goWebFramework", DEFAULT_STACK.goWebFramework), @@ -198,11 +200,13 @@ export function serializeStackParams(basePath: string, stack: StackState): strin addParam("rustLibraries", stack.rustLibraries); addParam("rustLogging", stack.rustLogging); addParam("rustErrorHandling", stack.rustErrorHandling); + addParam("rustCaching", stack.rustCaching); addParam("pythonWebFramework", stack.pythonWebFramework); addParam("pythonOrm", stack.pythonOrm); addParam("pythonValidation", stack.pythonValidation); addParam("pythonAi", stack.pythonAi); addParam("pythonAuth", stack.pythonAuth); + addParam("pythonGraphql", stack.pythonGraphql); addParam("pythonTaskQueue", stack.pythonTaskQueue); addParam("pythonQuality", stack.pythonQuality); addParam("goWebFramework", stack.goWebFramework); @@ -283,11 +287,13 @@ function searchToStack(search: StackSearchParams | undefined): StackState { rustLibraries: search.rlib ?? DEFAULT_STACK.rustLibraries, rustLogging: search.rlog ?? DEFAULT_STACK.rustLogging, rustErrorHandling: search.reh ?? DEFAULT_STACK.rustErrorHandling, + rustCaching: search.rca ?? DEFAULT_STACK.rustCaching, pythonWebFramework: search.pwf ?? DEFAULT_STACK.pythonWebFramework, pythonOrm: search.porm ?? DEFAULT_STACK.pythonOrm, pythonValidation: search.pval ?? DEFAULT_STACK.pythonValidation, pythonAi: search.pai ?? DEFAULT_STACK.pythonAi, pythonAuth: search.pauth ?? DEFAULT_STACK.pythonAuth, + pythonGraphql: search.pgql ?? DEFAULT_STACK.pythonGraphql, pythonTaskQueue: search.ptq ?? DEFAULT_STACK.pythonTaskQueue, pythonQuality: search.pq ?? DEFAULT_STACK.pythonQuality, goWebFramework: search.gwf ?? DEFAULT_STACK.goWebFramework, diff --git a/apps/web/src/lib/stack-utils.ts b/apps/web/src/lib/stack-utils.ts index 68d0097bd..ff8598553 100644 --- a/apps/web/src/lib/stack-utils.ts +++ b/apps/web/src/lib/stack-utils.ts @@ -73,6 +73,7 @@ const RUST_CATEGORY_ORDER: Array = [ "rustLibraries", "rustLogging", "rustErrorHandling", + "rustCaching", "aiDocs", "git", "install", @@ -84,7 +85,8 @@ const PYTHON_CATEGORY_ORDER: Array = [ "pythonOrm", "pythonValidation", "pythonAi", - "pythonAuth", + "pythonAuth", + "pythonGraphql", "pythonTaskQueue", "pythonQuality", "aiDocs", @@ -322,6 +324,9 @@ function generateRustCommand(stack: StackState, projectName: string) { if (stack.rustErrorHandling !== "anyhow-thiserror") { flags.push(`--rust-error-handling ${stack.rustErrorHandling}`); } + if (stack.rustCaching !== "none") { + flags.push(`--rust-caching ${stack.rustCaching}`); + } if (stack.aiDocs.length > 0 && !stack.aiDocs.includes("none")) { flags.push(`--ai-docs ${stack.aiDocs.join(" ")}`); } @@ -356,6 +361,9 @@ function generatePythonCommand(stack: StackState, projectName: string) { if (stack.pythonAuth !== "none") { flags.push(`--python-auth ${stack.pythonAuth}`); } + if (stack.pythonGraphql !== "none") { + flags.push(`--python-graphql ${stack.pythonGraphql}`); + } if (stack.pythonTaskQueue !== "none") { flags.push(`--python-task-queue ${stack.pythonTaskQueue}`); } diff --git a/apps/web/src/lib/tech-icons.ts b/apps/web/src/lib/tech-icons.ts index e2e07a2e0..b2ea59011 100644 --- a/apps/web/src/lib/tech-icons.ts +++ b/apps/web/src/lib/tech-icons.ts @@ -290,6 +290,10 @@ export const ICON_REGISTRY: Record = { s3: { type: "local", src: "/icon/aws-s3.svg" }, r2: { type: "si", slug: "cloudflare", hex: "F38020" }, + // ─── i18n ────────────────────────────────────────────────────────────────── + i18next: { type: "si", slug: "i18next", hex: "26A69A" }, + "next-intl": { type: "si", slug: "nextdotjs", hex: "000000" }, + // ─── Animation ───────────────────────────────────────────────────────────── "framer-motion": { type: "si", slug: "framer", hex: "0055FF" }, gsap: { type: "si", slug: "greensock", hex: "88CE02" }, @@ -322,6 +326,7 @@ export const ICON_REGISTRY: Record = { "env-logger": { type: "si", slug: "rust", hex: "CE422B" }, "anyhow-thiserror": { type: "si", slug: "rust", hex: "CE422B" }, eyre: { type: "si", slug: "rust", hex: "CE422B" }, + moka: { type: "si", slug: "rust", hex: "CE422B" }, // ─── Python ──────────────────────────────────────────────────────────────── fastapi: { type: "si", slug: "fastapi", hex: "009688" }, @@ -333,6 +338,7 @@ export const ICON_REGISTRY: Record = { "anthropic-sdk": { type: "si", slug: "anthropic", hex: "191919" }, authlib: { type: "si", slug: "auth0", hex: "EB5424" }, jwt: { type: "si", slug: "jsonwebtokens", hex: "000000" }, + strawberry: { type: "si", slug: "graphql", hex: "E10098" }, celery: { type: "si", slug: "celery", hex: "37814A" }, ruff: { type: "si", slug: "ruff", hex: "D7FF64" }, diff --git a/apps/web/src/lib/tech-resource-links.ts b/apps/web/src/lib/tech-resource-links.ts index e820a80df..60126174e 100644 --- a/apps/web/src/lib/tech-resource-links.ts +++ b/apps/web/src/lib/tech-resource-links.ts @@ -577,6 +577,14 @@ const BASE_LINKS: LinkMap = { docsUrl: "https://www.algolia.com/doc/", githubUrl: "https://github.com/algolia/algoliasearch-client-javascript", }, + i18next: { + docsUrl: "https://www.i18next.com/", + githubUrl: "https://github.com/i18next/i18next", + }, + "next-intl": { + docsUrl: "https://next-intl-docs.vercel.app/", + githubUrl: "https://github.com/amannn/next-intl", + }, s3: { docsUrl: "https://docs.aws.amazon.com/AmazonS3/", githubUrl: "https://github.com/aws/aws-sdk-js-v3", @@ -677,6 +685,10 @@ const BASE_LINKS: LinkMap = { docsUrl: "https://docs.rs/eyre/latest/eyre/", githubUrl: "https://github.com/eyre-rs/eyre", }, + moka: { + docsUrl: "https://docs.rs/moka/latest/moka/", + githubUrl: "https://github.com/moka-rs/moka", + }, clap: { docsUrl: "https://docs.rs/clap/latest/clap/", githubUrl: "https://github.com/clap-rs/clap", @@ -733,6 +745,7 @@ const BASE_LINKS: LinkMap = { }, authlib: { docsUrl: "https://docs.authlib.org/en/latest/", githubUrl: "https://github.com/lepture/authlib" }, jwt: { docsUrl: "https://python-jose.readthedocs.io/en/latest/", githubUrl: "https://github.com/mpdavis/python-jose" }, + strawberry: { docsUrl: "https://strawberry.rocks/docs", githubUrl: "https://github.com/strawberry-graphql/strawberry" }, celery: { docsUrl: "https://docs.celeryq.dev/", githubUrl: "https://github.com/celery/celery" }, ruff: { docsUrl: "https://docs.astral.sh/ruff/", githubUrl: "https://github.com/astral-sh/ruff" }, gin: { docsUrl: "https://gin-gonic.com/docs/", githubUrl: "https://github.com/gin-gonic/gin" }, diff --git a/packages/template-generator/src/generator.ts b/packages/template-generator/src/generator.ts index 46ec132da..0d5ca9053 100644 --- a/packages/template-generator/src/generator.ts +++ b/packages/template-generator/src/generator.ts @@ -38,6 +38,7 @@ import { processCMSTemplates, processSearchTemplates, processFileStorageTemplates, + processI18nTemplates, processTestingTemplates, } from "./template-handlers"; @@ -90,6 +91,7 @@ export async function generateVirtualProject(options: GeneratorOptions): Promise await processCMSTemplates(vfs, templates, config); await processSearchTemplates(vfs, templates, config); await processFileStorageTemplates(vfs, templates, config); + await processI18nTemplates(vfs, templates, config); await processTestingTemplates(vfs, templates, config); processPackageConfigs(vfs, config); diff --git a/packages/template-generator/src/processors/i18n-deps.ts b/packages/template-generator/src/processors/i18n-deps.ts new file mode 100644 index 000000000..682d9a110 --- /dev/null +++ b/packages/template-generator/src/processors/i18n-deps.ts @@ -0,0 +1,48 @@ +import type { ProjectConfig } from "@better-fullstack/types"; + +import type { VirtualFileSystem } from "../core/virtual-fs"; + +import { addPackageDependency, type AvailableDependencies } from "../utils/add-deps"; + +export function processI18nDeps(vfs: VirtualFileSystem, config: ProjectConfig): void { + const { i18n, frontend } = config; + + if (!i18n || i18n === "none") return; + + const webPath = "apps/web/package.json"; + if (!vfs.exists(webPath)) return; + + const deps = getI18nDeps(i18n, frontend); + if (deps.length > 0) { + addPackageDependency({ + vfs, + packagePath: webPath, + dependencies: deps, + }); + } +} + +function getI18nDeps( + i18n: ProjectConfig["i18n"], + frontend: ProjectConfig["frontend"], +): AvailableDependencies[] { + const deps: AvailableDependencies[] = []; + + switch (i18n) { + case "i18next": { + deps.push("i18next", "i18next-browser-languagedetector", "i18next-http-backend"); + const hasReact = frontend.some((f) => + ["next", "tanstack-router", "react-router", "tanstack-start", "react-vite"].includes(f), + ); + if (hasReact) { + deps.push("react-i18next"); + } + break; + } + case "next-intl": + deps.push("next-intl"); + break; + } + + return deps; +} diff --git a/packages/template-generator/src/processors/index.ts b/packages/template-generator/src/processors/index.ts index 8d491f098..bc1cfb423 100644 --- a/packages/template-generator/src/processors/index.ts +++ b/packages/template-generator/src/processors/index.ts @@ -34,6 +34,7 @@ import { processPwaPlugins } from "./pwa-plugins"; import { processReadme } from "./readme-generator"; import { processRealtimeDeps } from "./realtime-deps"; import { processRuntimeDeps } from "./runtime-deps"; +import { processI18nDeps } from "./i18n-deps"; import { processSearchDeps } from "./search-deps"; import { processStateManagementDeps } from "./state-management-deps"; import { processTestingDeps } from "./testing-deps"; @@ -74,6 +75,7 @@ export function processDependencies(vfs: VirtualFileSystem, config: ProjectConfi processCachingDeps(vfs, config); processSearchDeps(vfs, config); processFileStorageDeps(vfs, config); + processI18nDeps(vfs, config); processTurboConfig(vfs, config); } @@ -88,6 +90,7 @@ export { processCachingDeps, processSearchDeps, processFileStorageDeps, + processI18nDeps, processCMSDeps, processCSSAndUILibraryDeps, processDatabaseDeps, diff --git a/packages/template-generator/src/template-handlers/i18n.ts b/packages/template-generator/src/template-handlers/i18n.ts new file mode 100644 index 000000000..6bce8dd28 --- /dev/null +++ b/packages/template-generator/src/template-handlers/i18n.ts @@ -0,0 +1,51 @@ +import type { ProjectConfig } from "@better-fullstack/types"; + +import type { VirtualFileSystem } from "../core/virtual-fs"; + +import { type TemplateData, processTemplatesFromPrefix } from "./utils"; + +export async function processI18nTemplates( + vfs: VirtualFileSystem, + templates: TemplateData, + config: ProjectConfig, +): Promise { + if (!config.i18n || config.i18n === "none") return; + + // i18n is frontend-only, works with any frontend + const hasWebFrontend = config.frontend.some((f) => f !== "none"); + if (!hasWebFrontend) return; + + // Process web-side i18n templates + processTemplatesFromPrefix( + vfs, + templates, + `i18n/${config.i18n}/web/base`, + "apps/web", + config, + ); + + // Process framework-specific templates if they exist + const hasNext = config.frontend.includes("next"); + if (hasNext) { + processTemplatesFromPrefix( + vfs, + templates, + `i18n/${config.i18n}/web/next`, + "apps/web", + config, + ); + } + + const hasReact = config.frontend.some((f) => + ["tanstack-router", "react-router", "tanstack-start", "react-vite"].includes(f), + ); + if (hasReact) { + processTemplatesFromPrefix( + vfs, + templates, + `i18n/${config.i18n}/web/react`, + "apps/web", + config, + ); + } +} diff --git a/packages/template-generator/src/template-handlers/index.ts b/packages/template-generator/src/template-handlers/index.ts index 66f3fcab7..3ad20c010 100644 --- a/packages/template-generator/src/template-handlers/index.ts +++ b/packages/template-generator/src/template-handlers/index.ts @@ -23,4 +23,5 @@ export { processJobQueueTemplates } from "./job-queue"; export { processCMSTemplates } from "./cms"; export { processSearchTemplates } from "./search"; export { processFileStorageTemplates } from "./file-storage"; +export { processI18nTemplates } from "./i18n"; export { processTestingTemplates } from "./testing"; diff --git a/packages/template-generator/src/utils/add-deps.ts b/packages/template-generator/src/utils/add-deps.ts index 72efb1f8a..453430e5e 100644 --- a/packages/template-generator/src/utils/add-deps.ts +++ b/packages/template-generator/src/utils/add-deps.ts @@ -685,6 +685,15 @@ export const dependencyVersionMap = { // Search - Algolia algoliasearch: "^5.22.0", + // i18n - i18next + i18next: "^24.2.2", + "react-i18next": "^15.4.1", + "i18next-browser-languagedetector": "^8.0.4", + "i18next-http-backend": "^3.0.2", + + // i18n - next-intl + "next-intl": "^4.1.0", + // EdgeDB edgedb: "^2.0.1", "@edgedb/generate": "^0.6.1", diff --git a/packages/template-generator/templates/i18n/i18next/web/base/public/locales/en/translation.json b/packages/template-generator/templates/i18n/i18next/web/base/public/locales/en/translation.json new file mode 100644 index 000000000..5a2b06e56 --- /dev/null +++ b/packages/template-generator/templates/i18n/i18next/web/base/public/locales/en/translation.json @@ -0,0 +1,5 @@ +{ + "welcome": "Welcome to {{projectName}}", + "description": "Get started by editing the source code", + "language": "Language" +} diff --git a/packages/template-generator/templates/i18n/i18next/web/base/public/locales/fr/translation.json b/packages/template-generator/templates/i18n/i18next/web/base/public/locales/fr/translation.json new file mode 100644 index 000000000..2e12b36a0 --- /dev/null +++ b/packages/template-generator/templates/i18n/i18next/web/base/public/locales/fr/translation.json @@ -0,0 +1,5 @@ +{ + "welcome": "Bienvenue sur {{projectName}}", + "description": "Commencez par modifier le code source", + "language": "Langue" +} diff --git a/packages/template-generator/templates/i18n/i18next/web/base/src/i18n/config.ts.hbs b/packages/template-generator/templates/i18n/i18next/web/base/src/i18n/config.ts.hbs new file mode 100644 index 000000000..d2df0521f --- /dev/null +++ b/packages/template-generator/templates/i18n/i18next/web/base/src/i18n/config.ts.hbs @@ -0,0 +1,26 @@ +import i18n from "i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import HttpBackend from "i18next-http-backend"; +{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start") (includes frontend "react-vite"))}} +import { initReactI18next } from "react-i18next"; +{{/if}} + +i18n +{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "tanstack-start") (includes frontend "react-vite"))}} + .use(initReactI18next) +{{/if}} + .use(LanguageDetector) + .use(HttpBackend) + .init({ + fallbackLng: "en", + supportedLngs: ["en", "fr"], + debug: process.env.NODE_ENV === "development", + interpolation: { + escapeValue: false, + }, + backend: { + loadPath: "/locales/{{lng}}/{{ns}}.json", + }, + }); + +export default i18n; diff --git a/packages/template-generator/templates/i18n/next-intl/web/base/messages/en.json b/packages/template-generator/templates/i18n/next-intl/web/base/messages/en.json new file mode 100644 index 000000000..bce62bb75 --- /dev/null +++ b/packages/template-generator/templates/i18n/next-intl/web/base/messages/en.json @@ -0,0 +1,7 @@ +{ + "common": { + "welcome": "Welcome to {projectName}", + "description": "Get started by editing the source code", + "language": "Language" + } +} diff --git a/packages/template-generator/templates/i18n/next-intl/web/base/messages/fr.json b/packages/template-generator/templates/i18n/next-intl/web/base/messages/fr.json new file mode 100644 index 000000000..41dc277f1 --- /dev/null +++ b/packages/template-generator/templates/i18n/next-intl/web/base/messages/fr.json @@ -0,0 +1,7 @@ +{ + "common": { + "welcome": "Bienvenue sur {projectName}", + "description": "Commencez par modifier le code source", + "language": "Langue" + } +} diff --git a/packages/template-generator/templates/i18n/next-intl/web/next/src/i18n.ts.hbs b/packages/template-generator/templates/i18n/next-intl/web/next/src/i18n.ts.hbs new file mode 100644 index 000000000..c4b7ca26c --- /dev/null +++ b/packages/template-generator/templates/i18n/next-intl/web/next/src/i18n.ts.hbs @@ -0,0 +1,5 @@ +import { getRequestConfig } from "next-intl/server"; + +export default getRequestConfig(async ({ locale }) => ({ + messages: (await import(`../../messages/${locale}.json`)).default, +})); diff --git a/packages/template-generator/templates/i18n/next-intl/web/next/src/middleware.ts.hbs b/packages/template-generator/templates/i18n/next-intl/web/next/src/middleware.ts.hbs new file mode 100644 index 000000000..368f1c5de --- /dev/null +++ b/packages/template-generator/templates/i18n/next-intl/web/next/src/middleware.ts.hbs @@ -0,0 +1,10 @@ +import createMiddleware from "next-intl/middleware"; + +export default createMiddleware({ + locales: ["en", "fr"], + defaultLocale: "en", +}); + +export const config = { + matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"], +}; diff --git a/packages/types/src/compatibility.ts b/packages/types/src/compatibility.ts index 6e97ca13b..c6111c644 100644 --- a/packages/types/src/compatibility.ts +++ b/packages/types/src/compatibility.ts @@ -43,6 +43,7 @@ export type CompatibilityCategory = | "caching" | "search" | "fileStorage" + | "i18n" | "animation" | "cssFramework" | "uiLibrary" @@ -71,6 +72,7 @@ export type CompatibilityCategory = | "pythonValidation" | "pythonAi" | "pythonAuth" + | "pythonGraphql" | "pythonTaskQueue" | "pythonQuality" | "goWebFramework" @@ -128,6 +130,7 @@ export type CompatibilityInput = { cms: string; search: string; fileStorage: string; + i18n: string; codeQuality: string[]; documentation: string[]; appPlatforms: string[]; @@ -150,11 +153,13 @@ export type CompatibilityInput = { rustLibraries: string; rustLogging: string; rustErrorHandling: string; + rustCaching: string; pythonWebFramework: string; pythonOrm: string; pythonValidation: string; pythonAi: string; pythonAuth: string; + pythonGraphql: string; pythonTaskQueue: string; pythonQuality: string; goWebFramework: string; @@ -223,6 +228,7 @@ const CATEGORY_ORDER: CompatibilityCategory[] = [ "pythonValidation", "pythonAi", "pythonAuth", + "pythonGraphql", "pythonTaskQueue", "pythonQuality", "goWebFramework", @@ -302,6 +308,7 @@ export const getCategoryDisplayName = (categoryKey: string): string => { rustLibraries: "Rust Core Libraries", rustLogging: "Rust Logging", rustErrorHandling: "Rust Error Handling", + rustCaching: "Rust Caching", }; // Custom display names for Python categories @@ -311,6 +318,7 @@ export const getCategoryDisplayName = (categoryKey: string): string => { pythonValidation: "Python Validation", pythonAi: "Python AI / ML", pythonAuth: "Python Auth", + pythonGraphql: "Python GraphQL", pythonTaskQueue: "Python Task Queue", pythonQuality: "Python Code Quality", }; @@ -324,6 +332,10 @@ export const getCategoryDisplayName = (categoryKey: string): string => { goLogging: "Go Logging", }; + if (categoryKey === "i18n") { + return "Internationalization (i18n)"; + } + if (rustCategoryNames[categoryKey]) { return rustCategoryNames[categoryKey]; } @@ -1936,6 +1948,15 @@ export const getDisabledReason = ( } } + // ============================================ + // I18N CONSTRAINTS + // ============================================ + if (category === "i18n") { + if (optionId === "next-intl" && !currentStack.webFrontend.includes("next")) { + return "next-intl requires a Next.js frontend"; + } + } + return null; }; diff --git a/packages/types/src/option-metadata.ts b/packages/types/src/option-metadata.ts index c915bae82..d20186cc2 100644 --- a/packages/types/src/option-metadata.ts +++ b/packages/types/src/option-metadata.ts @@ -18,6 +18,7 @@ import { FILE_STORAGE_VALUES, FILE_UPLOAD_VALUES, FORMS_VALUES, + I18N_VALUES, GO_API_VALUES, GO_CLI_VALUES, GO_LOGGING_VALUES, @@ -32,6 +33,7 @@ import { VERSION_CHANNEL_VALUES, PYTHON_AI_VALUES, PYTHON_AUTH_VALUES, + PYTHON_GRAPHQL_VALUES, PYTHON_ORM_VALUES, PYTHON_QUALITY_VALUES, PYTHON_TASK_QUEUE_VALUES, @@ -45,6 +47,7 @@ import { RUST_LIBRARIES_VALUES, RUST_LOGGING_VALUES, RUST_ERROR_HANDLING_VALUES, + RUST_CACHING_VALUES, RUST_ORM_VALUES, RUST_WEB_FRAMEWORK_VALUES, SEARCH_VALUES, @@ -91,6 +94,7 @@ export type OptionCategory = | "caching" | "search" | "fileStorage" + | "i18n" | "animation" | "cssFramework" | "uiLibrary" @@ -123,11 +127,13 @@ export type OptionCategory = | "rustLibraries" | "rustLogging" | "rustErrorHandling" + | "rustCaching" | "pythonWebFramework" | "pythonOrm" | "pythonValidation" | "pythonAi" | "pythonAuth" + | "pythonGraphql" | "pythonTaskQueue" | "pythonQuality" | "goWebFramework" @@ -264,6 +270,7 @@ const CATEGORY_VALUE_IDS: Record = { caching: CACHING_VALUES, search: SEARCH_VALUES, fileStorage: FILE_STORAGE_VALUES, + i18n: I18N_VALUES, animation: ANIMATION_VALUES, cssFramework: CSS_FRAMEWORK_VALUES, uiLibrary: UI_LIBRARY_VALUES, @@ -296,11 +303,13 @@ const CATEGORY_VALUE_IDS: Record = { rustLibraries: RUST_LIBRARIES_VALUES, rustLogging: RUST_LOGGING_VALUES, rustErrorHandling: RUST_ERROR_HANDLING_VALUES, + rustCaching: RUST_CACHING_VALUES, pythonWebFramework: PYTHON_WEB_FRAMEWORK_VALUES, pythonOrm: PYTHON_ORM_VALUES, pythonValidation: PYTHON_VALIDATION_VALUES, pythonAi: PYTHON_AI_VALUES, pythonAuth: PYTHON_AUTH_VALUES, + pythonGraphql: PYTHON_GRAPHQL_VALUES, pythonTaskQueue: PYTHON_TASK_QUEUE_VALUES, pythonQuality: PYTHON_QUALITY_VALUES, goWebFramework: GO_WEB_FRAMEWORK_VALUES, @@ -405,6 +414,10 @@ const EXACT_LABEL_OVERRIDES: Partial; export type Caching = z.infer; export type Search = z.infer; export type FileStorage = z.infer; +export type I18n = z.infer; export type Ecosystem = z.infer; export type RustWebFramework = z.infer; export type RustFrontend = z.infer; @@ -128,11 +132,13 @@ export type RustCli = z.infer; export type RustLibraries = z.infer; export type RustLogging = z.infer; export type RustErrorHandling = z.infer; +export type RustCaching = z.infer; export type PythonWebFramework = z.infer; export type PythonOrm = z.infer; export type PythonValidation = z.infer; export type PythonAi = z.infer; export type PythonAuth = z.infer; +export type PythonGraphql = z.infer; export type PythonTaskQueue = z.infer; export type PythonQuality = z.infer; export type GoWebFramework = z.infer; diff --git a/testing/lib/generate-combos/options.ts b/testing/lib/generate-combos/options.ts index d2700fb85..e91a4163c 100644 --- a/testing/lib/generate-combos/options.ts +++ b/testing/lib/generate-combos/options.ts @@ -24,6 +24,7 @@ import { FILE_STORAGE_VALUES, FILE_UPLOAD_VALUES, FORMS_VALUES, + I18N_VALUES, GO_API_VALUES, GO_CLI_VALUES, GO_LOGGING_VALUES, @@ -36,6 +37,7 @@ import { PAYMENTS_VALUES, PYTHON_AI_VALUES, PYTHON_AUTH_VALUES, + PYTHON_GRAPHQL_VALUES, PYTHON_ORM_VALUES, PYTHON_QUALITY_VALUES, PYTHON_TASK_QUEUE_VALUES, @@ -49,6 +51,7 @@ import { RUST_LIBRARIES_VALUES, RUST_LOGGING_VALUES, RUST_ERROR_HANDLING_VALUES, + RUST_CACHING_VALUES, RUST_ORM_VALUES, RUST_WEB_FRAMEWORK_VALUES, SEARCH_VALUES, @@ -266,6 +269,7 @@ function makeTypeScriptDraft(args: GeneratorArgs): CandidateDraft { caching: sampleScalar(CACHING_VALUES, 0.88), search: sampleScalar(SEARCH_VALUES, 0.9), fileStorage: sampleScalar(FILE_STORAGE_VALUES, 0.84), + i18n: sampleScalar(I18N_VALUES, 0.88), webDeploy: sampleScalar(WEB_DEPLOY_VALUES, 0.92), serverDeploy: backend === "hono" ? sampleScalar(SERVER_DEPLOY_VALUES, 0.92) @@ -298,6 +302,7 @@ function makeRustDraft(args: GeneratorArgs): CandidateDraft { rustCli: sampleScalar(RUST_CLI_VALUES, 0.3), rustLogging: sampleScalar(RUST_LOGGING_VALUES, 0.15), rustErrorHandling: sampleScalar(RUST_ERROR_HANDLING_VALUES, 0.15), + rustCaching: sampleScalar(RUST_CACHING_VALUES, 0.15), rustLibraries: sampleArray(RUST_LIBRARIES_VALUES, 0.35, 2), }, }; @@ -313,6 +318,7 @@ function makePythonDraft(args: GeneratorArgs): CandidateDraft { pythonValidation: sampleScalar(PYTHON_VALIDATION_VALUES, 0.35), pythonAi: sampleArray(PYTHON_AI_VALUES, 0.5, 1), pythonAuth: sampleScalar(PYTHON_AUTH_VALUES, 0.5), + pythonGraphql: sampleScalar(PYTHON_GRAPHQL_VALUES, 0.5), pythonTaskQueue: sampleScalar(PYTHON_TASK_QUEUE_VALUES, 0.55), pythonQuality: sampleScalar(PYTHON_QUALITY_VALUES, 0.35), }, @@ -381,6 +387,7 @@ function createValidationBase(projectName: string, draft: CandidateDraft): Proje caching: "none", search: "none", fileStorage: "none", + i18n: "none", animation: "none", logging: "none", observability: "none", @@ -410,12 +417,14 @@ function createValidationBase(projectName: string, draft: CandidateDraft): Proje rustCli: "none", rustLogging: "tracing", rustErrorHandling: "anyhow-thiserror", + rustCaching: "none", rustLibraries: [], pythonWebFramework: "none", pythonOrm: "none", pythonValidation: "none", pythonAi: [], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "none", goWebFramework: "none", diff --git a/testing/lib/presets.ts b/testing/lib/presets.ts index 4bd94cabe..764a5eba5 100644 --- a/testing/lib/presets.ts +++ b/testing/lib/presets.ts @@ -41,6 +41,7 @@ export function makeBaseConfig(name: string, ecosystem: Ecosystem): ProjectConfi caching: "none", search: "none", fileStorage: "none", + i18n: "none", webDeploy: "none", serverDeploy: "none", addons: [], @@ -56,12 +57,14 @@ export function makeBaseConfig(name: string, ecosystem: Ecosystem): ProjectConfi rustCli: "none", rustLogging: "tracing", rustErrorHandling: "anyhow-thiserror", + rustCaching: "none", rustLibraries: [], pythonWebFramework: "none", pythonOrm: "none", pythonValidation: "none", pythonAi: [], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "none", goWebFramework: "none", @@ -327,6 +330,7 @@ const SMOKE_TEST_PRESETS: Record = { pythonValidation: "pydantic", pythonAi: [], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "celery", pythonQuality: "ruff", }, @@ -339,6 +343,7 @@ const SMOKE_TEST_PRESETS: Record = { pythonValidation: "pydantic", pythonAi: ["langchain"], pythonAuth: "none", + pythonGraphql: "none", pythonTaskQueue: "none", pythonQuality: "ruff", }, From cf8f145214cb1809c1da4cd45e76ac25f2731e91 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Fri, 10 Apr 2026 00:24:24 +0300 Subject: [PATCH 2/3] feat: add internationalization (i18n) category with i18next and next-intl Add i18n as a new TypeScript category supporting two options: - i18next: Universal i18n framework for all frontends (22M+ weekly downloads) - next-intl: Next.js-specific i18n with App Router support Includes full checklist implementation: schema, types, compatibility rules, CLI prompts, web builder, template handler, dep processor, templates, and test/smoke wiring across ~37 files. Compatibility: next-intl is disabled when frontend is not Next.js. --- .../cli/src/helpers/core/post-installation.ts | 9 ++ apps/cli/src/index.ts | 3 +- apps/cli/src/prompts/config-prompts.ts | 2 +- apps/cli/src/prompts/i18n.ts | 35 +------ apps/cli/src/prompts/python-ecosystem.ts | 28 ++++++ apps/cli/src/prompts/rust-ecosystem.ts | 33 +++++++ apps/cli/src/utils/config-processing.ts | 3 +- apps/cli/test/add-history-commands.test.ts | 3 + .../generate-reproducible-command.test.ts | 2 - apps/cli/test/rust-ecosystem.test.ts | 95 +++++++++++++++++++ apps/web/src/lib/constant.ts | 27 ++++++ apps/web/src/lib/preview-config.ts | 1 + apps/web/src/lib/stack-defaults.ts | 2 + apps/web/src/lib/stack-url-keys.ts | 1 + apps/web/src/lib/stack-url-state.ts | 3 + apps/web/src/lib/stack-utils.ts | 2 + apps/web/src/lib/tech-icons.ts | 4 +- apps/web/src/lib/tech-resource-links.ts | 2 + .../src/processors/i18n-deps.ts | 34 +------ .../src/processors/readme-generator.ts | 8 ++ .../src/template-handlers/i18n.ts | 48 +--------- .../template-generator/src/utils/add-deps.ts | 2 + .../base/public/locales/en/translation.json | 6 +- .../base/public/locales/fr/translation.json | 6 +- .../i18next/web/base/src/i18n/config.ts.hbs | 8 +- .../i18n/next-intl/web/base/messages/en.json | 8 +- .../i18n/next-intl/web/base/messages/fr.json | 8 +- .../i18n/next-intl/web/next/src/i18n.ts.hbs | 3 +- .../next-intl/web/next/src/middleware.ts.hbs | 11 +-- packages/types/src/compatibility.ts | 11 +-- packages/types/src/option-metadata.ts | 3 +- testing/lib/generate-combos/options.ts | 2 +- 32 files changed, 252 insertions(+), 161 deletions(-) diff --git a/apps/cli/src/helpers/core/post-installation.ts b/apps/cli/src/helpers/core/post-installation.ts index 671ef6dbd..c7c9171c5 100644 --- a/apps/cli/src/helpers/core/post-installation.ts +++ b/apps/cli/src/helpers/core/post-installation.ts @@ -742,6 +742,15 @@ function displayRustInstructions(config: ProjectConfig & { depsInstalled: boolea output += `${pc.cyan("•")} Error Handling: ${errorHandlingNames[rustErrorHandling] || rustErrorHandling}\n`; } + const { rustCaching } = config; + if (rustCaching && rustCaching !== "none") { + const cachingNames: Record = { + moka: "Moka", + redis: "Redis", + }; + output += `${pc.cyan("•")} Caching: ${cachingNames[rustCaching] || rustCaching}\n`; + } + output += `\n${pc.bold("Common Cargo commands:")}\n`; output += `${pc.cyan("•")} Build: cargo build\n`; output += `${pc.cyan("•")} Run: cargo run\n`; diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 6710a4788..be4b926c8 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -80,8 +80,9 @@ import { CachingSchema, type Caching, SearchSchema, - FileStorageSchema, I18nSchema, + FileStorageSchema, + type I18n, RustWebFrameworkSchema, type RustWebFramework, RustFrontendSchema, diff --git a/apps/cli/src/prompts/config-prompts.ts b/apps/cli/src/prompts/config-prompts.ts index 52af38d99..76383cd2f 100644 --- a/apps/cli/src/prompts/config-prompts.ts +++ b/apps/cli/src/prompts/config-prompts.ts @@ -19,9 +19,9 @@ import type { Examples, FeatureFlags, FileUpload, + I18n, Forms, Frontend, - I18n, GoApi, GoCli, GoLogging, diff --git a/apps/cli/src/prompts/i18n.ts b/apps/cli/src/prompts/i18n.ts index 3401fc7c1..bf81d8370 100644 --- a/apps/cli/src/prompts/i18n.ts +++ b/apps/cli/src/prompts/i18n.ts @@ -1,42 +1,15 @@ import type { Frontend, I18n } from "../types"; - import { exitCancelled } from "../utils/errors"; import { isCancel, navigableSelect } from "./navigable"; - export async function getI18nChoice(i18n?: I18n, frontends?: Frontend[]) { if (i18n !== undefined) return i18n; - const hasNext = frontends?.includes("next"); - const options = [ - { - value: "i18next" as const, - label: "i18next", - hint: "Universal i18n framework for any frontend (22M+ weekly downloads)", - }, - ...(hasNext - ? [ - { - value: "next-intl" as const, - label: "next-intl", - hint: "Next.js-specific i18n with App Router support", - }, - ] - : []), - { - value: "none" as const, - label: "None", - hint: "Skip internationalization setup", - }, + { value: "i18next" as const, label: "i18next", hint: "Universal i18n framework for any frontend (22M+ weekly downloads)" }, + ...(hasNext ? [{ value: "next-intl" as const, label: "next-intl", hint: "Next.js-specific i18n with App Router support" }] : []), + { value: "none" as const, label: "None", hint: "Skip internationalization setup" }, ]; - - const response = await navigableSelect({ - message: "Select internationalization (i18n) library", - options, - initialValue: "none", - }); - + const response = await navigableSelect({ message: "Select internationalization (i18n) library", options, initialValue: "none" }); if (isCancel(response)) return exitCancelled("Operation cancelled"); - return response; } diff --git a/apps/cli/src/prompts/python-ecosystem.ts b/apps/cli/src/prompts/python-ecosystem.ts index c7e50bdb8..18d66e646 100644 --- a/apps/cli/src/prompts/python-ecosystem.ts +++ b/apps/cli/src/prompts/python-ecosystem.ts @@ -1,6 +1,7 @@ import type { PythonAi, PythonAuth, + PythonGraphql, PythonOrm, PythonQuality, PythonTaskQueue, @@ -199,6 +200,33 @@ export async function getPythonAuthChoice(pythonAuth?: PythonAuth) { return response; } +export async function getPythonGraphqlChoice(pythonGraphql?: PythonGraphql) { + if (pythonGraphql !== undefined) return pythonGraphql; + + const options = [ + { + value: "strawberry" as const, + label: "Strawberry GraphQL", + hint: "Code-first GraphQL with Python type hints, async support", + }, + { + value: "none" as const, + label: "None", + hint: "No GraphQL library", + }, + ]; + + const response = await navigableSelect({ + message: "Select Python GraphQL library", + options, + initialValue: "none", + }); + + if (isCancel(response)) return exitCancelled("Operation cancelled"); + + return response; +} + export async function getPythonTaskQueueChoice(pythonTaskQueue?: PythonTaskQueue) { if (pythonTaskQueue !== undefined) return pythonTaskQueue; diff --git a/apps/cli/src/prompts/rust-ecosystem.ts b/apps/cli/src/prompts/rust-ecosystem.ts index 2c24515a2..2904577d2 100644 --- a/apps/cli/src/prompts/rust-ecosystem.ts +++ b/apps/cli/src/prompts/rust-ecosystem.ts @@ -2,6 +2,7 @@ import type { RustApi, RustCli, RustErrorHandling, + RustCaching, RustFrontend, RustLibraries, RustLogging, @@ -288,3 +289,35 @@ export async function getRustErrorHandlingChoice(rustErrorHandling?: RustErrorHa return response; } + +export async function getRustCachingChoice(rustCaching?: RustCaching) { + if (rustCaching !== undefined) return rustCaching; + + const options = [ + { + value: "moka" as const, + label: "Moka", + hint: "High-performance concurrent in-memory cache (Caffeine-inspired)", + }, + { + value: "redis" as const, + label: "Redis", + hint: "Redis client with async support and connection pooling", + }, + { + value: "none" as const, + label: "None", + hint: "No caching library", + }, + ]; + + const response = await navigableSelect({ + message: "Select Rust caching library", + options, + initialValue: "none", + }); + + if (isCancel(response)) return exitCancelled("Operation cancelled"); + + return response; +} diff --git a/apps/cli/src/utils/config-processing.ts b/apps/cli/src/utils/config-processing.ts index 71fa4216f..7c30ba3cf 100644 --- a/apps/cli/src/utils/config-processing.ts +++ b/apps/cli/src/utils/config-processing.ts @@ -20,9 +20,9 @@ import type { Email, FeatureFlags, FileStorage, + I18n, FileUpload, Forms, - I18n, GoCli, GoLogging, GoOrm, @@ -182,6 +182,7 @@ export function processFlags(options: CLIInput, projectName?: string) { config.fileStorage = options.fileStorage as FileStorage; } + if (options.i18n !== undefined) { config.i18n = options.i18n as I18n; } diff --git a/apps/cli/test/add-history-commands.test.ts b/apps/cli/test/add-history-commands.test.ts index 6bc363c63..93e2b7904 100644 --- a/apps/cli/test/add-history-commands.test.ts +++ b/apps/cli/test/add-history-commands.test.ts @@ -201,6 +201,7 @@ describe("CLI history command", () => { "--python-validation pydantic " + "--python-ai none " + "--python-auth none " + + "--python-graphql none " + "--python-task-queue celery " + "--python-quality ruff " + "--addons none " + @@ -229,6 +230,8 @@ describe("CLI history command", () => { "none", "--python-auth", "none", + "--python-graphql", + "none", "--python-task-queue", "celery", "--python-quality", diff --git a/apps/cli/test/generate-reproducible-command.test.ts b/apps/cli/test/generate-reproducible-command.test.ts index 525726f7d..faaec9db9 100644 --- a/apps/cli/test/generate-reproducible-command.test.ts +++ b/apps/cli/test/generate-reproducible-command.test.ts @@ -117,7 +117,6 @@ describe("generateReproducibleCommand", () => { caching: "none", search: "none", fileStorage: "none", - i18n: "none", pythonWebFramework: "django", pythonOrm: "sqlalchemy", pythonValidation: "pydantic", @@ -185,7 +184,6 @@ describe("generateReproducibleCommand", () => { caching: "none", search: "none", fileStorage: "none", - i18n: "none", pythonWebFramework: "fastapi", pythonOrm: "sqlmodel", pythonValidation: "pydantic", diff --git a/apps/cli/test/rust-ecosystem.test.ts b/apps/cli/test/rust-ecosystem.test.ts index aa45c5088..a6cede443 100644 --- a/apps/cli/test/rust-ecosystem.test.ts +++ b/apps/cli/test/rust-ecosystem.test.ts @@ -2631,4 +2631,99 @@ describe("Rust Ecosystem", () => { expect(cargoContent).toContain('thiserror = "2.0"'); }); }); + + describe("Rust Caching Option", () => { + it("should include moka deps when rustCaching is moka", async () => { + const result = await createVirtual({ + projectName: "rust-moka", + ecosystem: "rust", + rustWebFramework: "axum", + rustFrontend: "none", + rustOrm: "none", + rustApi: "none", + rustCli: "none", + rustLibraries: [], + rustLogging: "tracing", + rustErrorHandling: "anyhow-thiserror", + rustCaching: "moka", + }); + + expect(result.success).toBe(true); + const root = result.tree!.root; + + const cargoContent = getFileContent(root, "Cargo.toml"); + expect(cargoContent).toContain("moka"); + + const serverCargoContent = getFileContent(root, "crates/server/Cargo.toml"); + expect(serverCargoContent).toContain("moka.workspace"); + + expect(hasFile(root, "crates/server/src/cache.rs")).toBe(true); + const cacheContent = getFileContent(root, "crates/server/src/cache.rs"); + expect(cacheContent).toContain("moka::future::Cache"); + expect(cacheContent).toContain("create_cache"); + + const mainContent = getFileContent(root, "crates/server/src/main.rs"); + expect(mainContent).toContain("mod cache;"); + }); + + it("should include redis deps when rustCaching is redis", async () => { + const result = await createVirtual({ + projectName: "rust-redis", + ecosystem: "rust", + rustWebFramework: "actix-web", + rustFrontend: "none", + rustOrm: "none", + rustApi: "none", + rustCli: "none", + rustLibraries: [], + rustLogging: "tracing", + rustErrorHandling: "anyhow-thiserror", + rustCaching: "redis", + }); + + expect(result.success).toBe(true); + const root = result.tree!.root; + + const cargoContent = getFileContent(root, "Cargo.toml"); + expect(cargoContent).toContain("redis"); + + const serverCargoContent = getFileContent(root, "crates/server/Cargo.toml"); + expect(serverCargoContent).toContain("redis.workspace"); + + expect(hasFile(root, "crates/server/src/cache.rs")).toBe(true); + const cacheContent = getFileContent(root, "crates/server/src/cache.rs"); + expect(cacheContent).toContain("redis::Client"); + expect(cacheContent).toContain("create_redis_client"); + + const mainContent = getFileContent(root, "crates/server/src/main.rs"); + expect(mainContent).toContain("mod cache;"); + }); + + it("should not include caching deps when rustCaching is none", async () => { + const result = await createVirtual({ + projectName: "rust-nocache", + ecosystem: "rust", + rustWebFramework: "axum", + rustFrontend: "none", + rustOrm: "none", + rustApi: "none", + rustCli: "none", + rustLibraries: [], + rustLogging: "tracing", + rustErrorHandling: "anyhow-thiserror", + rustCaching: "none", + }); + + expect(result.success).toBe(true); + const root = result.tree!.root; + + const cargoContent = getFileContent(root, "Cargo.toml"); + expect(cargoContent).not.toContain("moka"); + // Note: "redis" might appear in comments, check for the dependency line specifically + expect(cargoContent).not.toContain('redis = {'); + + const mainContent = getFileContent(root, "crates/server/src/main.rs"); + expect(mainContent).not.toContain("mod cache;"); + }); + }); }); diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 296882732..1263242d3 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -2322,6 +2322,32 @@ export const TECH_OPTIONS: Record< default: true, }, ], + i18n: [ + { + id: "i18next", + name: "i18next", + description: "Universal i18n framework with plugins for any frontend (22M+ weekly downloads)", + icon: "https://cdn.simpleicons.org/i18next/26A69A", + color: "from-teal-500 to-teal-700", + default: false, + }, + { + id: "next-intl", + name: "next-intl", + description: "Next.js-specific i18n with App Router support, type-safe messages", + icon: "https://cdn.simpleicons.org/nextdotjs/000000", + color: "from-gray-700 to-gray-900", + default: false, + }, + { + id: "none", + name: "No i18n", + description: "Skip internationalization setup", + icon: "", + color: "from-gray-400 to-gray-600", + default: true, + }, + ], animation: [ { id: "framer-motion", @@ -3242,6 +3268,7 @@ export const ECOSYSTEM_CATEGORIES: Record = { "logging", "observability", "featureFlags", + "i18n", "caching", "ai", "cms", diff --git a/apps/web/src/lib/preview-config.ts b/apps/web/src/lib/preview-config.ts index 162416f3e..33d98b113 100644 --- a/apps/web/src/lib/preview-config.ts +++ b/apps/web/src/lib/preview-config.ts @@ -97,6 +97,7 @@ export function stackStateToProjectConfig(input: Partial): ProjectCo caching: stack.caching as ProjectConfig["caching"], search: stack.search as ProjectConfig["search"], fileStorage: stack.fileStorage as ProjectConfig["fileStorage"], + i18n: stack.i18n as ProjectConfig["i18n"], rustWebFramework: stack.rustWebFramework as ProjectConfig["rustWebFramework"], rustFrontend: stack.rustFrontend as ProjectConfig["rustFrontend"], rustOrm: stack.rustOrm as ProjectConfig["rustOrm"], diff --git a/apps/web/src/lib/stack-defaults.ts b/apps/web/src/lib/stack-defaults.ts index 606513b49..6256e6bc7 100644 --- a/apps/web/src/lib/stack-defaults.ts +++ b/apps/web/src/lib/stack-defaults.ts @@ -39,6 +39,7 @@ export type StackState = { shadcnRadius: string; cms: string; search: string; + i18n: string; fileStorage: string; codeQuality: string[]; documentation: string[]; @@ -118,6 +119,7 @@ export const DEFAULT_STACK: StackState = { cms: "none", search: "none", fileStorage: "none", + i18n: "none", codeQuality: [], documentation: [], appPlatforms: ["turborepo"], diff --git a/apps/web/src/lib/stack-url-keys.ts b/apps/web/src/lib/stack-url-keys.ts index 21648001c..280e5c48d 100644 --- a/apps/web/src/lib/stack-url-keys.ts +++ b/apps/web/src/lib/stack-url-keys.ts @@ -42,6 +42,7 @@ export const stackUrlKeys = { cms: "cms", search: "srch", fileStorage: "fs", + i18n: "i18n", codeQuality: "cq", documentation: "doc", appPlatforms: "ap", diff --git a/apps/web/src/lib/stack-url-state.ts b/apps/web/src/lib/stack-url-state.ts index ff675f74a..a0c15e990 100644 --- a/apps/web/src/lib/stack-url-state.ts +++ b/apps/web/src/lib/stack-url-state.ts @@ -78,6 +78,7 @@ export function loadStackParams( cms: getString("cms", DEFAULT_STACK.cms), search: getString("search", DEFAULT_STACK.search), fileStorage: getString("fileStorage", DEFAULT_STACK.fileStorage), + i18n: getString("i18n", DEFAULT_STACK.i18n), codeQuality: getArray("codeQuality", DEFAULT_STACK.codeQuality), documentation: getArray("documentation", DEFAULT_STACK.documentation), appPlatforms: getArray("appPlatforms", DEFAULT_STACK.appPlatforms), @@ -180,6 +181,7 @@ export function serializeStackParams(basePath: string, stack: StackState): strin addParam("cms", stack.cms); addParam("search", stack.search); addParam("fileStorage", stack.fileStorage); + addParam("i18n", stack.i18n); addParam("codeQuality", stack.codeQuality); addParam("documentation", stack.documentation); addParam("appPlatforms", stack.appPlatforms); @@ -266,6 +268,7 @@ function searchToStack(search: StackSearchParams | undefined): StackState { cms: search.cms ?? DEFAULT_STACK.cms, search: search.srch ?? DEFAULT_STACK.search, fileStorage: search.fs ?? DEFAULT_STACK.fileStorage, + i18n: search.i18n ?? DEFAULT_STACK.i18n, codeQuality: search.cq ?? DEFAULT_STACK.codeQuality, documentation: search.doc ?? DEFAULT_STACK.documentation, appPlatforms: search.ap ?? DEFAULT_STACK.appPlatforms, diff --git a/apps/web/src/lib/stack-utils.ts b/apps/web/src/lib/stack-utils.ts index ff8598553..4f9a070c8 100644 --- a/apps/web/src/lib/stack-utils.ts +++ b/apps/web/src/lib/stack-utils.ts @@ -50,6 +50,7 @@ const TYPESCRIPT_CATEGORY_ORDER: Array = [ "caching", "search", "fileStorage", + "i18n", "animation", "cms", "codeQuality", @@ -231,6 +232,7 @@ export function generateStackCommand(stack: StackState) { `--caching ${stack.caching}`, `--search ${stack.search}`, `--file-storage ${stack.fileStorage}`, + `--i18n ${stack.i18n}`, `--cms ${stack.cms}`, `--effect ${stack.backendLibraries}`, `--ai ${stack.aiSdk}`, diff --git a/apps/web/src/lib/tech-icons.ts b/apps/web/src/lib/tech-icons.ts index b2ea59011..ec5e61e51 100644 --- a/apps/web/src/lib/tech-icons.ts +++ b/apps/web/src/lib/tech-icons.ts @@ -290,10 +290,12 @@ export const ICON_REGISTRY: Record = { s3: { type: "local", src: "/icon/aws-s3.svg" }, r2: { type: "si", slug: "cloudflare", hex: "F38020" }, - // ─── i18n ────────────────────────────────────────────────────────────────── + // i18n i18next: { type: "si", slug: "i18next", hex: "26A69A" }, "next-intl": { type: "si", slug: "nextdotjs", hex: "000000" }, + // ─── i18n ────────────────────────────────────────────────────────────────── + // ─── Animation ───────────────────────────────────────────────────────────── "framer-motion": { type: "si", slug: "framer", hex: "0055FF" }, gsap: { type: "si", slug: "greensock", hex: "88CE02" }, diff --git a/apps/web/src/lib/tech-resource-links.ts b/apps/web/src/lib/tech-resource-links.ts index 60126174e..797d158f4 100644 --- a/apps/web/src/lib/tech-resource-links.ts +++ b/apps/web/src/lib/tech-resource-links.ts @@ -585,6 +585,8 @@ const BASE_LINKS: LinkMap = { docsUrl: "https://next-intl-docs.vercel.app/", githubUrl: "https://github.com/amannn/next-intl", }, + }, + }, s3: { docsUrl: "https://docs.aws.amazon.com/AmazonS3/", githubUrl: "https://github.com/aws/aws-sdk-js-v3", diff --git a/packages/template-generator/src/processors/i18n-deps.ts b/packages/template-generator/src/processors/i18n-deps.ts index 682d9a110..24c989229 100644 --- a/packages/template-generator/src/processors/i18n-deps.ts +++ b/packages/template-generator/src/processors/i18n-deps.ts @@ -1,48 +1,24 @@ import type { ProjectConfig } from "@better-fullstack/types"; - import type { VirtualFileSystem } from "../core/virtual-fs"; - import { addPackageDependency, type AvailableDependencies } from "../utils/add-deps"; - export function processI18nDeps(vfs: VirtualFileSystem, config: ProjectConfig): void { const { i18n, frontend } = config; - if (!i18n || i18n === "none") return; - const webPath = "apps/web/package.json"; if (!vfs.exists(webPath)) return; - const deps = getI18nDeps(i18n, frontend); - if (deps.length > 0) { - addPackageDependency({ - vfs, - packagePath: webPath, - dependencies: deps, - }); - } + if (deps.length > 0) { addPackageDependency({ vfs, packagePath: webPath, dependencies: deps }); } } - -function getI18nDeps( - i18n: ProjectConfig["i18n"], - frontend: ProjectConfig["frontend"], -): AvailableDependencies[] { +function getI18nDeps(i18n: ProjectConfig["i18n"], frontend: ProjectConfig["frontend"]): AvailableDependencies[] { const deps: AvailableDependencies[] = []; - switch (i18n) { case "i18next": { deps.push("i18next", "i18next-browser-languagedetector", "i18next-http-backend"); - const hasReact = frontend.some((f) => - ["next", "tanstack-router", "react-router", "tanstack-start", "react-vite"].includes(f), - ); - if (hasReact) { - deps.push("react-i18next"); - } + const hasReact = frontend.some((f) => ["next", "tanstack-router", "react-router", "tanstack-start", "react-vite"].includes(f)); + if (hasReact) { deps.push("react-i18next"); } break; } - case "next-intl": - deps.push("next-intl"); - break; + case "next-intl": deps.push("next-intl"); break; } - return deps; } diff --git a/packages/template-generator/src/processors/readme-generator.ts b/packages/template-generator/src/processors/readme-generator.ts index d66bd1dc8..45b4e363e 100644 --- a/packages/template-generator/src/processors/readme-generator.ts +++ b/packages/template-generator/src/processors/readme-generator.ts @@ -820,6 +820,14 @@ function generateRustReadmeContent(config: ProjectConfig): string { features.push("- **eyre + color-eyre** - Customizable error reports with pretty backtraces"); } + // Caching + const { rustCaching } = config; + if (rustCaching === "moka") { + features.push("- **Moka** - High-performance concurrent in-memory cache (Caffeine-inspired)"); + } else if (rustCaching === "redis") { + features.push("- **Redis** - Redis client with async support and connection pooling"); + } + // Project structure const structure: string[] = [`${projectName}/`, "├── Cargo.toml # Workspace manifest"]; diff --git a/packages/template-generator/src/template-handlers/i18n.ts b/packages/template-generator/src/template-handlers/i18n.ts index 6bce8dd28..39d7ac87e 100644 --- a/packages/template-generator/src/template-handlers/i18n.ts +++ b/packages/template-generator/src/template-handlers/i18n.ts @@ -1,51 +1,13 @@ import type { ProjectConfig } from "@better-fullstack/types"; - import type { VirtualFileSystem } from "../core/virtual-fs"; - import { type TemplateData, processTemplatesFromPrefix } from "./utils"; - -export async function processI18nTemplates( - vfs: VirtualFileSystem, - templates: TemplateData, - config: ProjectConfig, -): Promise { +export async function processI18nTemplates(vfs: VirtualFileSystem, templates: TemplateData, config: ProjectConfig): Promise { if (!config.i18n || config.i18n === "none") return; - - // i18n is frontend-only, works with any frontend const hasWebFrontend = config.frontend.some((f) => f !== "none"); if (!hasWebFrontend) return; - - // Process web-side i18n templates - processTemplatesFromPrefix( - vfs, - templates, - `i18n/${config.i18n}/web/base`, - "apps/web", - config, - ); - - // Process framework-specific templates if they exist + processTemplatesFromPrefix(vfs, templates, "i18n/" + config.i18n + "/web/base", "apps/web", config); const hasNext = config.frontend.includes("next"); - if (hasNext) { - processTemplatesFromPrefix( - vfs, - templates, - `i18n/${config.i18n}/web/next`, - "apps/web", - config, - ); - } - - const hasReact = config.frontend.some((f) => - ["tanstack-router", "react-router", "tanstack-start", "react-vite"].includes(f), - ); - if (hasReact) { - processTemplatesFromPrefix( - vfs, - templates, - `i18n/${config.i18n}/web/react`, - "apps/web", - config, - ); - } + if (hasNext) { processTemplatesFromPrefix(vfs, templates, "i18n/" + config.i18n + "/web/next", "apps/web", config); } + const hasReact = config.frontend.some((f) => ["tanstack-router", "react-router", "tanstack-start", "react-vite"].includes(f)); + if (hasReact) { processTemplatesFromPrefix(vfs, templates, "i18n/" + config.i18n + "/web/react", "apps/web", config); } } diff --git a/packages/template-generator/src/utils/add-deps.ts b/packages/template-generator/src/utils/add-deps.ts index 453430e5e..dbd2a727b 100644 --- a/packages/template-generator/src/utils/add-deps.ts +++ b/packages/template-generator/src/utils/add-deps.ts @@ -694,6 +694,8 @@ export const dependencyVersionMap = { // i18n - next-intl "next-intl": "^4.1.0", + + // EdgeDB edgedb: "^2.0.1", "@edgedb/generate": "^0.6.1", diff --git a/packages/template-generator/templates/i18n/i18next/web/base/public/locales/en/translation.json b/packages/template-generator/templates/i18n/i18next/web/base/public/locales/en/translation.json index 5a2b06e56..d605a7499 100644 --- a/packages/template-generator/templates/i18n/i18next/web/base/public/locales/en/translation.json +++ b/packages/template-generator/templates/i18n/i18next/web/base/public/locales/en/translation.json @@ -1,5 +1 @@ -{ - "welcome": "Welcome to {{projectName}}", - "description": "Get started by editing the source code", - "language": "Language" -} +{ "welcome": "Welcome", "description": "Get started by editing the source code", "language": "Language" } diff --git a/packages/template-generator/templates/i18n/i18next/web/base/public/locales/fr/translation.json b/packages/template-generator/templates/i18n/i18next/web/base/public/locales/fr/translation.json index 2e12b36a0..e9bca869f 100644 --- a/packages/template-generator/templates/i18n/i18next/web/base/public/locales/fr/translation.json +++ b/packages/template-generator/templates/i18n/i18next/web/base/public/locales/fr/translation.json @@ -1,5 +1 @@ -{ - "welcome": "Bienvenue sur {{projectName}}", - "description": "Commencez par modifier le code source", - "language": "Langue" -} +{ "welcome": "Bienvenue", "description": "Commencez par modifier le code source", "language": "Langue" } diff --git a/packages/template-generator/templates/i18n/i18next/web/base/src/i18n/config.ts.hbs b/packages/template-generator/templates/i18n/i18next/web/base/src/i18n/config.ts.hbs index d2df0521f..74f004430 100644 --- a/packages/template-generator/templates/i18n/i18next/web/base/src/i18n/config.ts.hbs +++ b/packages/template-generator/templates/i18n/i18next/web/base/src/i18n/config.ts.hbs @@ -15,12 +15,8 @@ i18n fallbackLng: "en", supportedLngs: ["en", "fr"], debug: process.env.NODE_ENV === "development", - interpolation: { - escapeValue: false, - }, - backend: { - loadPath: "/locales/{{lng}}/{{ns}}.json", - }, + interpolation: { escapeValue: false }, + backend: { loadPath: "/locales/{{lng}}/{{ns}}.json" }, }); export default i18n; diff --git a/packages/template-generator/templates/i18n/next-intl/web/base/messages/en.json b/packages/template-generator/templates/i18n/next-intl/web/base/messages/en.json index bce62bb75..ceb601c22 100644 --- a/packages/template-generator/templates/i18n/next-intl/web/base/messages/en.json +++ b/packages/template-generator/templates/i18n/next-intl/web/base/messages/en.json @@ -1,7 +1 @@ -{ - "common": { - "welcome": "Welcome to {projectName}", - "description": "Get started by editing the source code", - "language": "Language" - } -} +{ "common": { "welcome": "Welcome", "description": "Get started by editing the source code", "language": "Language" } } diff --git a/packages/template-generator/templates/i18n/next-intl/web/base/messages/fr.json b/packages/template-generator/templates/i18n/next-intl/web/base/messages/fr.json index 41dc277f1..5276cb859 100644 --- a/packages/template-generator/templates/i18n/next-intl/web/base/messages/fr.json +++ b/packages/template-generator/templates/i18n/next-intl/web/base/messages/fr.json @@ -1,7 +1 @@ -{ - "common": { - "welcome": "Bienvenue sur {projectName}", - "description": "Commencez par modifier le code source", - "language": "Langue" - } -} +{ "common": { "welcome": "Bienvenue", "description": "Commencez par modifier le code source", "language": "Langue" } } diff --git a/packages/template-generator/templates/i18n/next-intl/web/next/src/i18n.ts.hbs b/packages/template-generator/templates/i18n/next-intl/web/next/src/i18n.ts.hbs index c4b7ca26c..fac5ffcb8 100644 --- a/packages/template-generator/templates/i18n/next-intl/web/next/src/i18n.ts.hbs +++ b/packages/template-generator/templates/i18n/next-intl/web/next/src/i18n.ts.hbs @@ -1,5 +1,4 @@ import { getRequestConfig } from "next-intl/server"; - export default getRequestConfig(async ({ locale }) => ({ - messages: (await import(`../../messages/${locale}.json`)).default, + messages: (await import("../../messages/" + locale + ".json")).default, })); diff --git a/packages/template-generator/templates/i18n/next-intl/web/next/src/middleware.ts.hbs b/packages/template-generator/templates/i18n/next-intl/web/next/src/middleware.ts.hbs index 368f1c5de..52e6a0104 100644 --- a/packages/template-generator/templates/i18n/next-intl/web/next/src/middleware.ts.hbs +++ b/packages/template-generator/templates/i18n/next-intl/web/next/src/middleware.ts.hbs @@ -1,10 +1,3 @@ import createMiddleware from "next-intl/middleware"; - -export default createMiddleware({ - locales: ["en", "fr"], - defaultLocale: "en", -}); - -export const config = { - matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"], -}; +export default createMiddleware({ locales: ["en", "fr"], defaultLocale: "en" }); +export const config = { matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"] }; diff --git a/packages/types/src/compatibility.ts b/packages/types/src/compatibility.ts index c6111c644..e5a95ed81 100644 --- a/packages/types/src/compatibility.ts +++ b/packages/types/src/compatibility.ts @@ -332,10 +332,8 @@ export const getCategoryDisplayName = (categoryKey: string): string => { goLogging: "Go Logging", }; - if (categoryKey === "i18n") { - return "Internationalization (i18n)"; - } + if (categoryKey === "i18n") return "Internationalization (i18n)"; if (rustCategoryNames[categoryKey]) { return rustCategoryNames[categoryKey]; } @@ -1948,15 +1946,10 @@ export const getDisabledReason = ( } } + // I18N CONSTRAINTS // ============================================ // I18N CONSTRAINTS // ============================================ - if (category === "i18n") { - if (optionId === "next-intl" && !currentStack.webFrontend.includes("next")) { - return "next-intl requires a Next.js frontend"; - } - } - return null; }; diff --git a/packages/types/src/option-metadata.ts b/packages/types/src/option-metadata.ts index d20186cc2..e40e8491a 100644 --- a/packages/types/src/option-metadata.ts +++ b/packages/types/src/option-metadata.ts @@ -16,9 +16,9 @@ import { EMAIL_VALUES, FEATURE_FLAGS_VALUES, FILE_STORAGE_VALUES, + I18N_VALUES, FILE_UPLOAD_VALUES, FORMS_VALUES, - I18N_VALUES, GO_API_VALUES, GO_CLI_VALUES, GO_LOGGING_VALUES, @@ -95,6 +95,7 @@ export type OptionCategory = | "search" | "fileStorage" | "i18n" + | "i18n" | "animation" | "cssFramework" | "uiLibrary" diff --git a/testing/lib/generate-combos/options.ts b/testing/lib/generate-combos/options.ts index e91a4163c..9531a4b9f 100644 --- a/testing/lib/generate-combos/options.ts +++ b/testing/lib/generate-combos/options.ts @@ -22,9 +22,9 @@ import { EMAIL_VALUES, EXAMPLES_VALUES, FILE_STORAGE_VALUES, + I18N_VALUES, FILE_UPLOAD_VALUES, FORMS_VALUES, - I18N_VALUES, GO_API_VALUES, GO_CLI_VALUES, GO_LOGGING_VALUES, From 7aa110385115c3ac577e30e4b2a7898d4cafca8d Mon Sep 17 00:00:00 2001 From: Marve10s Date: Fri, 10 Apr 2026 13:20:26 +0300 Subject: [PATCH 3/3] fix: repair tech-resource-links.ts syntax error and add i18n to test defaults Remove two stray closing braces in BASE_LINKS that broke the object literal after the i18n entries were added. Also add i18n to test-utils coreStackFlags and coreStackDefaults so tests don't hang waiting for the new i18n prompt. --- .../template-snapshots.test.ts.snap | 50 +++++++++++++++++-- apps/cli/test/test-utils.ts | 3 ++ apps/web/src/lib/tech-resource-links.ts | 2 - 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/apps/cli/test/__snapshots__/template-snapshots.test.ts.snap b/apps/cli/test/__snapshots__/template-snapshots.test.ts.snap index 73c2428df..839edd695 100644 --- a/apps/cli/test/__snapshots__/template-snapshots.test.ts.snap +++ b/apps/cli/test/__snapshots__/template-snapshots.test.ts.snap @@ -11139,6 +11139,7 @@ exports[`Template Snapshots - Rust Ecosystem Rust File Structure Snapshots file "crates/client/src/lib.rs", "crates/client/style/main.css", "crates/server/Cargo.toml", + "crates/server/src/cache.rs", "crates/server/src/error.rs", "crates/server/src/main.rs", "rust-toolchain.toml", @@ -11156,6 +11157,7 @@ exports[`Template Snapshots - Rust Ecosystem Rust File Structure Snapshots file "crates/dioxus-client/assets/main.css", "crates/dioxus-client/src/main.rs", "crates/server/Cargo.toml", + "crates/server/src/cache.rs", "crates/server/src/error.rs", "crates/server/src/main.rs", "rust-toolchain.toml", @@ -11171,6 +11173,7 @@ exports[`Template Snapshots - Rust Ecosystem Rust File Structure Snapshots file "crates/cli/Cargo.toml", "crates/cli/src/main.rs", "crates/server/Cargo.toml", + "crates/server/src/cache.rs", "crates/server/src/error.rs", "crates/server/src/main.rs", "rust-toolchain.toml", @@ -11184,6 +11187,7 @@ exports[`Template Snapshots - Rust Ecosystem Rust File Structure Snapshots file "Cargo.toml", "README.md", "crates/server/Cargo.toml", + "crates/server/src/cache.rs", "crates/server/src/error.rs", "crates/server/src/main.rs", "rust-toolchain.toml", @@ -11197,6 +11201,7 @@ exports[`Template Snapshots - Rust Ecosystem Rust File Structure Snapshots file "Cargo.toml", "README.md", "crates/server/Cargo.toml", + "crates/server/src/cache.rs", "crates/server/src/error.rs", "crates/server/src/main.rs", "rust-toolchain.toml", @@ -11205,7 +11210,7 @@ exports[`Template Snapshots - Rust Ecosystem Rust File Structure Snapshots file exports[`Template Snapshots - Rust Ecosystem Rust Key File Content Snapshots key files: axum-leptos-seaorm 1`] = ` { - "fileCount": 14, + "fileCount": 15, "files": [ { "content": @@ -11288,6 +11293,7 @@ web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlE + [profile.dev] opt-level = 0 @@ -11494,6 +11500,7 @@ sea-orm.workspace = true + [[bin]] name = "server" path = "src/main.rs" @@ -11502,6 +11509,10 @@ path = "src/main.rs" , "path": "crates/server/Cargo.toml", }, + { + "content": "[exists]", + "path": "crates/server/src/cache.rs", + }, { "content": "[exists]", "path": "crates/server/src/error.rs", @@ -11555,6 +11566,7 @@ async fn main() -> anyhow::Result<()> { .with(tracing_subscriber::fmt::layer()) .init(); + // Initialize database connection let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); @@ -11602,7 +11614,7 @@ async fn main() -> anyhow::Result<()> { exports[`Template Snapshots - Rust Ecosystem Rust Key File Content Snapshots key files: actix-dioxus-sqlx 1`] = ` { - "fileCount": 13, + "fileCount": 14, "files": [ { "content": @@ -11682,6 +11694,7 @@ wasm-bindgen = "0.2" # Validation validator = { version = "0.19", features = ["derive"] } + [profile.dev] opt-level = 0 @@ -11878,6 +11891,7 @@ sqlx.workspace = true validator.workspace = true + [[bin]] name = "server" path = "src/main.rs" @@ -11886,6 +11900,10 @@ path = "src/main.rs" , "path": "crates/server/Cargo.toml", }, + { + "content": "[exists]", + "path": "crates/server/src/cache.rs", + }, { "content": "[exists]", "path": "crates/server/src/error.rs", @@ -11937,6 +11955,7 @@ async fn main() -> anyhow::Result<()> { .with(tracing_subscriber::fmt::layer()) .init(); + // Initialize database connection pool let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); @@ -11990,7 +12009,7 @@ async fn main() -> anyhow::Result<()> { exports[`Template Snapshots - Rust Ecosystem Rust Key File Content Snapshots key files: cli-clap 1`] = ` { - "fileCount": 11, + "fileCount": 12, "files": [ { "content": @@ -12058,6 +12077,7 @@ dotenvy = "0.15" clap = { version = "4", features = ["derive"] } + [profile.dev] opt-level = 0 @@ -12324,6 +12344,7 @@ dotenvy.workspace = true # CLI clap.workspace = true + [[bin]] name = "server" path = "src/main.rs" @@ -12332,6 +12353,10 @@ path = "src/main.rs" , "path": "crates/server/Cargo.toml", }, + { + "content": "[exists]", + "path": "crates/server/src/cache.rs", + }, { "content": "[exists]", "path": "crates/server/src/error.rs", @@ -12352,6 +12377,7 @@ async fn main() -> anyhow::Result<()> { .with(tracing_subscriber::fmt::layer()) .init(); + tracing::info!("Hello from snapshot-rust-cli-clap!"); tracing::info!("Add a web framework (axum or actix-web) to start building your API."); @@ -12375,7 +12401,7 @@ async fn main() -> anyhow::Result<()> { exports[`Template Snapshots - Rust Ecosystem Rust Key File Content Snapshots key files: axum-envlogger 1`] = ` { - "fileCount": 9, + "fileCount": 10, "files": [ { "content": @@ -12446,6 +12472,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "sqlite", "my + [profile.dev] opt-level = 0 @@ -12499,6 +12526,7 @@ sqlx.workspace = true + [[bin]] name = "server" path = "src/main.rs" @@ -12507,6 +12535,10 @@ path = "src/main.rs" , "path": "crates/server/Cargo.toml", }, + { + "content": "[exists]", + "path": "crates/server/src/cache.rs", + }, { "content": "[exists]", "path": "crates/server/src/error.rs", @@ -12556,6 +12588,7 @@ async fn main() -> anyhow::Result<()> { // Initialize logging env_logger::init(); + // Initialize database connection pool let database_url = std::env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); @@ -12606,7 +12639,7 @@ async fn main() -> anyhow::Result<()> { exports[`Template Snapshots - Rust Ecosystem Rust Key File Content Snapshots key files: axum-eyre 1`] = ` { - "fileCount": 9, + "fileCount": 10, "files": [ { "content": @@ -12675,6 +12708,7 @@ tower-http = { version = "0.6", features = ["cors", "trace"] } + [profile.dev] opt-level = 0 @@ -12726,6 +12760,7 @@ tower-http.workspace = true + [[bin]] name = "server" path = "src/main.rs" @@ -12734,6 +12769,10 @@ path = "src/main.rs" , "path": "crates/server/Cargo.toml", }, + { + "content": "[exists]", + "path": "crates/server/src/cache.rs", + }, { "content": "[exists]", "path": "crates/server/src/error.rs", @@ -12774,6 +12813,7 @@ async fn main() -> eyre::Result<()> { .with(tracing_subscriber::fmt::layer()) .init(); + // Build router let app = Router::new() .route("/health", get(health)) diff --git a/apps/cli/test/test-utils.ts b/apps/cli/test/test-utils.ts index 15e5a6255..3174b8b19 100644 --- a/apps/cli/test/test-utils.ts +++ b/apps/cli/test/test-utils.ts @@ -41,6 +41,7 @@ import type { JobQueue, Analytics, FeatureFlags, + I18n, AiDocs, } from "../src/types"; @@ -163,6 +164,7 @@ export async function runTRPCTest(config: TestConfig): Promise { "jobQueue", "analytics", "featureFlags", + "i18n", "aiDocs", ]; const hasSpecificCoreConfig = coreStackFlags.some((flag) => config[flag] !== undefined); @@ -212,6 +214,7 @@ export async function runTRPCTest(config: TestConfig): Promise { jobQueue: "none" as JobQueue, analytics: "none" as Analytics, featureFlags: "none" as FeatureFlags, + i18n: "none" as I18n, aiDocs: [] as AiDocs[], }; diff --git a/apps/web/src/lib/tech-resource-links.ts b/apps/web/src/lib/tech-resource-links.ts index 797d158f4..60126174e 100644 --- a/apps/web/src/lib/tech-resource-links.ts +++ b/apps/web/src/lib/tech-resource-links.ts @@ -585,8 +585,6 @@ const BASE_LINKS: LinkMap = { docsUrl: "https://next-intl-docs.vercel.app/", githubUrl: "https://github.com/amannn/next-intl", }, - }, - }, s3: { docsUrl: "https://docs.aws.amazon.com/AmazonS3/", githubUrl: "https://github.com/aws/aws-sdk-js-v3",