npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@statsbygg/generators

v0.1.16

Published

Boilerplate generation scripts for Statsbygg projects

Readme

@statsbygg/generators

Interactive code generators for scaffolding components, forms, API services, state management, and routes in Statsbygg applications.

Overview

@statsbygg/generators is a collection of CLI tools that automate the creation of boilerplate code. Instead of manually creating files, importing dependencies, and writing repetitive code, these generators handle it for you while enforcing Statsbygg's best practices.

Philosophy:

  • Golden Path: Generators produce opinionated, production-ready code
  • Consistency: All generated code follows the same patterns and structure
  • Speed: Scaffold complex features in seconds, not hours
  • Flexibility: Customize generated code through interactive prompts

Installation

This package is automatically included in projects created with @statsbygg/create-app. If you need to add it manually:

npm install --save-dev @statsbygg/generators

Usage

All generators are available as npm scripts in your package.json:

npm run gen:<generator-name>

Each generator provides an interactive CLI that guides you through the creation process.


Available Generators

1. gen:component - Component Generator

Scaffolds a new React component with TypeScript, CSS modules, tests, and Storybook stories.

Command

npm run gen:component

Or with a specific name:

npm run gen:component MyButton

Interactive Prompts

  1. Component name: PascalCase (e.g., UserCard, SearchBar)
  2. Component type:
    • ui - Reusable UI component (e.g., buttons, inputs)
    • feature - Feature-specific component
  3. Feature name (if type is feature): e.g., userManagement, dashboard
  4. Nested path (optional): Subdirectory within the feature (e.g., forms, modals)
  5. Files to create:
    • Component file (.tsx) - Always included
    • Styles (.module.css)
    • Types (.types.ts)
    • Tests (.test.tsx)
    • Stories (.stories.tsx)
  6. Next.js integration options:
    • State management (Zustand)
    • API integration (TanStack Query)
    • Client component

Generated Structure

For UI Component:

src/components/ui/MyButton/
├── MyButton.tsx              # Main component
├── MyButton.module.css       # CSS module
├── MyButton.types.ts         # TypeScript types
├── MyButton.test.tsx         # Vitest tests
├── MyButton.stories.tsx      # Storybook stories
└── index.ts                  # Barrel export

For Feature Component:

src/components/features/userManagement/forms/UserForm/
├── UserForm.tsx
├── UserForm.module.css
├── UserForm.types.ts
├── UserForm.test.tsx
├── UserForm.stories.tsx
└── index.ts

Generated Code Example

// MyButton.tsx
import { MyButtonProps } from './MyButton.types';
import styles from './MyButton.module.css';

export function MyButton(props: MyButtonProps) {
  return (
    <div className={styles.container}>
      MyButton
    </div>
  );
}

Index File Updates

The generator automatically updates:

  • /src/components/ui/index.ts (for UI components)
  • /src/components/features/{featureName}/index.ts (for feature components)

2. gen:route - App Router Route Generator

Creates Next.js App Router pages with special files (layout, loading, error, not-found).

Command

npm run gen:route

Or specify the route path directly:

npm run gen:route dashboard
npm run gen:route admin/users
npm run gen:route api/users

Interactive Prompts

  1. Route path: e.g., about, dashboard, api/users
  2. Files to create:
    • Page (page.tsx or route.ts for API routes)
    • Styles (page.module.css)
    • Loading (loading.tsx)
    • Error (error.tsx)
    • Not Found (not-found.tsx)
    • Layout (layout.tsx)
    • Template (template.tsx)
  3. Client component? (for page routes only)

Generated Structure

For Standard Page:

src/app/dashboard/
├── page.tsx                  # Main page component
├── page.module.css           # Page-specific styles
├── loading.tsx               # Loading UI (Suspense fallback)
├── error.tsx                 # Error boundary
├── not-found.tsx             # 404 page
├── layout.tsx                # Layout wrapper
└── template.tsx              # Template with animations

For API Route:

src/app/api/users/
└── route.ts                  # API endpoint handlers

Generated Code Example

// page.tsx (Server Component)
import { Heading } from '@digdir/designsystemet-react';
import styles from './page.module.css';

export default function DashboardPage() {
  return (
    <div className={styles.container}>
      <Heading level={1}>Dashboard</Heading>
      <p>This is the dashboard page.</p>
    </div>
  );
}
// error.tsx (Client Component)
'use client';

import { Button, Heading } from '@digdir/designsystemet-react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
import styles from './error.module.css';

interface ErrorProps {
  error: Error & { digest?: string };
  reset: () => void;
}

export default function Error({ error, reset }: ErrorProps) {
  return (
    <div className={styles.container}>
      <AlertTriangle className={styles.icon} />
      <Heading level={1}>Something went wrong</Heading>
      <Button onClick={reset}>
        <RefreshCw /> Try again
      </Button>
    </div>
  );
}
// route.ts (API Route)
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  try {
    return NextResponse.json({ message: 'GET users' });
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    return NextResponse.json({ message: 'POST users', data: body });
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

3. gen:api - API Service Generator

Creates type-safe API request functions and TanStack Query hooks for data fetching.

Command

npm run gen:api

Or specify the API name:

npm run gen:api users

Interactive Prompts

  1. API service name: camelCase (e.g., users, products, newsArticles)
  2. API endpoint path: e.g., /users, /api/products (defaults to kebab-case of name)
  3. Select endpoints:
    • Get List (GET collection)
    • Get Single (GET by ID)
    • Create (POST)
    • Update (PUT)
    • Delete (DELETE)
  4. Requires authentication?: Yes/No

Generated Structure

src/lib/api/
├── requests/
│   └── users.ts              # Request functions (get, post, put, delete)
├── hooks/
│   └── users.ts              # TanStack Query hooks
└── index.ts                  # Updated with exports

src/types/api/
└── users.ts                  # TypeScript interfaces

Generated Code Example

Request Functions (requests/users.ts):

import { getRequest, postRequest, putRequest, deleteRequest } from '../requests';
import {
  GetUsersListResponse,
  GetUsersByIdResponse,
  CreateUsersRequest,
  CreateUsersResponse,
} from '@/types/api/users';

const ENDPOINT = '/users';

async function getUsersList(): Promise<GetUsersListResponse> {
  const response = await getRequest(ENDPOINT);
  return response.json();
}

async function getUsersById(id: string): Promise<GetUsersByIdResponse> {
  const response = await getRequest(`${ENDPOINT}/${id}`);
  return response.json();
}

async function createUsers(data: CreateUsersRequest): Promise<CreateUsersResponse> {
  const response = await postRequest(ENDPOINT, data);
  return response.json();
}

export { getUsersList, getUsersById, createUsers };

TanStack Query Hooks (hooks/users.ts):

import { useQuery, useMutation } from '@tanstack/react-query';
import { getUsersList, getUsersById, createUsers } from '../requests/users';
import {
  GetUsersListResponse,
  GetUsersByIdResponse,
  CreateUsersRequest,
  CreateUsersResponse,
} from '@/types/api/users';

export function useGetUsersList(options?: any) {
  return useQuery<GetUsersListResponse>({
    queryKey: ['users'],
    queryFn: () => getUsersList(),
    ...options,
  });
}

export function useGetUsersById(id: string, options?: any) {
  return useQuery<GetUsersByIdResponse>({
    queryKey: ['users', id],
    queryFn: () => getUsersById(id),
    enabled: !!id,
    ...options,
  });
}

export function useCreateUsers(options?: any) {
  return useMutation<CreateUsersResponse, Error, CreateUsersRequest>({
    mutationFn: (data: CreateUsersRequest) => createUsers(data),
    ...options,
  });
}

TypeScript Types (types/api/users.ts):

export interface User {
  id: string;
  // TODO: Define your User entity type
}

export interface GetUsersListResponse {
  data: User[];
  total: number;
  page?: number;
  limit?: number;
}

export interface GetUsersByIdResponse extends User {
  // TODO: Add any additional fields
}

export interface CreateUsersRequest {
  // TODO: Define request type
}

export interface CreateUsersResponse extends User {
  // TODO: Add any additional fields
}

Usage Example

'use client';

import { useGetUsersList, useCreateUsers } from '@/lib/api';

export function UserList() {
  const { data, isLoading, error } = useGetUsersList();
  const createUser = useCreateUsers();

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <div>
      <ul>
        {data?.data.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      
      <button
        onClick={() => createUser.mutate({ name: 'John Doe' })}
      >
        Add User
      </button>
    </div>
  );
}

4. gen:state - State Management Generator

Creates Zustand stores with different patterns (list management, UI state, API integration).

Command

npm run gen:state

Interactive Prompts

  1. State name: camelCase (e.g., userList, appSettings, searchFilters)
  2. Generation mode:
    • Guided Setup (interactive pattern selection)
    • Schema-based (use a state schema file)
  3. State pattern (for guided setup):
    • List Management (arrays with filtering, sorting, CRUD)
    • UI State (simple toggles, forms, modals)
    • API Integration (loading/error/data states)
  4. Features (based on pattern):
    • List: Filtering, Sorting, Pagination, Persistence
    • UI: Persistence, Reset Actions
    • API: Caching, Optimistic Updates

Generated Structure

src/lib/store/
├── useUserListStore.ts       # Zustand store
└── useUserList.ts            # Optional: API integration hook

Generated Code Example

List Management Store:

import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';

interface UserListState {
  // Data
  items: unknown[];
  filters: Record<string, unknown>;
  filteredItems: unknown[];
  sortBy: string;
  sortOrder: 'asc' | 'desc';

  // Actions
  setItems: (items: unknown[]) => void;
  addItem: (item: unknown) => void;
  updateItem: (id: string, updates: Partial<unknown>) => void;
  removeItem: (id: string) => void;
  clearItems: () => void;
  setFilter: (key: string, value: unknown) => void;
  clearFilter: (key?: string) => void;
  setSortBy: (field: string, order?: 'asc' | 'desc') => void;
}

const initialState = {
  items: [],
  filters: {},
  filteredItems: [],
  sortBy: '',
  sortOrder: 'asc' as const,
};

export const useUserListStore = create<UserListState>()(
  immer((set, get) => ({
    ...initialState,

    setItems: (items) => set((state) => {
      state.items = items;
      state.filteredItems = filterItems(items, state.filters);
    }),

    addItem: (item) => set((state) => {
      state.items.push(item);
      state.filteredItems = filterItems(state.items, state.filters);
    }),

    setFilter: (key, value) => set((state) => {
      if (value === null || value === undefined || value === '') {
        delete state.filters[key];
      } else {
        state.filters[key] = value;
      }
      state.filteredItems = filterItems(state.items, state.filters);
    }),

    // ... other actions
  }))
);

// Selectors
export const useUserListItems = () => useUserListStore((state) => state.items);
export const useUserListFilteredItems = () => useUserListStore((state) => state.filteredItems);

// Helper function
function filterItems(items: unknown[], filters: Record<string, unknown>): unknown[] {
  if (Object.keys(filters).length === 0) return items;
  
  return items.filter((item: any) => {
    return Object.entries(filters).every(([key, value]) => {
      if (!value) return true;
      const itemValue = item[key];
      if (typeof itemValue === 'string' && typeof value === 'string') {
        return itemValue.toLowerCase().includes(value.toLowerCase());
      }
      return itemValue === value;
    });
  });
}

UI State Store:

interface AppSettingsState {
  value: unknown;
  setValue: (value: unknown) => void;
  reset: () => void;
}

const initialState = { value: null };

export const useAppSettingsStore = create<AppSettingsState>()(
  immer((set) => ({
    ...initialState,
    
    setValue: (value) => set((state) => {
      state.value = value;
    }),
    
    reset: () => set(initialState),
  }))
);

Usage Example

'use client';

import { useUserListStore, useUserListFilteredItems } from '@/lib/store/useUserListStore';

export function UserList() {
  const filteredUsers = useUserListFilteredItems();
  const { setFilter, addItem } = useUserListStore();

  return (
    <div>
      <input
        type="text"
        placeholder="Search users..."
        onChange={(e) => setFilter('name', e.target.value)}
      />
      
      <ul>
        {filteredUsers.map((user: any) => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
      
      <button onClick={() => addItem({ id: '123', name: 'New User' })}>
        Add User
      </button>
    </div>
  );
}

5. gen:schema - Form Schema Generator

Creates form schemas using a fluent DSL. These schemas are used by the gen:form generator.

Command

npm run gen:schema

Interactive Prompts

  1. Form name: PascalCase (e.g., ContactForm, UserRegistrationForm)
  2. Features:
    • Validation (Zod)
    • Auto-save drafts
    • Advanced state management
    • File upload
    • Storybook stories
    • Testing
  3. Number of sections: How many groups of fields (1-5)
  4. For each section:
    • Section name (camelCase)
    • Number of fields (1-10)
  5. For each field:
    • Field name (camelCase)
    • Field type (string, email, number, textarea, select, checkbox, boolean, files, any)
    • Configuration (required, validation rules, options, etc.)

Generated Structure

schemas/
└── contact-form.schema.js    # Form schema definition

Generated Schema Example

/**
 * Form Schema: ContactForm
 * This schema is designed to be loaded by the @statsbygg/generators tool.
 */

module.exports = defineFormSchema({
  name: 'ContactForm',
  features: ['validation', 'testing'],
  translationNamespace: 'contactForm',
  
  fields: {
    firstName: field.string()
      .min(2, 'validation.string.min')
      .max(50, 'validation.string.max')
      .required(),
      
    lastName: field.string()
      .min(2, 'validation.string.min')
      .max(50, 'validation.string.max')
      .required(),
      
    email: field.email()
      .required(),
      
    message: field.textarea()
      .rows(6)
      .min(10, 'validation.string.min')
      .required(),
      
    contactMethod: field.select({
      options: [
        { value: 'email', label: 'Email' },
        { value: 'phone', label: 'Phone' },
        { value: 'mail', label: 'Mail' },
      ]
    }).required(),
      
    agreeToTerms: field.boolean()
      .required(),
  },
  
  sections: [
    {
      name: 'personalInfo',
      title: 'contactForm.sections.personalInfo',
      fields: ['firstName', 'lastName', 'email']
    },
    {
      name: 'messageDetails',
      title: 'contactForm.sections.messageDetails',
      fields: ['message', 'contactMethod', 'agreeToTerms']
    }
  ],
});

Field API Reference

String Fields:

field.string()
  .min(2, 'Custom error message')
  .max(100)
  .pattern(/^[A-Z]/, 'Must start with capital letter')
  .required()

Email Fields:

field.email()
  .required()

Number Fields:

field.number()
  .min(0)
  .max(100)
  .step(0.1)
  .integer('Must be a whole number')
  .required()

Textarea Fields:

field.textarea()
  .rows(6)
  .min(10)
  .max(500)
  .required()

Select Fields:

field.select({
  options: [
    { value: 'option1', label: 'Option 1' },
    { value: 'option2', label: 'Option 2' },
  ]
})
  .required()
  .searchable()  // Enable search/filter
  .multiple()    // Allow multiple selection

File Upload Fields:

field.files()
  .accept(['.pdf', '.doc', '.docx'])
  .maxFiles(3)
  .maxSize('10MB')
  .multiple()
  .required()

Boolean/Checkbox Fields:

field.boolean()
  .required()

6. gen:form - Form Component Generator

Generates production-ready form components with React Hook Form, Zod validation, and DDDS components.

Command

npm run gen:form

Or generate directly from a schema file:

npm run gen:form schemas/contact-form.schema.js

Interactive Prompts

  1. Select schema file: Choose from available .schema.js files in /schemas
  2. Select feature: Choose which feature this form belongs to
  3. Nested path (optional): Subdirectory within the feature (e.g., forms)

Generated Structure

src/components/features/contact/forms/ContactForm/
├── ContactForm.tsx               # Main form component
├── ContactForm.module.css        # Styles
├── ContactForm.types.ts          # TypeScript types
├── contactFormSchema.ts          # Zod schema
├── ContactForm.test.tsx          # Vitest tests
├── ContactForm.stories.tsx       # Storybook stories
├── useContactForm.ts             # Custom hook (if features include autoSave)
├── contactFormStore.ts           # Zustand store (if features include persistence)
└── index.ts                      # Barrel export

src/lib/locales/no/forms/
└── contactForm.json              # Norwegian translations

src/lib/locales/en/forms/
└── contactForm.json              # English translations

Generated Form Component Example

'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button, Card, Spinner, Textfield, Select } from '@digdir/designsystemet-react';
import { useTranslation } from '@/lib/i18n';
import clsx from 'clsx';
import { contactFormSchema } from './contactFormSchema';
import { ContactFormData } from './ContactForm.types';
import styles from './ContactForm.module.css';

interface ContactFormProps {
  onSubmit: (data: ContactFormData) => void;
  defaultValues?: Partial<ContactFormData>;
  className?: string;
  isLoading?: boolean;
}

export function ContactForm({ 
  onSubmit, 
  defaultValues, 
  className,
  isLoading = false
}: ContactFormProps) {
  const { t } = useTranslation('contactForm');
  
  const form = useForm<ContactFormData>({
    resolver: zodResolver(contactFormSchema),
    defaultValues,
  });

  const onFormSubmit = async (data: ContactFormData) => {
    try {
      await onSubmit(data);
    } catch (error) {
      console.error('Form submission failed:', error);
    }
  };

  if (isLoading || form.formState.isSubmitting) {
    return (
      <Card className={clsx(styles.form, className)}>
        <div className={styles.loadingContainer}>
          <Spinner size="large" />
          <p>{t('actions.loading')}</p>
        </div>
      </Card>
    );
  }

  return (
    <Card className={clsx(styles.form, className)}>
      <form onSubmit={form.handleSubmit(onFormSubmit)} className={styles.formContainer}>
        {/* Personal Info Section */}
        <section className={styles.section}>
          <h2 className={styles.sectionTitle}>{t('sections.personalInfo')}</h2>
          
          <div className={styles.fieldGrid}>
            <div className={styles.field}>
              <Textfield
                type="text"
                label={t('fields.firstName.label')}
                placeholder={t('fields.firstName.placeholder')}
                error={form.formState.errors.firstName?.message}
                required={true}
                {...form.register('firstName')}
              />
            </div>
            
            <div className={styles.field}>
              <Textfield
                type="text"
                label={t('fields.lastName.label')}
                placeholder={t('fields.lastName.placeholder')}
                error={form.formState.errors.lastName?.message}
                required={true}
                {...form.register('lastName')}
              />
            </div>
            
            <div className={styles.field}>
              <Textfield
                type="email"
                label={t('fields.email.label')}
                placeholder={t('fields.email.placeholder')}
                error={form.formState.errors.email?.message}
                required={true}
                {...form.register('email')}
              />
            </div>
          </div>
        </section>

        {/* Message Details Section */}
        <section className={styles.section}>
          <h2 className={styles.sectionTitle}>{t('sections.messageDetails')}</h2>
          
          <div className={styles.fieldGrid}>
            <div className={styles.field}>
              <Textfield
                as="textarea"
                label={t('fields.message.label')}
                placeholder={t('fields.message.placeholder')}
                rows={6}
                error={form.formState.errors.message?.message}
                required={true}
                {...form.register('message')}
              />
            </div>
            
            <div className={styles.field}>
              <Select
                label={t('fields.contactMethod.label')}
                error={form.formState.errors.contactMethod?.message}
                required={true}
                {...form.register('contactMethod')}
              >
                <option value="">{t('common.select_option')}</option>
                <option value="email">{t('contactMethodOptions.email')}</option>
                <option value="phone">{t('contactMethodOptions.phone')}</option>
                <option value="mail">{t('contactMethodOptions.mail')}</option>
              </Select>
            </div>
            
            <div className={styles.field}>
              <label className={styles.checkboxLabel}>
                <input
                  type="checkbox"
                  className={styles.checkbox}
                  {...form.register('agreeToTerms')}
                />
                {t('fields.agreeToTerms.label')}
              </label>
            </div>
          </div>
        </section>
        
        {/* Submit Actions */}
        <div className={styles.actions}>
          <Button
            type="submit"
            size="large"
            disabled={form.formState.isSubmitting}
          >
            {form.formState.isSubmitting ? (
              <>
                <Spinner size="small" />
                {t('actions.submitting')}
              </>
            ) : (
              t('actions.submit')
            )}
          </Button>
          
          <Button
            type="button"
            variant="secondary"
            size="large"
            onClick={() => window.history.back()}
          >
            {t('actions.cancel')}
          </Button>
        </div>
      </form>
    </Card>
  );
}

Generated Zod Schema Example

import { z } from 'zod';

export const contactFormSchema = z.object({
  firstName: z.string().min(2, 'validation.string.min').max(50, 'validation.string.max'),
  lastName: z.string().min(2, 'validation.string.min').max(50, 'validation.string.max'),
  email: z.string().email(),
  message: z.string().min(10, 'validation.string.min'),
  contactMethod: z.enum(['email', 'phone', 'mail']),
  agreeToTerms: z.boolean(),
});

export type ContactFormData = z.infer<typeof contactFormSchema>;

Generated Translation Files

Norwegian (no/forms/contactForm.json):

{
  "sections": {
    "personalInfo": "Personlig informasjon",
    "messageDetails": "Melding"
  },
  "fields": {
    "firstName": {
      "label": "Fornavn",
      "placeholder": "Skriv inn fornavn"
    },
    "lastName": {
      "label": "Etternavn",
      "placeholder": "Skriv inn etternavn"
    },
    "email": {
      "label": "E-post",
      "placeholder": "[email protected]"
    },
    "message": {
      "label": "Melding",
      "placeholder": "Skriv din melding her..."
    },
    "contactMethod": {
      "label": "Foretrukket kontaktmetode"
    },
    "agreeToTerms": {
      "label": "Jeg godtar vilkårene"
    }
  },
  "contactMethodOptions": {
    "email": "E-post",
    "phone": "Telefon",
    "mail": "Post"
  },
  "validation": {
    "string": {
      "required": "Dette feltet er påkrevd",
      "min": "For kort",
      "max": "For langt"
    }
  },
  "actions": {
    "submit": "Send inn",
    "submitting": "Sender...",
    "cancel": "Avbryt",
    "loading": "Laster..."
  },
  "common": {
    "select_option": "Velg et alternativ..."
  }
}

Usage Example

// In a page or parent component
import { ContactForm } from '@/components/features/contact';

export default function ContactPage() {
  const handleSubmit = async (data) => {
    console.log('Form data:', data);
    // Send to API
  };

  return (
    <div>
      <h1>Contact Us</h1>
      <ContactForm onSubmit={handleSubmit} />
    </div>
  );
}

7. i18n:update - i18n Configuration Updater

Automatically scans translation files and updates the i18n configuration.

Command

npm run i18n:update

What It Does

  1. Scans /src/lib/locales/ for all JSON translation files
  2. Generates import statements for each file
  3. Builds the resource structure for i18next
  4. Updates /src/lib/i18n/config.ts with all translations

When to Use

Run this command whenever you:

  • Add a new translation file
  • Add a new locale directory
  • Reorganize translation files

Before Running

src/lib/locales/
├── no/
│   ├── common.json
│   └── forms/
│       └── contactForm.json
└── en/
    ├── common.json
    └── forms/
        └── contactForm.json

After Running

// config.ts (auto-generated)
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

import commonNo from '../locales/no/common.json';
import formsContactFormNo from '../locales/no/forms/contactForm.json';
import commonEn from '../locales/en/common.json';
import formsContactFormEn from '../locales/en/forms/contactForm.json';

const resources = {
  no: {
    common: commonNo,
    forms: {
      contactForm: formsContactFormNo
    }
  },
  en: {
    common: commonEn,
    forms: {
      contactForm: formsContactFormEn
    }
  },
};

i18n.use(initReactI18next).init({
  resources,
  lng: 'no',
  fallbackLng: 'no',
  defaultNS: 'common',
  ns: ['common', 'forms.contactForm'],
  interpolation: { escapeValue: false },
  react: { useSuspense: false },
});

export default i18n;

Advanced Usage

Generator Options via CLI

Most generators support passing options directly:

# Component generator
npm run gen:component MyButton -- --type=ui --no-tests

# API generator
npm run gen:api users -- --auth=true --endpoints=list,single,create

Custom Templates

You can override default templates by creating a .generators/ directory in your project root:

.generators/
└── templates/
    └── component/
        └── component.tsx.hbs

Generator Hooks

Add pre/post generation hooks in .generators/hooks.js:

module.exports = {
  beforeGenerate: async (type, options) => {
    console.log(`Generating ${type}...`);
  },
  
  afterGenerate: async (type, files) => {
    console.log(`Created ${files.length} files`);
  },
};

Troubleshooting

Generator Fails with "Permission Denied"

Run with elevated permissions:

sudo npm run gen:component

Or fix permissions on your node_modules:

sudo chown -R $(whoami) node_modules

Generated Files Not Updating in VS Code

Reload the TypeScript server: Cmd+Shift+P → "TypeScript: Restart TS Server"

Import Paths Not Resolving

Ensure your tsconfig.json has the correct paths:

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

Schema Parser Errors

If the schema generator fails:

  1. Check that you're using valid JavaScript in the schema file
  2. Don't use require() or import in schema files
  3. Avoid arrow functions - use function() {} syntax

Translation Keys Not Found

After adding translation files, run:

npm run i18n:update

Then restart your dev server.

Learn More


Created with ❤️ as Part of the Statsbygg Component System