i18nsmith 0.4.3 → 0.6.0

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.
Files changed (42) hide show
  1. package/README.md +126 -0
  2. package/build.mjs +5 -0
  3. package/dist/commands/backup.d.ts.map +1 -1
  4. package/dist/commands/check.d.ts.map +1 -1
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/coverage.d.ts.map +1 -1
  7. package/dist/commands/detect.d.ts.map +1 -1
  8. package/dist/commands/diagnose.d.ts.map +1 -1
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/rename.d.ts.map +1 -1
  11. package/dist/commands/review.d.ts.map +1 -1
  12. package/dist/commands/scaffold-adapter.d.ts.map +1 -1
  13. package/dist/commands/scan.d.ts.map +1 -1
  14. package/dist/commands/sync.d.ts.map +1 -1
  15. package/dist/commands/transform.d.ts.map +1 -1
  16. package/dist/index.cjs +13069 -6794
  17. package/dist/services/index.d.ts +12 -0
  18. package/dist/services/index.d.ts.map +1 -0
  19. package/dist/services/transform-service.d.ts +64 -0
  20. package/dist/services/transform-service.d.ts.map +1 -0
  21. package/dist/utils/bootstrap.d.ts +83 -0
  22. package/dist/utils/bootstrap.d.ts.map +1 -0
  23. package/package.json +1 -1
  24. package/src/commands/backup.ts +28 -10
  25. package/src/commands/check.ts +16 -4
  26. package/src/commands/config.ts +126 -0
  27. package/src/commands/coverage.ts +11 -4
  28. package/src/commands/detect.ts +11 -4
  29. package/src/commands/diagnose.ts +12 -2
  30. package/src/commands/init.test.ts +80 -0
  31. package/src/commands/init.ts +92 -49
  32. package/src/commands/rename.ts +24 -5
  33. package/src/commands/review.ts +10 -3
  34. package/src/commands/scaffold-adapter.ts +11 -2
  35. package/src/commands/scan.ts +20 -7
  36. package/src/commands/sync.ts +91 -23
  37. package/src/commands/transform.ts +8 -1
  38. package/src/index.ts +1 -1
  39. package/src/integration.test.ts +145 -12
  40. package/src/services/index.ts +12 -0
  41. package/src/services/transform-service.ts +203 -0
  42. package/src/utils/bootstrap.ts +221 -0
@@ -0,0 +1,203 @@
1
+ /**
2
+ * @fileoverview
3
+ * CLI Transform Service - Implements ITransformService for CLI usage
4
+ *
5
+ * This service wraps @i18nsmith/transformer's Transformer class and provides
6
+ * the ITransformService interface. It lives in CLI because:
7
+ * - @i18nsmith/core should NOT depend on @i18nsmith/transformer
8
+ * - CLI depends on both packages, making this the natural location
9
+ *
10
+ * @module @i18nsmith/cli/services/transform-service
11
+ */
12
+
13
+ import { Transformer } from '@i18nsmith/transformer';
14
+ import type { TransformSummary, TransformRunOptions as TransformerRunOptions } from '@i18nsmith/transformer';
15
+ import {
16
+ type I18nConfig,
17
+ type ITransformService,
18
+ type TransformOptions,
19
+ type TransformSummaryBase,
20
+ type ScanCandidate,
21
+ type Result,
22
+ type CoreError,
23
+ success,
24
+ failure,
25
+ } from '@i18nsmith/core';
26
+
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+ // Types
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Configuration for the CLI transform service.
33
+ */
34
+ export interface CliTransformServiceConfig {
35
+ /** Workspace root directory */
36
+ readonly workspaceRoot: string;
37
+ }
38
+
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+ // Helper Functions
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * Converts a TransformSummary to TransformSummaryBase.
45
+ * Maps the full candidate objects to the base type and includes optional display fields.
46
+ */
47
+ function toSummaryBase(summary: TransformSummary): TransformSummaryBase {
48
+ return {
49
+ filesScanned: summary.filesScanned,
50
+ filesChanged: summary.filesChanged,
51
+ candidates: summary.candidates.map(c => ({
52
+ filePath: c.filePath,
53
+ position: c.position,
54
+ text: c.text,
55
+ kind: c.kind,
56
+ suggestedKey: c.suggestedKey,
57
+ status: c.status,
58
+ reason: c.reason,
59
+ })),
60
+ candidateStats: summary.candidateStats,
61
+ write: summary.write,
62
+ localeStats: summary.localeStats?.map(s => ({
63
+ locale: s.locale,
64
+ added: s.added,
65
+ updated: s.updated,
66
+ removed: s.removed,
67
+ totalKeys: s.totalKeys,
68
+ })),
69
+ skippedFiles: summary.skippedFiles,
70
+ skippedReasons: summary.skippedReasons,
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Creates a CoreError for transform operations.
76
+ */
77
+ function createTransformError(message: string, code: string = 'TRANSFORM_ERROR'): CoreError {
78
+ return {
79
+ code,
80
+ message,
81
+ category: 'internal',
82
+ severity: 'error',
83
+ };
84
+ }
85
+
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+ // Service Implementation
88
+ // ─────────────────────────────────────────────────────────────────────────────
89
+
90
+ /**
91
+ * CLI implementation of ITransformService.
92
+ *
93
+ * Wraps the @i18nsmith/transformer Transformer class with the standard
94
+ * Result-based error handling pattern used across the service layer.
95
+ */
96
+ export class CliTransformService implements ITransformService {
97
+ private readonly workspaceRoot: string;
98
+
99
+ constructor(config: CliTransformServiceConfig) {
100
+ this.workspaceRoot = config.workspaceRoot;
101
+ }
102
+
103
+ /**
104
+ * Transforms source files to use translation functions.
105
+ */
106
+ async transform(
107
+ config: I18nConfig,
108
+ options?: TransformOptions
109
+ ): Promise<Result<TransformSummaryBase>> {
110
+ try {
111
+ const transformer = new Transformer(config, {
112
+ workspaceRoot: options?.workspaceRoot ?? this.workspaceRoot,
113
+ keyNamespace: options?.keyNamespace,
114
+ });
115
+
116
+ const runOptions: TransformerRunOptions = {
117
+ write: options?.write ?? false,
118
+ targets: options?.targets ? [...options.targets] : undefined,
119
+ diff: options?.diff ?? false,
120
+ migrateTextKeys: options?.migrateTextKeys ?? false,
121
+ onProgress: options?.onProgress,
122
+ };
123
+
124
+ const summary = await transformer.run(runOptions);
125
+ return success(toSummaryBase(summary));
126
+ } catch (error) {
127
+ const message = error instanceof Error ? error.message : String(error);
128
+ return failure(createTransformError(`Transform failed: ${message}`));
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Transforms specific candidates (from a previous scan).
134
+ *
135
+ * Note: The current Transformer doesn't have a direct method for this,
136
+ * so we run a full transform with targets limited to the candidate files.
137
+ */
138
+ async transformCandidates(
139
+ config: I18nConfig,
140
+ candidates: readonly ScanCandidate[],
141
+ options?: TransformOptions
142
+ ): Promise<Result<TransformSummaryBase>> {
143
+ try {
144
+ // Extract unique file paths from candidates
145
+ const uniqueFiles = [...new Set(candidates.map(c => c.filePath))];
146
+
147
+ const transformer = new Transformer(config, {
148
+ workspaceRoot: options?.workspaceRoot ?? this.workspaceRoot,
149
+ keyNamespace: options?.keyNamespace,
150
+ });
151
+
152
+ const runOptions: TransformerRunOptions = {
153
+ write: options?.write ?? false,
154
+ targets: uniqueFiles,
155
+ diff: options?.diff ?? false,
156
+ migrateTextKeys: options?.migrateTextKeys ?? false,
157
+ onProgress: options?.onProgress,
158
+ };
159
+
160
+ const summary = await transformer.run(runOptions);
161
+ return success(toSummaryBase(summary));
162
+ } catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ return failure(createTransformError(`Transform candidates failed: ${message}`));
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Generates a preview of transformation without applying.
170
+ */
171
+ async preview(
172
+ config: I18nConfig,
173
+ options?: Omit<TransformOptions, 'write'>
174
+ ): Promise<Result<TransformSummaryBase>> {
175
+ return this.transform(config, { ...options, write: false });
176
+ }
177
+ }
178
+
179
+ // ─────────────────────────────────────────────────────────────────────────────
180
+ // Factory Function
181
+ // ─────────────────────────────────────────────────────────────────────────────
182
+
183
+ /**
184
+ * Creates a CLI transform service instance.
185
+ *
186
+ * @param workspaceRoot - The workspace root directory
187
+ * @returns ITransformService instance
188
+ *
189
+ * @example
190
+ * ```typescript
191
+ * import { getTransformService } from '../services/transform-service.js';
192
+ *
193
+ * const transformService = getTransformService(projectRoot);
194
+ * const result = await transformService.transform(config, { write: true });
195
+ *
196
+ * if (!result.success) {
197
+ * throw new CliError(result.error.message);
198
+ * }
199
+ * ```
200
+ */
201
+ export function getTransformService(workspaceRoot: string): ITransformService {
202
+ return new CliTransformService({ workspaceRoot });
203
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * @fileoverview
3
+ * CLI Bootstrap - Service Container Setup for CLI
4
+ *
5
+ * This module provides a pre-configured ServiceContainer with Node.js
6
+ * platform adapters. CLI commands can import this to use the service layer.
7
+ *
8
+ * Following Hexagonal Architecture:
9
+ * - The ServiceContainer is the "Application Layer"
10
+ * - This bootstrap module configures "Adapters" for CLI (Node.js) environment
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { getServiceContainer } from '../utils/bootstrap.js';
15
+ *
16
+ * async function myCommand() {
17
+ * const container = getServiceContainer();
18
+ * const result = await container.scanner.scan(config);
19
+ * // ...
20
+ * }
21
+ * ```
22
+ *
23
+ * @module @i18nsmith/cli/utils/bootstrap
24
+ */
25
+
26
+ import chalk from 'chalk';
27
+ import {
28
+ ServiceContainer,
29
+ createServiceContainer,
30
+ NodeFileSystem,
31
+ NodeLoggerFactory,
32
+ type ServiceContainerConfig,
33
+ type PlatformAdapters,
34
+ } from '@i18nsmith/core';
35
+ import type { ILogger, LogLevel, ILoggerFactory } from '@i18nsmith/core';
36
+
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+ // CLI Logger Adapter
39
+ // ─────────────────────────────────────────────────────────────────────────────
40
+
41
+ /**
42
+ * CLI-specific logger that outputs with chalk formatting.
43
+ */
44
+ class CliLogger implements ILogger {
45
+ private currentLevel: LogLevel;
46
+ private readonly scope: string;
47
+ private readonly levelOrder: Record<LogLevel, number> = {
48
+ trace: 0,
49
+ debug: 1,
50
+ info: 2,
51
+ warn: 3,
52
+ error: 4,
53
+ };
54
+
55
+ constructor(scope: string = 'CLI', level: LogLevel = 'info') {
56
+ this.scope = scope;
57
+ this.currentLevel = level;
58
+ }
59
+
60
+ private shouldLog(level: LogLevel): boolean {
61
+ return this.levelOrder[level] >= this.levelOrder[this.currentLevel];
62
+ }
63
+
64
+ trace(message: string, data?: Record<string, unknown>): void {
65
+ if (this.shouldLog('trace')) {
66
+ console.log(chalk.gray(`[${this.scope}] ${message}`), data ? data : '');
67
+ }
68
+ }
69
+
70
+ debug(message: string, data?: Record<string, unknown>): void {
71
+ if (this.shouldLog('debug')) {
72
+ console.log(chalk.gray(`[${this.scope}] ${message}`), data ? data : '');
73
+ }
74
+ }
75
+
76
+ info(message: string, data?: Record<string, unknown>): void {
77
+ if (this.shouldLog('info')) {
78
+ console.log(chalk.blue(`[${this.scope}] ${message}`), data ? data : '');
79
+ }
80
+ }
81
+
82
+ warn(message: string, data?: Record<string, unknown>): void {
83
+ if (this.shouldLog('warn')) {
84
+ console.warn(chalk.yellow(`[${this.scope}] ⚠ ${message}`), data ? data : '');
85
+ }
86
+ }
87
+
88
+ error(message: string, error?: Error, data?: Record<string, unknown>): void {
89
+ if (this.shouldLog('error')) {
90
+ console.error(chalk.red(`[${this.scope}] ✖ ${message}`));
91
+ if (error?.stack) {
92
+ console.error(chalk.gray(error.stack));
93
+ }
94
+ if (data) {
95
+ console.error(chalk.gray(JSON.stringify(data, null, 2)));
96
+ }
97
+ }
98
+ }
99
+
100
+ child(scope: string): ILogger {
101
+ return new CliLogger(`${this.scope}:${scope}`, this.currentLevel);
102
+ }
103
+
104
+ getLevel(): LogLevel {
105
+ return this.currentLevel;
106
+ }
107
+
108
+ setLevel(level: LogLevel): void {
109
+ this.currentLevel = level;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Factory for creating CLI loggers.
115
+ */
116
+ class CliLoggerFactory implements ILoggerFactory {
117
+ private readonly level: LogLevel;
118
+ private readonly rootLogger: CliLogger;
119
+
120
+ constructor(level: LogLevel = 'info') {
121
+ this.level = level;
122
+ this.rootLogger = new CliLogger('CLI', level);
123
+ }
124
+
125
+ createLogger(scope: string): ILogger {
126
+ return new CliLogger(scope, this.level);
127
+ }
128
+
129
+ getRootLogger(): ILogger {
130
+ return this.rootLogger;
131
+ }
132
+
133
+ setGlobalLevel(level: LogLevel): void {
134
+ this.rootLogger.setLevel(level);
135
+ }
136
+ }
137
+
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+ // Service Container Management
140
+ // ─────────────────────────────────────────────────────────────────────────────
141
+
142
+ let globalContainer: ServiceContainer | null = null;
143
+ let globalLogLevel: LogLevel = 'warn'; // CLI defaults to quiet
144
+
145
+ /**
146
+ * Sets the global log level for CLI operations.
147
+ */
148
+ export function setLogLevel(level: LogLevel): void {
149
+ globalLogLevel = level;
150
+ if (globalContainer) {
151
+ // Reset container to re-create with new log level
152
+ globalContainer = null;
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Creates platform adapters for CLI environment.
158
+ */
159
+ function createCliAdapters(): PlatformAdapters {
160
+ return {
161
+ fileSystem: new NodeFileSystem(),
162
+ loggerFactory: new CliLoggerFactory(globalLogLevel),
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Gets the global service container, creating it if needed.
168
+ *
169
+ * @param options - Optional configuration overrides
170
+ * @returns Configured ServiceContainer
171
+ */
172
+ export function getServiceContainer(options?: {
173
+ workspaceRoot?: string;
174
+ logLevel?: LogLevel;
175
+ }): ServiceContainer {
176
+ if (options?.logLevel) {
177
+ globalLogLevel = options.logLevel;
178
+ }
179
+
180
+ const workspaceRoot = options?.workspaceRoot ?? process.cwd();
181
+
182
+ // Create new container if needed or if workspaceRoot changed
183
+ if (!globalContainer || globalContainer.workspaceRoot !== workspaceRoot) {
184
+ globalContainer = createServiceContainer({
185
+ workspaceRoot,
186
+ adapters: createCliAdapters(),
187
+ });
188
+ }
189
+
190
+ return globalContainer;
191
+ }
192
+
193
+ /**
194
+ * Creates a fresh service container (useful for testing or scoped operations).
195
+ *
196
+ * @param config - Container configuration
197
+ * @returns New ServiceContainer instance
198
+ */
199
+ export function createCliServiceContainer(config?: ServiceContainerConfig): ServiceContainer {
200
+ return createServiceContainer({
201
+ workspaceRoot: config?.workspaceRoot ?? process.cwd(),
202
+ adapters: {
203
+ ...createCliAdapters(),
204
+ ...config?.adapters,
205
+ },
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Resets the global container (useful for testing).
211
+ */
212
+ export function resetServiceContainer(): void {
213
+ globalContainer = null;
214
+ }
215
+
216
+ // ─────────────────────────────────────────────────────────────────────────────
217
+ // Utility Exports
218
+ // ─────────────────────────────────────────────────────────────────────────────
219
+
220
+ export { CliLogger, CliLoggerFactory };
221
+ export type { LogLevel };