@famgia/omnify-laravel 0.0.119 → 0.0.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ # Internationalization (i18n) Guide
2
+
3
+ > **Related:** [README](./README.md) | [Ant Design](./antd-guide.md)
4
+
5
+ This project uses `next-intl` for internationalization.
6
+
7
+ ## Current Locales
8
+
9
+ | Code | Language | Default |
10
+ | ---- | ---------- | ------- |
11
+ | ja | 日本語 | ✅ |
12
+ | en | English | |
13
+ | vi | Tiếng Việt | |
14
+
15
+ ---
16
+
17
+ ## Usage
18
+
19
+ ### Basic Translation
20
+
21
+ ```typescript
22
+ import { useTranslations } from "next-intl";
23
+
24
+ function MyComponent() {
25
+ const t = useTranslations();
26
+
27
+ return (
28
+ <Button>{t("common.save")}</Button>
29
+ );
30
+ }
31
+ ```
32
+
33
+ ### With Namespace
34
+
35
+ ```typescript
36
+ const t = useTranslations("messages");
37
+ t("created") // "作成しました" (ja) | "Created successfully" (en)
38
+ ```
39
+
40
+ ### With Parameters
41
+
42
+ ```typescript
43
+ t("validation.minLength", { field: "Password", min: 8 })
44
+ // "Password must be at least 8 characters"
45
+ ```
46
+
47
+ ### LocaleSwitcher Component
48
+
49
+ ```typescript
50
+ import LocaleSwitcher from "@/components/LocaleSwitcher";
51
+
52
+ <LocaleSwitcher /> // Dropdown to switch language
53
+ ```
54
+
55
+ ### Get Current Locale
56
+
57
+ ```typescript
58
+ import { useLocale } from "@/hooks/useLocale";
59
+
60
+ function MyComponent() {
61
+ const { locale, setLocale, localeNames } = useLocale();
62
+ // locale: "ja" | "en" | "vi"
63
+ // setLocale: (locale) => void
64
+ // localeNames: { ja: "日本語", en: "English", vi: "Tiếng Việt" }
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Adding New Language
71
+
72
+ ### Step 1: Create Message File
73
+
74
+ Create `src/i18n/messages/{locale}.json`:
75
+
76
+ ```json
77
+ {
78
+ "common": {
79
+ "save": "저장",
80
+ "cancel": "취소",
81
+ "delete": "삭제",
82
+ "edit": "편집",
83
+ "create": "만들기",
84
+ "search": "검색",
85
+ "loading": "로딩 중...",
86
+ "noData": "데이터 없음",
87
+ "confirm": "확인",
88
+ "back": "뒤로",
89
+ "next": "다음",
90
+ "previous": "이전",
91
+ "submit": "제출",
92
+ "reset": "초기화",
93
+ "close": "닫기",
94
+ "yes": "예",
95
+ "no": "아니오"
96
+ },
97
+ "messages": {
98
+ "created": "생성되었습니다",
99
+ "updated": "업데이트되었습니다",
100
+ "deleted": "삭제되었습니다",
101
+ "saved": "저장되었습니다",
102
+ "error": "오류가 발생했습니다",
103
+ "confirmDelete": "정말 삭제하시겠습니까?",
104
+ "networkError": "네트워크 오류",
105
+ "serverError": "서버 오류",
106
+ "unauthorized": "로그인해 주세요",
107
+ "forbidden": "접근 권한이 없습니다",
108
+ "notFound": "찾을 수 없습니다",
109
+ "sessionExpired": "세션이 만료되었습니다. 페이지를 새로고침해 주세요",
110
+ "tooManyRequests": "요청이 너무 많습니다. 잠시 기다려 주세요"
111
+ },
112
+ "auth": {
113
+ "login": "로그인",
114
+ "logout": "로그아웃",
115
+ "register": "회원가입",
116
+ "email": "이메일",
117
+ "password": "비밀번호",
118
+ "passwordConfirm": "비밀번호 확인",
119
+ "rememberMe": "로그인 상태 유지",
120
+ "forgotPassword": "비밀번호를 잊으셨나요?",
121
+ "resetPassword": "비밀번호 재설정",
122
+ "loginSuccess": "로그인되었습니다",
123
+ "logoutSuccess": "로그아웃되었습니다",
124
+ "registerSuccess": "가입이 완료되었습니다"
125
+ },
126
+ "validation": {
127
+ "required": "{field}은(는) 필수입니다",
128
+ "email": "유효한 이메일 주소를 입력해 주세요",
129
+ "minLength": "{field}은(는) {min}자 이상이어야 합니다",
130
+ "maxLength": "{field}은(는) {max}자 이하여야 합니다",
131
+ "passwordMatch": "비밀번호가 일치하지 않습니다"
132
+ },
133
+ "nav": {
134
+ "home": "홈",
135
+ "dashboard": "대시보드",
136
+ "users": "사용자",
137
+ "settings": "설정",
138
+ "profile": "프로필"
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### Step 2: Update Config
144
+
145
+ Edit `src/i18n/config.ts`:
146
+
147
+ ```typescript
148
+ export const locales = ["ja", "en", "vi", "ko"] as const; // Add new locale
149
+
150
+ export const localeNames: Record<Locale, string> = {
151
+ ja: "日本語",
152
+ en: "English",
153
+ vi: "Tiếng Việt",
154
+ ko: "한국어", // Add display name
155
+ };
156
+ ```
157
+
158
+ ### Step 3: Add Ant Design Locale
159
+
160
+ Edit `src/components/AntdThemeProvider.tsx`:
161
+
162
+ ```typescript
163
+ import jaJP from "antd/locale/ja_JP";
164
+ import enUS from "antd/locale/en_US";
165
+ import viVN from "antd/locale/vi_VN";
166
+ import koKR from "antd/locale/ko_KR"; // Add import
167
+
168
+ const antdLocales = {
169
+ ja: jaJP,
170
+ en: enUS,
171
+ vi: viVN,
172
+ ko: koKR, // Add mapping
173
+ };
174
+ ```
175
+
176
+ ---
177
+
178
+ ## File Structure
179
+
180
+ ```
181
+ src/i18n/
182
+ ├── config.ts # Locales configuration
183
+ ├── request.ts # Server-side locale detection
184
+ ├── index.ts # Exports
185
+ └── messages/
186
+ ├── ja.json # Japanese translations
187
+ ├── en.json # English translations
188
+ └── vi.json # Vietnamese translations
189
+ ```
190
+
191
+ ---
192
+
193
+ ## Best Practices
194
+
195
+ ```typescript
196
+ // ✅ DO: Use translation keys
197
+ <Button>{t("common.save")}</Button>
198
+
199
+ // ❌ DON'T: Hardcode strings
200
+ <Button>保存</Button>
201
+
202
+ // ✅ DO: Use namespaces for context
203
+ const tAuth = useTranslations("auth");
204
+ const tMessages = useTranslations("messages");
205
+
206
+ // ✅ DO: Use parameters for dynamic content
207
+ t("validation.minLength", { field: t("auth.password"), min: 8 })
208
+
209
+ // ❌ DON'T: Concatenate strings
210
+ `${t("auth.password")} must be at least 8 characters`
211
+ ```
@@ -0,0 +1,181 @@
1
+ # Laravel Integration
2
+
3
+ > **Related:** [README](./README.md) | [Service Pattern](./service-pattern.md)
4
+
5
+ ## Sanctum Authentication Flow
6
+
7
+ ```typescript
8
+ // Step 1: Get CSRF cookie (required before POST requests)
9
+ await api.get("/sanctum/csrf-cookie");
10
+
11
+ // Step 2: Login
12
+ await api.post("/login", { email, password });
13
+ // Cookie is now set automatically
14
+
15
+ // Step 3: Access protected routes
16
+ await api.get("/api/user"); // Works! Cookie sent automatically
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Error Handling Map
22
+
23
+ | HTTP Status | Laravel Meaning | Frontend Action |
24
+ | ----------- | ------------------- | ------------------------- |
25
+ | 200 | Success | Process response |
26
+ | 201 | Created | Process response |
27
+ | 204 | No Content | Success (no body) |
28
+ | 401 | Unauthenticated | Redirect to `/login` |
29
+ | 403 | Forbidden | Show error message |
30
+ | 404 | Not Found | Show error message |
31
+ | 419 | CSRF Token Mismatch | Refresh page |
32
+ | 422 | Validation Error | Display in form fields |
33
+ | 429 | Too Many Requests | Show rate limit message |
34
+ | 500+ | Server Error | Show server error message |
35
+
36
+ ---
37
+
38
+ ## API Response Types
39
+
40
+ ### Laravel Pagination Response
41
+
42
+ ```typescript
43
+ interface PaginatedResponse<T> {
44
+ data: T[];
45
+ links: {
46
+ first: string | null;
47
+ last: string | null;
48
+ prev: string | null;
49
+ next: string | null;
50
+ };
51
+ meta: {
52
+ current_page: number;
53
+ from: number | null;
54
+ last_page: number;
55
+ per_page: number;
56
+ to: number | null;
57
+ total: number;
58
+ };
59
+ }
60
+ ```
61
+
62
+ ### Laravel API Resource Response (single item)
63
+
64
+ ```typescript
65
+ interface ResourceResponse<T> {
66
+ data: T;
67
+ }
68
+ ```
69
+
70
+ ### Laravel Validation Error Response (422)
71
+
72
+ ```typescript
73
+ interface ValidationErrorResponse {
74
+ message: string;
75
+ errors: Record<string, string[]>;
76
+ }
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Handling Laravel Responses
82
+
83
+ ### In Service Layer
84
+
85
+ ```typescript
86
+ const userService = {
87
+ // Paginated list
88
+ list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
89
+ const { data } = await api.get("/api/users", { params });
90
+ return data; // Already typed as PaginatedResponse
91
+ },
92
+
93
+ // Single resource (handle { data: ... } wrapper)
94
+ get: async (id: number): Promise<User> => {
95
+ const { data } = await api.get(`/api/users/${id}`);
96
+ return data.data ?? data; // Handle both wrapped and unwrapped
97
+ },
98
+ };
99
+ ```
100
+
101
+ ### In Components (Form Validation)
102
+
103
+ ```typescript
104
+ import { getFormErrors } from "@/lib/api";
105
+
106
+ const mutation = useMutation({
107
+ mutationFn: userService.create,
108
+ onError: (error) => {
109
+ // Transform Laravel 422 errors to Ant Design format
110
+ form.setFields(getFormErrors(error));
111
+ },
112
+ });
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Axios Instance Configuration
118
+
119
+ The `lib/api.ts` is pre-configured for Laravel Sanctum:
120
+
121
+ ```typescript
122
+ const api = axios.create({
123
+ baseURL: process.env.NEXT_PUBLIC_API_URL,
124
+ timeout: 30000,
125
+ headers: {
126
+ "Content-Type": "application/json",
127
+ Accept: "application/json",
128
+ },
129
+ withCredentials: true, // Required for Sanctum cookies
130
+ withXSRFToken: true, // Auto send XSRF-TOKEN cookie as header
131
+ xsrfCookieName: "XSRF-TOKEN",
132
+ xsrfHeaderName: "X-XSRF-TOKEN",
133
+ });
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Common Patterns
139
+
140
+ ### Login Flow
141
+
142
+ ```typescript
143
+ import { csrf } from "@/lib/api";
144
+
145
+ const login = async (email: string, password: string) => {
146
+ // 1. Get CSRF cookie first
147
+ await csrf();
148
+
149
+ // 2. Login
150
+ await api.post("/login", { email, password });
151
+
152
+ // 3. Get user data
153
+ const { data } = await api.get("/api/user");
154
+ return data;
155
+ };
156
+ ```
157
+
158
+ ### Protected API Call
159
+
160
+ ```typescript
161
+ // No special handling needed - cookies are sent automatically
162
+ const users = await userService.list();
163
+ ```
164
+
165
+ ### Handling 401 (Unauthenticated)
166
+
167
+ The interceptor in `lib/api.ts` automatically redirects to `/login` on 401:
168
+
169
+ ```typescript
170
+ api.interceptors.response.use(
171
+ (response) => response,
172
+ (error) => {
173
+ if (error.response?.status === 401) {
174
+ if (!window.location.pathname.includes("/login")) {
175
+ window.location.href = "/login";
176
+ }
177
+ }
178
+ return Promise.reject(error);
179
+ }
180
+ );
181
+ ```
@@ -0,0 +1,180 @@
1
+ # Service Layer Pattern
2
+
3
+ > **Related:** [README](./README.md) | [TanStack Query](./tanstack-query.md) | [Laravel Integration](./laravel-integration.md)
4
+
5
+ ## Types Rule
6
+
7
+ ```typescript
8
+ // ✅ DO: Import Model + Create/Update types from Omnify
9
+ import type { User, UserCreate, UserUpdate } from "@/types/model";
10
+
11
+ // ✅ DO: Only define query params locally (not generated by Omnify)
12
+ export interface UserListParams {
13
+ search?: string;
14
+ page?: number;
15
+ }
16
+
17
+ // ❌ DON'T: Define types that Omnify already generates
18
+ export interface UserCreateInput { ... } // WRONG - use UserCreate from Omnify
19
+ export interface User { ... } // WRONG - use User from Omnify
20
+ ```
21
+
22
+ ---
23
+
24
+ ## Service Template
25
+
26
+ ```typescript
27
+ /**
28
+ * Users Service
29
+ *
30
+ * CRUD operations for User resource.
31
+ * Maps to Laravel UserController.
32
+ */
33
+
34
+ import api, { PaginatedResponse } from "@/lib/api";
35
+ import type { User, UserCreate, UserUpdate } from "@/types/model";
36
+
37
+ // =============================================================================
38
+ // Types - Only query params (Create/Update come from Omnify)
39
+ // =============================================================================
40
+
41
+ /** Query params for listing users */
42
+ export interface UserListParams {
43
+ search?: string;
44
+ page?: number;
45
+ per_page?: number;
46
+ sort_by?: keyof User;
47
+ sort_order?: "asc" | "desc";
48
+ }
49
+
50
+ // =============================================================================
51
+ // Service
52
+ // =============================================================================
53
+
54
+ const BASE_URL = "/api/users";
55
+
56
+ export const userService = {
57
+ /**
58
+ * Get paginated list of users
59
+ * GET /api/users
60
+ */
61
+ list: async (params?: UserListParams): Promise<PaginatedResponse<User>> => {
62
+ const { data } = await api.get(BASE_URL, { params });
63
+ return data;
64
+ },
65
+
66
+ /**
67
+ * Get single user by ID
68
+ * GET /api/users/:id
69
+ */
70
+ get: async (id: number): Promise<User> => {
71
+ const { data } = await api.get(`${BASE_URL}/${id}`);
72
+ return data.data ?? data;
73
+ },
74
+
75
+ /**
76
+ * Create new user
77
+ * POST /api/users
78
+ */
79
+ create: async (input: UserCreate): Promise<User> => {
80
+ const { data } = await api.post(BASE_URL, input);
81
+ return data.data ?? data;
82
+ },
83
+
84
+ /**
85
+ * Update existing user
86
+ * PUT /api/users/:id
87
+ */
88
+ update: async (id: number, input: UserUpdate): Promise<User> => {
89
+ const { data } = await api.put(`${BASE_URL}/${id}`, input);
90
+ return data.data ?? data;
91
+ },
92
+
93
+ /**
94
+ * Delete user
95
+ * DELETE /api/users/:id
96
+ */
97
+ delete: async (id: number): Promise<void> => {
98
+ await api.delete(`${BASE_URL}/${id}`);
99
+ },
100
+ };
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Service Rules
106
+
107
+ ```typescript
108
+ // ✅ DO: Import all types from Omnify
109
+ import type { User, UserCreate, UserUpdate } from "@/types/model";
110
+
111
+ // ❌ DON'T: Define types that Omnify generates
112
+ export interface UserCreate { ... } // WRONG!
113
+ export interface User { ... } // WRONG!
114
+
115
+ // ✅ DO: Only define query params locally
116
+ export interface UserListParams { ... } // OK - not in Omnify
117
+
118
+ // ✅ DO: Keep services pure (no React hooks)
119
+ export const userService = {
120
+ get: (id) => api.get(`/api/users/${id}`).then(r => r.data.data ?? r.data),
121
+ };
122
+
123
+ // ❌ DON'T: Use React hooks in services
124
+ export const userService = {
125
+ get: (id) => {
126
+ const [data, setData] = useState(); // WRONG!
127
+ },
128
+ };
129
+
130
+ // ✅ DO: Handle Laravel's { data: ... } wrapper
131
+ get: (id) => api.get(`/api/users/${id}`).then(r => r.data.data ?? r.data),
132
+
133
+ // ❌ DON'T: Return raw axios response
134
+ get: (id) => api.get(`/api/users/${id}`), // Returns AxiosResponse, not data
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Using Validation Rules
140
+
141
+ ```typescript
142
+ import { Form, Input } from "antd";
143
+ import { useLocale } from "next-intl";
144
+ import { userSchemas, getUserFieldLabel } from "@/types/model/User";
145
+ import { zodRule } from "@/lib/form-validation";
146
+
147
+ function UserForm() {
148
+ const locale = useLocale();
149
+ const label = (key: string) => getUserFieldLabel(key, locale);
150
+
151
+ return (
152
+ <Form>
153
+ {/* Name */}
154
+ <Form.Item
155
+ name="name"
156
+ label={label("name")}
157
+ rules={[zodRule(userSchemas.name, label("name"))]}
158
+ >
159
+ <Input />
160
+ </Form.Item>
161
+ </Form>
162
+ );
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Types Summary
169
+
170
+ | Type | Source | Example |
171
+ | -------- | ---------------------------- | ------------------------------------------ |
172
+ | Model | `@/types/model` (Omnify) | `User` |
173
+ | Create | `@/types/model` (Omnify) | `UserCreate` |
174
+ | Update | `@/types/model` (Omnify) | `UserUpdate` |
175
+ | Schemas | `@/types/model` (Omnify) | `userSchemas.email` |
176
+ | Params | Service file (manual) | `UserListParams` |
177
+ | Response | `@/lib/api.ts` | `PaginatedResponse<T>` |
178
+ | Rules | `@/lib/form-validation` | `zodRule(schema, label)` |
179
+
180
+ See [Types Guide](./types-guide.md) for complete details.