Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/cli/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const DEFAULT_CONFIG_BASE = {
realtime: "none",
jobQueue: "none",
caching: "none",
i18n: "none",
search: "none",
fileStorage: "none",
animation: "none",
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/helpers/core/command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export async function createProjectHandler(
rustCaching: "none",
cms: "none",
caching: "none",
i18n: "none",
search: "none",
featureFlags: "none",
analytics: "none",
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ import {
type CMS,
CachingSchema,
type Caching,
I18nSchema,
type I18n,
SearchSchema,
FileStorageSchema,
RustWebFrameworkSchema,
Expand Down Expand Up @@ -191,6 +193,7 @@ export const router = os.router({
analytics: AnalyticsSchema.optional().describe("Privacy-focused analytics"),
cms: CMSSchema.optional().describe("Headless CMS solution"),
caching: CachingSchema.optional().describe("Caching solution"),
i18n: I18nSchema.optional().describe("Internationalization (i18n) library"),
search: SearchSchema.optional().describe("Search engine solution"),
fileStorage: FileStorageSchema.optional().describe("File storage solution (S3, R2)"),
frontend: z.array(FrontendSchema).optional(),
Expand Down Expand Up @@ -574,6 +577,7 @@ export async function createVirtual(
analytics: options.analytics || "none",
cms: options.cms || "none",
caching: options.caching || "none",
i18n: options.i18n || "none",
search: options.search || "none",
fileStorage: options.fileStorage || "none",
// Rust ecosystem options
Expand Down
7 changes: 6 additions & 1 deletion apps/cli/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
GoLoggingSchema,
GoOrmSchema,
GoWebFrameworkSchema,
I18nSchema,
JobQueueSchema,
LoggingSchema,
ObservabilitySchema,
Expand Down Expand Up @@ -172,6 +173,7 @@ const SCHEMA_MAP: Record<string, z.ZodType> = {
analytics: AnalyticsSchema,
cms: CMSSchema,
caching: CachingSchema,
i18n: I18nSchema,
search: SearchSchema,
fileStorage: FileStorageSchema,
addons: AddonsSchema,
Expand Down Expand Up @@ -210,7 +212,7 @@ const ECOSYSTEM_CATEGORIES: Record<string, string[]> = {
"email", "fileUpload", "effect", "ai", "stateManagement", "forms", "validation",
"testing", "cssFramework", "uiLibrary", "realtime", "jobQueue", "animation",
"logging", "observability", "featureFlags", "analytics", "cms", "caching",
"search", "fileStorage", "astroIntegration",
"i18n", "search", "fileStorage", "astroIntegration",
],
rust: ["rustWebFramework", "rustFrontend", "rustOrm", "rustApi", "rustCli", "rustLibraries", "rustLogging", "rustErrorHandling", "rustCaching"],
python: ["pythonWebFramework", "pythonOrm", "pythonValidation", "pythonAi", "pythonAuth", "pythonTaskQueue", "pythonQuality"],
Expand Down Expand Up @@ -311,6 +313,7 @@ function buildProjectConfig(
analytics: "none",
cms: (input.cms as ProjectConfig["cms"]) ?? "none",
caching: (input.caching as ProjectConfig["caching"]) ?? "none",
i18n: (input.i18n as ProjectConfig["i18n"]) ?? "none",
search: (input.search as ProjectConfig["search"]) ?? "none",
fileStorage: (input.fileStorage as ProjectConfig["fileStorage"]) ?? "none",
addons: (input.addons as ProjectConfig["addons"]) ?? [],
Expand Down Expand Up @@ -400,6 +403,7 @@ function buildCompatibilityInput(input: Record<string, unknown>): CompatibilityI
realtime: (input.realtime as string) ?? "none",
jobQueue: (input.jobQueue as string) ?? "none",
caching: (input.caching as string) ?? "none",
i18n: (input.i18n as string) ?? "none",
animation: (input.animation as string) ?? "none",
cssFramework: (input.cssFramework as string) ?? "tailwind",
uiLibrary: (input.uiLibrary as string) ?? "none",
Expand Down Expand Up @@ -635,6 +639,7 @@ export async function startMcpServer() {
observability: ObservabilitySchema.optional().describe("Observability"),
search: SearchSchema.optional().describe("Search engine"),
caching: CachingSchema.optional().describe("Caching solution"),
i18n: I18nSchema.optional().describe("Internationalization (i18n) library"),
cms: CMSSchema.optional().describe("CMS"),
fileStorage: FileStorageSchema.optional().describe("File storage"),
fileUpload: FileUploadSchema.optional().describe("File upload"),
Expand Down
8 changes: 8 additions & 0 deletions apps/cli/src/prompts/config-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
Caching,
CMS,
CSSFramework,
I18n,
Database,
DatabaseSetup,
Ecosystem,
Expand Down Expand Up @@ -93,6 +94,7 @@ import {
getGoOrmChoice,
getGoWebFrameworkChoice,
} from "./go-ecosystem";
import { getI18nChoice } from "./i18n";
import { getinstallChoice } from "./install";
import { getJobQueueChoice } from "./job-queue";
import { getLoggingChoice } from "./logging";
Expand Down Expand Up @@ -170,6 +172,7 @@ type PromptGroupResults = {
analytics: Analytics;
cms: CMS;
caching: Caching;
i18n: I18n;
search: Search;
fileStorage: FileStorage;
// Rust ecosystem
Expand Down Expand Up @@ -412,6 +415,10 @@ export async function gatherConfig(
if (results.ecosystem !== "typescript") return Promise.resolve("none" as Caching);
return getCachingChoice(flags.caching, results.backend);
},
i18n: ({ results }) => {
if (results.ecosystem !== "typescript") return Promise.resolve("none" as I18n);
return getI18nChoice(flags.i18n, results.frontend);
},
search: ({ results }) => {
if (results.ecosystem !== "typescript") return Promise.resolve("none" as Search);
return getSearchChoice(flags.search, results.backend);
Expand Down Expand Up @@ -569,6 +576,7 @@ export async function gatherConfig(
analytics: result.analytics,
cms: result.cms,
caching: result.caching,
i18n: result.i18n,
search: result.search,
fileStorage: result.fileStorage,
// Ecosystem
Expand Down
42 changes: 42 additions & 0 deletions apps/cli/src/prompts/i18n.ts
Original file line number Diff line number Diff line change
@@ -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, frontend?: Frontend[]) {
if (i18n !== undefined) return i18n;

const hasNext = frontend?.includes("next") ?? false;

const options = [
{
value: "i18next" as const,
label: "i18next",
hint: "Full-featured i18n framework, works with all frontends",
},
...(hasNext
? [
{
value: "next-intl" as const,
label: "next-intl",
hint: "Lightweight i18n for Next.js with App Router support",
},
]
: []),
{
value: "none" as const,
label: "None",
hint: "No internationalization setup",
},
];

const response = await navigableSelect<I18n>({
message: "Select internationalization (i18n) library",
options,
initialValue: "none",
});

if (isCancel(response)) return exitCancelled("Operation cancelled");

return response;
}
2 changes: 2 additions & 0 deletions apps/cli/src/utils/bts-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) {
analytics: projectConfig.analytics,
cms: projectConfig.cms,
caching: projectConfig.caching,
i18n: projectConfig.i18n,
search: projectConfig.search,
fileStorage: projectConfig.fileStorage,
rustWebFramework: projectConfig.rustWebFramework,
Expand Down Expand Up @@ -112,6 +113,7 @@ export async function writeBtsConfig(projectConfig: ProjectConfig) {
analytics: btsConfig.analytics,
cms: btsConfig.cms,
caching: btsConfig.caching,
i18n: btsConfig.i18n,
search: btsConfig.search,
fileStorage: btsConfig.fileStorage,
rustWebFramework: btsConfig.rustWebFramework,
Expand Down
5 changes: 5 additions & 0 deletions apps/cli/src/utils/config-processing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
GoOrm,
GoApi,
GoWebFramework,
I18n,
JobQueue,
Logging,
Observability,
Expand Down Expand Up @@ -172,6 +173,10 @@ export function processFlags(options: CLIInput, projectName?: string) {
config.caching = options.caching as Caching;
}

if (options.i18n !== undefined) {
config.i18n = options.i18n as I18n;
}

if (options.search !== undefined) {
config.search = options.search as Search;
}
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/utils/generate-reproducible-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function getTypeScriptFlags(config: ProjectConfig) {
flags.push(`--logging ${config.logging}`);
flags.push(`--observability ${config.observability}`);
flags.push(`--caching ${config.caching}`);
flags.push(`--i18n ${config.i18n}`);
flags.push(`--cms ${config.cms}`);
flags.push(`--search ${config.search}`);
flags.push(`--file-storage ${config.fileStorage}`);
Expand Down
3 changes: 3 additions & 0 deletions apps/cli/test/generate-reproducible-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function makeConfig(overrides: Partial<ProjectConfig> = {}): ProjectConfig {
analytics: "none",
cms: "none",
caching: "none",
i18n: "none",
search: "none",
fileStorage: "none",
rustWebFramework: "none",
Expand Down Expand Up @@ -113,6 +114,7 @@ describe("generateReproducibleCommand", () => {
observability: "none",
cms: "none",
caching: "none",
i18n: "none",
search: "none",
fileStorage: "none",
pythonWebFramework: "django",
Expand Down Expand Up @@ -178,6 +180,7 @@ describe("generateReproducibleCommand", () => {
observability: "none",
cms: "none",
caching: "none",
i18n: "none",
search: "none",
fileStorage: "none",
pythonWebFramework: "fastapi",
Expand Down
3 changes: 3 additions & 0 deletions apps/cli/test/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type {
Observability,
CMS,
Caching,
I18n,
Search,
Ecosystem,
AI,
Expand Down Expand Up @@ -156,6 +157,7 @@ export async function runTRPCTest(config: TestConfig): Promise<TestResult> {
"logging",
"observability",
"caching",
"i18n",
"search",
"fileStorage",
"cms",
Expand Down Expand Up @@ -205,6 +207,7 @@ export async function runTRPCTest(config: TestConfig): Promise<TestResult> {
logging: "none" as Logging,
observability: "none" as Observability,
caching: "none" as Caching,
i18n: "none" as I18n,
search: "none" as Search,
fileStorage: "none" as FileStorage,
cms: "none" as CMS,
Expand Down
4 changes: 4 additions & 0 deletions apps/web/public/icon/next-intl.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions apps/web/src/lib/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2253,6 +2253,32 @@ export const TECH_OPTIONS: Record<
default: true,
},
],
i18n: [
{
id: "i18next",
name: "i18next",
description: "Full-featured i18n framework with plugins for all major frontends",
icon: "https://cdn.simpleicons.org/i18next/26A69A",
color: "from-teal-500 to-green-600",
default: false,
},
{
id: "next-intl",
name: "next-intl",
description: "Lightweight internationalization for Next.js with App Router support",
icon: "/icon/next-intl.svg",
color: "from-blue-500 to-indigo-600",
default: false,
},
{
id: "none",
name: "No i18n",
description: "Skip internationalization setup",
icon: "",
color: "from-gray-400 to-gray-600",
default: true,
},
],
search: [
{
id: "meilisearch",
Expand Down Expand Up @@ -3241,6 +3267,7 @@ export const ECOSYSTEM_CATEGORIES: Record<Ecosystem, TechCategory[]> = {
"observability",
"featureFlags",
"caching",
"i18n",
"ai",
"cms",
"codeQuality",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/preview-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function stackStateToProjectConfig(input: Partial<StackState>): ProjectCo
analytics: stack.analytics as ProjectConfig["analytics"],
cms: stack.cms as ProjectConfig["cms"],
caching: stack.caching as ProjectConfig["caching"],
i18n: stack.i18n as ProjectConfig["i18n"],
search: stack.search as ProjectConfig["search"],
fileStorage: stack.fileStorage as ProjectConfig["fileStorage"],
rustWebFramework: stack.rustWebFramework as ProjectConfig["rustWebFramework"],
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/stack-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type StackState = {
realtime: string;
jobQueue: string;
caching: string;
i18n: string;
animation: string;
cssFramework: string;
uiLibrary: string;
Expand Down Expand Up @@ -104,6 +105,7 @@ export const DEFAULT_STACK: StackState = {
realtime: "none",
jobQueue: "none",
caching: "none",
i18n: "none",
animation: "none",
cssFramework: "tailwind",
uiLibrary: "shadcn-ui",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/stack-url-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const stackUrlKeys = {
realtime: "rt2",
jobQueue: "jq",
caching: "cache",
i18n: "i18n",
animation: "anim",
cms: "cms",
search: "srch",
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/lib/stack-url-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function loadStackParams(
realtime: getString("realtime", DEFAULT_STACK.realtime),
jobQueue: getString("jobQueue", DEFAULT_STACK.jobQueue),
caching: getString("caching", DEFAULT_STACK.caching),
i18n: getString("i18n", DEFAULT_STACK.i18n),
animation: getString("animation", DEFAULT_STACK.animation),
cms: getString("cms", DEFAULT_STACK.cms),
search: getString("search", DEFAULT_STACK.search),
Expand Down Expand Up @@ -175,6 +176,7 @@ export function serializeStackParams(basePath: string, stack: StackState): strin
addParam("realtime", stack.realtime);
addParam("jobQueue", stack.jobQueue);
addParam("caching", stack.caching);
addParam("i18n", stack.i18n);
addParam("animation", stack.animation);
addParam("cms", stack.cms);
addParam("search", stack.search);
Expand Down Expand Up @@ -260,6 +262,7 @@ function searchToStack(search: StackSearchParams | undefined): StackState {
realtime: search.rt2 ?? DEFAULT_STACK.realtime,
jobQueue: search.jq ?? DEFAULT_STACK.jobQueue,
caching: search.cache ?? DEFAULT_STACK.caching,
i18n: search.i18n ?? DEFAULT_STACK.i18n,
animation: search.anim ?? DEFAULT_STACK.animation,
cms: search.cms ?? DEFAULT_STACK.cms,
search: search.srch ?? DEFAULT_STACK.search,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/stack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const TYPESCRIPT_CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
"realtime",
"jobQueue",
"caching",
"i18n",
"search",
"fileStorage",
"animation",
Expand Down Expand Up @@ -228,6 +229,7 @@ export function generateStackCommand(stack: StackState) {
`--realtime ${stack.realtime}`,
`--job-queue ${stack.jobQueue}`,
`--caching ${stack.caching}`,
`--i18n ${stack.i18n}`,
`--search ${stack.search}`,
`--file-storage ${stack.fileStorage}`,
`--cms ${stack.cms}`,
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/lib/tech-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ export const ICON_REGISTRY: Record<string, IconConfig> = {
// ─── Caching ───────────────────────────────────────────────────────────────
"upstash-redis": { type: "si", slug: "upstash", hex: "00E9A3" },

// ─── i18n ─────────────────────────────────────────────────────────────────
i18next: { type: "si", slug: "i18next", hex: "26A69A" },
"next-intl": { type: "local", src: "/icon/next-intl.svg" },

// ─── Search ────────────────────────────────────────────────────────────────
meilisearch: { type: "si", slug: "meilisearch", hex: "FF5CAA" },
typesense: { type: "local", src: "/icon/typesense.png" },
Expand Down
Loading
Loading