localingos 0.1.2 → 0.1.4
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.
- package/README.md +32 -3
- package/bin/localingos.js +10 -0
- package/package.json +1 -1
- package/src/commands/extract.js +205 -0
- package/src/commands/init.js +59 -12
- package/src/commands/pull.js +3 -2
- package/src/commands/push.js +18 -2
- package/src/commands/sync.js +18 -2
- package/src/config.js +22 -1
package/README.md
CHANGED
|
@@ -29,7 +29,6 @@ This will interactively create a `.localingos.json` config file:
|
|
|
29
29
|
|
|
30
30
|
```json
|
|
31
31
|
{
|
|
32
|
-
"apiUrl": "https://api.localingos.com",
|
|
33
32
|
"apiKey": "your-api-key",
|
|
34
33
|
"familyId": "your-family-id",
|
|
35
34
|
"sourceLocale": "en-US",
|
|
@@ -42,7 +41,26 @@ This will interactively create a `.localingos.json` config file:
|
|
|
42
41
|
|
|
43
42
|
> ⚠️ Add `.localingos.json` to your `.gitignore` — it contains your API key.
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
If the source file doesn't exist yet, `init` will offer you three options:
|
|
45
|
+
- **🤖 Generate an AI prompt** to automatically extract all strings from your codebase
|
|
46
|
+
- **📝 Create a sample file** with hello-world entries to try the flow
|
|
47
|
+
- **⏭️ Skip** and create it yourself
|
|
48
|
+
|
|
49
|
+
### 3. Extract translatable strings (new projects)
|
|
50
|
+
|
|
51
|
+
If you're internationalizing an existing codebase, use:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
localingos extract
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This detects your project type (React, Next.js, Vue, Angular, etc.) and generates a tailored AI prompt that you paste into your AI assistant (Cline, Cursor, Copilot, ChatGPT). The AI will:
|
|
58
|
+
1. Scan your codebase for all user-facing strings
|
|
59
|
+
2. Create the source locale file (e.g., `en-US.json`)
|
|
60
|
+
3. Replace hardcoded strings with `t('key')` calls
|
|
61
|
+
4. Set up an i18n library if needed
|
|
62
|
+
|
|
63
|
+
### 4. Sync translations
|
|
46
64
|
|
|
47
65
|
```bash
|
|
48
66
|
localingos sync
|
|
@@ -60,6 +78,14 @@ This single command will:
|
|
|
60
78
|
|
|
61
79
|
Interactive setup wizard. Creates `.localingos.json` in the current directory.
|
|
62
80
|
|
|
81
|
+
### `localingos extract`
|
|
82
|
+
|
|
83
|
+
Generates an AI prompt to extract all translatable strings from your codebase.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
localingos extract # Detects project type, generates prompt, copies to clipboard
|
|
87
|
+
```
|
|
88
|
+
|
|
63
89
|
### `localingos sync`
|
|
64
90
|
|
|
65
91
|
The main command. Pushes source strings and pulls translations in one call.
|
|
@@ -91,10 +117,13 @@ localingos pull # Pull and write translation files
|
|
|
91
117
|
| Option | Description |
|
|
92
118
|
|--------|-------------|
|
|
93
119
|
| `-c, --config <path>` | Path to config file (default: `.localingos.json`) |
|
|
120
|
+
| `--env <environment>` | API environment: `prod`, `gamma`, `local` (default: `prod`) |
|
|
94
121
|
| `--dry-run` | Preview without making API calls (sync/push) |
|
|
95
122
|
| `-V, --version` | Show version |
|
|
96
123
|
| `-h, --help` | Show help |
|
|
97
124
|
|
|
125
|
+
The `--env` flag is for internal development. You can also set the `LOCALINGOS_ENV` environment variable.
|
|
126
|
+
|
|
98
127
|
## Supported Formats
|
|
99
128
|
|
|
100
129
|
| Format | Description | Example |
|
|
@@ -173,4 +202,4 @@ git push && git push --tags
|
|
|
173
202
|
|---------|------------|---------|
|
|
174
203
|
| `npm version patch` | Bug fix, typo, minor tweak | 0.1.0 → 0.1.1 |
|
|
175
204
|
| `npm version minor` | New feature, new format support | 0.1.0 → 0.2.0 |
|
|
176
|
-
| `npm version major` | Breaking config/API change | 0.1.0 → 1.0.0 |
|
|
205
|
+
| `npm version major` | Breaking config/API change | 0.1.0 → 1.0.0 |
|
package/bin/localingos.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initCommand } from '../src/commands/init.js';
|
|
|
5
5
|
import { syncCommand } from '../src/commands/sync.js';
|
|
6
6
|
import { pullCommand } from '../src/commands/pull.js';
|
|
7
7
|
import { pushCommand } from '../src/commands/push.js';
|
|
8
|
+
import { extractCommand } from '../src/commands/extract.js';
|
|
8
9
|
|
|
9
10
|
program
|
|
10
11
|
.name('localingos')
|
|
@@ -20,6 +21,7 @@ program
|
|
|
20
21
|
.command('sync')
|
|
21
22
|
.description('Extract source strings, push to Localingos, and pull all available translations')
|
|
22
23
|
.option('-c, --config <path>', 'Path to config file', '.localingos.json')
|
|
24
|
+
.option('--env <environment>', 'API environment: prod, gamma, local (default: prod)')
|
|
23
25
|
.option('--dry-run', 'Show what would be synced without making API calls')
|
|
24
26
|
.action(syncCommand);
|
|
25
27
|
|
|
@@ -27,13 +29,21 @@ program
|
|
|
27
29
|
.command('pull')
|
|
28
30
|
.description('Pull all available translations from Localingos and write locale files')
|
|
29
31
|
.option('-c, --config <path>', 'Path to config file', '.localingos.json')
|
|
32
|
+
.option('--env <environment>', 'API environment: prod, gamma, local (default: prod)')
|
|
30
33
|
.action(pullCommand);
|
|
31
34
|
|
|
32
35
|
program
|
|
33
36
|
.command('push')
|
|
34
37
|
.description('Extract and push source strings to Localingos without pulling translations')
|
|
35
38
|
.option('-c, --config <path>', 'Path to config file', '.localingos.json')
|
|
39
|
+
.option('--env <environment>', 'API environment: prod, gamma, local (default: prod)')
|
|
36
40
|
.option('--dry-run', 'Show what would be pushed without making API calls')
|
|
37
41
|
.action(pushCommand);
|
|
38
42
|
|
|
43
|
+
program
|
|
44
|
+
.command('extract')
|
|
45
|
+
.description('Generate an AI prompt to extract all translatable strings from your codebase')
|
|
46
|
+
.option('-c, --config <path>', 'Path to config file', '.localingos.json')
|
|
47
|
+
.action(extractCommand);
|
|
48
|
+
|
|
39
49
|
program.parse();
|
package/package.json
CHANGED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect project type by scanning for common files.
|
|
8
|
+
*/
|
|
9
|
+
function detectProjectType() {
|
|
10
|
+
const indicators = {
|
|
11
|
+
react: ['package.json'],
|
|
12
|
+
nextjs: ['next.config.js', 'next.config.mjs', 'next.config.ts'],
|
|
13
|
+
vue: ['vue.config.js', 'nuxt.config.js', 'nuxt.config.ts'],
|
|
14
|
+
angular: ['angular.json'],
|
|
15
|
+
svelte: ['svelte.config.js'],
|
|
16
|
+
flutter: ['pubspec.yaml'],
|
|
17
|
+
ios: ['Package.swift', 'Podfile'],
|
|
18
|
+
android: ['build.gradle', 'build.gradle.kts'],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const detected = [];
|
|
22
|
+
for (const [type, files] of Object.entries(indicators)) {
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
if (fs.existsSync(path.resolve(file))) {
|
|
25
|
+
detected.push(type);
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check package.json dependencies for more specificity
|
|
32
|
+
const pkgPath = path.resolve('package.json');
|
|
33
|
+
if (fs.existsSync(pkgPath)) {
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
36
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
37
|
+
if (allDeps['next']) detected.push('nextjs');
|
|
38
|
+
if (allDeps['react']) detected.push('react');
|
|
39
|
+
if (allDeps['vue']) detected.push('vue');
|
|
40
|
+
if (allDeps['@angular/core']) detected.push('angular');
|
|
41
|
+
if (allDeps['svelte']) detected.push('svelte');
|
|
42
|
+
if (allDeps['react-i18next']) detected.push('react-i18next');
|
|
43
|
+
if (allDeps['next-intl']) detected.push('next-intl');
|
|
44
|
+
if (allDeps['vue-i18n']) detected.push('vue-i18n');
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// Ignore parse errors
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return [...new Set(detected)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getI18nLibrarySuggestion(projectTypes) {
|
|
54
|
+
if (projectTypes.includes('react-i18next') || projectTypes.includes('next-intl') || projectTypes.includes('vue-i18n')) {
|
|
55
|
+
return null; // Already has one
|
|
56
|
+
}
|
|
57
|
+
if (projectTypes.includes('nextjs')) return 'next-intl';
|
|
58
|
+
if (projectTypes.includes('react')) return 'react-i18next';
|
|
59
|
+
if (projectTypes.includes('vue')) return 'vue-i18n';
|
|
60
|
+
if (projectTypes.includes('angular')) return '@ngx-translate/core';
|
|
61
|
+
if (projectTypes.includes('svelte')) return 'svelte-i18n';
|
|
62
|
+
return 'i18next'; // Generic fallback
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFileExtensions(projectTypes) {
|
|
66
|
+
const exts = ['.js', '.jsx', '.ts', '.tsx'];
|
|
67
|
+
if (projectTypes.includes('vue')) exts.push('.vue');
|
|
68
|
+
if (projectTypes.includes('svelte')) exts.push('.svelte');
|
|
69
|
+
if (projectTypes.includes('angular')) exts.push('.html');
|
|
70
|
+
return exts;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate the extraction prompt. Exported so init.js can call it too.
|
|
75
|
+
*/
|
|
76
|
+
export function generateExtractPrompt(config) {
|
|
77
|
+
const projectTypes = detectProjectType();
|
|
78
|
+
const i18nLib = getI18nLibrarySuggestion(projectTypes);
|
|
79
|
+
const fileExts = getFileExtensions(projectTypes);
|
|
80
|
+
const format = config.format || 'json-nested';
|
|
81
|
+
const sourceFile = config.sourceFile || './src/i18n/en-US.json';
|
|
82
|
+
const outputDir = config.outputDir || './src/i18n';
|
|
83
|
+
|
|
84
|
+
const formatExample = format === 'json-flat'
|
|
85
|
+
? `{
|
|
86
|
+
"page_name.section.element": "English text here"
|
|
87
|
+
}`
|
|
88
|
+
: `{
|
|
89
|
+
"page_name": {
|
|
90
|
+
"section": {
|
|
91
|
+
"element": "English text here"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}`;
|
|
95
|
+
|
|
96
|
+
const prompt = `## Task: Extract all translatable strings from this project and set up i18n
|
|
97
|
+
|
|
98
|
+
Scan this codebase and extract every user-facing string into an i18n source file.
|
|
99
|
+
|
|
100
|
+
### What to extract:
|
|
101
|
+
- All visible text content in UI components (${fileExts.join(', ')} files)
|
|
102
|
+
- Button labels, link text, menu items
|
|
103
|
+
- Form labels, input placeholders, helper text
|
|
104
|
+
- Headings, titles, page descriptions
|
|
105
|
+
- Error messages, validation messages, toast/notification messages
|
|
106
|
+
- Modal titles and content
|
|
107
|
+
- Alt text for images
|
|
108
|
+
- Aria labels and accessibility text
|
|
109
|
+
- Static list items and table headers
|
|
110
|
+
- Tooltip text
|
|
111
|
+
|
|
112
|
+
### What NOT to extract:
|
|
113
|
+
- Code comments, console.log messages
|
|
114
|
+
- CSS class names, HTML attributes that aren't text
|
|
115
|
+
- Technical identifiers (URLs, API paths, query params)
|
|
116
|
+
- Constants that aren't user-visible
|
|
117
|
+
- Enum values used only programmatically
|
|
118
|
+
- Package names, library identifiers
|
|
119
|
+
|
|
120
|
+
### Output file:
|
|
121
|
+
Create \`${sourceFile}\` in this format:
|
|
122
|
+
|
|
123
|
+
\`\`\`json
|
|
124
|
+
${formatExample}
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
### Key naming convention:
|
|
128
|
+
- Use dot-separated paths: \`{page}.{section}.{element}\`
|
|
129
|
+
- Use lowercase snake_case for key segments
|
|
130
|
+
- Be descriptive: \`login.form.email_placeholder\` not \`text_1\`
|
|
131
|
+
- Group by page/feature first, then by section, then by element
|
|
132
|
+
- Use common prefixes for shared strings: \`common.buttons.save\`, \`common.buttons.cancel\`
|
|
133
|
+
|
|
134
|
+
### Source code changes:
|
|
135
|
+
${i18nLib ? `1. Install \`${i18nLib}\` as a dependency
|
|
136
|
+
2. Set it up to load translations from \`${outputDir}/{locale}.json\`
|
|
137
|
+
3. Replace` : `1. Using the existing i18n library in this project, replace`} every hardcoded string in the source code with the appropriate translation function call (e.g., \`t('key')\` or equivalent)
|
|
138
|
+
${i18nLib ? '4' : '2'}. Make sure imports are added to every file that uses translation calls
|
|
139
|
+
${i18nLib ? '5' : '3'}. Preserve any dynamic values using interpolation: \`t('greeting.welcome', { name: userName })\`
|
|
140
|
+
|
|
141
|
+
### Important:
|
|
142
|
+
- Extract ALL user-facing strings — do not leave any hardcoded text
|
|
143
|
+
- If a string contains dynamic values like \`\${name}\`, use interpolation: \`"Hello, {name}"\`
|
|
144
|
+
- Maintain the original string exactly as-is in the source file (English)
|
|
145
|
+
- After extraction, \`localingos sync\` will handle translating to other languages`;
|
|
146
|
+
|
|
147
|
+
// Print the prompt
|
|
148
|
+
console.log(chalk.blue('🤖 AI Extraction Prompt'));
|
|
149
|
+
console.log(chalk.blue('─'.repeat(60)));
|
|
150
|
+
console.log(chalk.gray('Copy the prompt below and paste it into your AI assistant'));
|
|
151
|
+
console.log(chalk.gray('(Cline, Cursor, Copilot, ChatGPT, etc.)'));
|
|
152
|
+
console.log(chalk.blue('─'.repeat(60)));
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(prompt);
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(chalk.blue('─'.repeat(60)));
|
|
157
|
+
|
|
158
|
+
// Detect project info
|
|
159
|
+
if (projectTypes.length > 0) {
|
|
160
|
+
console.log(chalk.gray(`\n📋 Detected project: ${projectTypes.join(', ')}`));
|
|
161
|
+
}
|
|
162
|
+
if (i18nLib) {
|
|
163
|
+
console.log(chalk.gray(`📦 Suggested i18n library: ${i18nLib}`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Try to copy to clipboard (macOS)
|
|
167
|
+
try {
|
|
168
|
+
execSync('command -v pbcopy', { stdio: 'ignore' });
|
|
169
|
+
execSync('echo ' + JSON.stringify(prompt) + ' | pbcopy', { stdio: 'ignore' });
|
|
170
|
+
console.log(chalk.green('\n✅ Prompt copied to clipboard!'));
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.log(chalk.yellow('\n💡 Copy the prompt above and paste it into your AI assistant.'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(chalk.blue(`\nAfter the AI extracts your strings, run "localingos sync" to push and translate.\n`));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* CLI command handler
|
|
180
|
+
*/
|
|
181
|
+
export async function extractCommand(options) {
|
|
182
|
+
let config;
|
|
183
|
+
const configPath = path.resolve(options.config || '.localingos.json');
|
|
184
|
+
|
|
185
|
+
if (fs.existsSync(configPath)) {
|
|
186
|
+
try {
|
|
187
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
188
|
+
} catch (e) {
|
|
189
|
+
config = {};
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
// No config file — use sensible defaults
|
|
193
|
+
config = {};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Fill in defaults for anything missing
|
|
197
|
+
config = {
|
|
198
|
+
format: config.format || 'json-nested',
|
|
199
|
+
sourceFile: config.sourceFile || './src/i18n/en-US.json',
|
|
200
|
+
outputDir: config.outputDir || './src/i18n',
|
|
201
|
+
...config
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
generateExtractPrompt(config);
|
|
205
|
+
}
|
package/src/commands/init.js
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
2
1
|
import chalk from 'chalk';
|
|
3
2
|
import fs from 'fs';
|
|
4
3
|
import path from 'path';
|
|
5
4
|
import { saveConfig } from '../config.js';
|
|
6
5
|
import { getSupportedFormats } from '../formats/index.js';
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const SAMPLE_SOURCE_FILES = {
|
|
8
|
+
'json-nested': {
|
|
9
|
+
greeting: {
|
|
10
|
+
hello: 'Hello',
|
|
11
|
+
welcome: 'Welcome to our app'
|
|
12
|
+
},
|
|
13
|
+
buttons: {
|
|
14
|
+
submit: 'Submit',
|
|
15
|
+
cancel: 'Cancel'
|
|
16
|
+
},
|
|
17
|
+
errors: {
|
|
18
|
+
required: 'This field is required',
|
|
19
|
+
invalid_email: 'Please enter a valid email address'
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
'json-flat': {
|
|
23
|
+
'greeting.hello': 'Hello',
|
|
24
|
+
'greeting.welcome': 'Welcome to our app',
|
|
25
|
+
'buttons.submit': 'Submit',
|
|
26
|
+
'buttons.cancel': 'Cancel',
|
|
27
|
+
'errors.required': 'This field is required',
|
|
28
|
+
'errors.invalid_email': 'Please enter a valid email address'
|
|
29
|
+
}
|
|
30
|
+
};
|
|
10
31
|
|
|
11
32
|
export async function initCommand() {
|
|
12
33
|
const inquirer = (await import('inquirer')).default;
|
|
@@ -28,12 +49,6 @@ export async function initCommand() {
|
|
|
28
49
|
console.log(chalk.blue('\n🌍 Localingos CLI Setup\n'));
|
|
29
50
|
|
|
30
51
|
const answers = await inquirer.prompt([
|
|
31
|
-
{
|
|
32
|
-
type: 'input',
|
|
33
|
-
name: 'apiUrl',
|
|
34
|
-
message: 'API URL:',
|
|
35
|
-
default: 'https://api.localingos.com'
|
|
36
|
-
},
|
|
37
52
|
{
|
|
38
53
|
type: 'input',
|
|
39
54
|
name: 'apiKey',
|
|
@@ -43,7 +58,7 @@ export async function initCommand() {
|
|
|
43
58
|
{
|
|
44
59
|
type: 'input',
|
|
45
60
|
name: 'familyId',
|
|
46
|
-
message: 'Family ID (
|
|
61
|
+
message: 'Family ID (copy from Families page → Family ID column):',
|
|
47
62
|
validate: (input) => input.trim().length > 0 || 'Family ID is required'
|
|
48
63
|
},
|
|
49
64
|
{
|
|
@@ -79,7 +94,6 @@ export async function initCommand() {
|
|
|
79
94
|
]);
|
|
80
95
|
|
|
81
96
|
const config = {
|
|
82
|
-
apiUrl: answers.apiUrl.trim(),
|
|
83
97
|
apiKey: answers.apiKey.trim(),
|
|
84
98
|
familyId: answers.familyId.trim(),
|
|
85
99
|
sourceLocale: answers.sourceLocale.trim(),
|
|
@@ -110,5 +124,38 @@ export async function initCommand() {
|
|
|
110
124
|
}
|
|
111
125
|
}
|
|
112
126
|
|
|
113
|
-
|
|
127
|
+
// Check if source file exists — offer to create sample or generate extraction prompt
|
|
128
|
+
const sourceFilePath = path.resolve(config.sourceFile);
|
|
129
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
130
|
+
console.log(chalk.yellow(`\n⚠️ Source file doesn't exist yet: ${config.sourceFile}`));
|
|
131
|
+
|
|
132
|
+
const { action } = await inquirer.prompt([{
|
|
133
|
+
type: 'list',
|
|
134
|
+
name: 'action',
|
|
135
|
+
message: 'How would you like to set up your source strings?',
|
|
136
|
+
choices: [
|
|
137
|
+
{ name: '🤖 Generate an AI prompt to extract strings from my codebase (recommended)', value: 'extract' },
|
|
138
|
+
{ name: '📝 Create a sample file with hello-world entries', value: 'sample' },
|
|
139
|
+
{ name: '⏭️ Skip — I\'ll create it myself', value: 'skip' }
|
|
140
|
+
]
|
|
141
|
+
}]);
|
|
142
|
+
|
|
143
|
+
if (action === 'sample') {
|
|
144
|
+
const sampleContent = SAMPLE_SOURCE_FILES[config.format] || SAMPLE_SOURCE_FILES['json-nested'];
|
|
145
|
+
fs.mkdirSync(path.dirname(sourceFilePath), { recursive: true });
|
|
146
|
+
fs.writeFileSync(sourceFilePath, JSON.stringify(sampleContent, null, 2) + '\n', 'utf-8');
|
|
147
|
+
console.log(chalk.green(`✅ Created sample source file: ${config.sourceFile}`));
|
|
148
|
+
console.log(chalk.gray(' Edit this file to add your real translatable strings.'));
|
|
149
|
+
console.log(chalk.blue('\nRun "localingos sync" to push these source strings and get translations!\n'));
|
|
150
|
+
} else if (action === 'extract') {
|
|
151
|
+
console.log('');
|
|
152
|
+
// Dynamically import and run the extract prompt generator
|
|
153
|
+
const { generateExtractPrompt } = await import('./extract.js');
|
|
154
|
+
generateExtractPrompt(config);
|
|
155
|
+
} else {
|
|
156
|
+
console.log(chalk.blue(`\nCreate your source file at ${config.sourceFile}, then run "localingos sync".\n`));
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
console.log(chalk.blue('\nRun "localingos sync" to push source strings and pull translations.\n'));
|
|
160
|
+
}
|
|
114
161
|
}
|
package/src/commands/pull.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { loadConfig } from '../config.js';
|
|
3
|
+
import { loadConfig, resolveApiUrl } from '../config.js';
|
|
4
4
|
import { LocalingosApi } from '../api.js';
|
|
5
5
|
import { getFormatter } from '../formats/index.js';
|
|
6
6
|
|
|
7
7
|
export async function pullCommand(options) {
|
|
8
8
|
const config = loadConfig(options.config);
|
|
9
|
+
const apiUrl = resolveApiUrl(options.env);
|
|
9
10
|
const formatter = getFormatter(config.format);
|
|
10
11
|
|
|
11
12
|
console.log(chalk.blue(`⬇️ Pulling translations for family ${config.familyId}...`));
|
|
12
|
-
const api = new LocalingosApi(
|
|
13
|
+
const api = new LocalingosApi(apiUrl, config.apiKey);
|
|
13
14
|
|
|
14
15
|
let translations;
|
|
15
16
|
try {
|
package/src/commands/push.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
|
-
import { loadConfig } from '../config.js';
|
|
4
|
+
import { loadConfig, resolveApiUrl } from '../config.js';
|
|
4
5
|
import { LocalingosApi } from '../api.js';
|
|
5
6
|
import { getFormatter } from '../formats/index.js';
|
|
6
7
|
|
|
7
8
|
export async function pushCommand(options) {
|
|
8
9
|
const config = loadConfig(options.config);
|
|
10
|
+
const apiUrl = resolveApiUrl(options.env);
|
|
9
11
|
const formatter = getFormatter(config.format);
|
|
10
12
|
|
|
11
13
|
// 1. Extract source strings
|
|
12
14
|
const sourceFilePath = path.resolve(config.sourceFile);
|
|
13
15
|
console.log(chalk.blue(`📖 Extracting source strings from ${sourceFilePath}`));
|
|
14
16
|
|
|
17
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
18
|
+
console.error(chalk.red(`\n❌ Source file not found: ${config.sourceFile}`));
|
|
19
|
+
console.error(chalk.yellow(' To create it, you have two options:'));
|
|
20
|
+
console.error(chalk.yellow(' 1. Run "localingos extract" to generate an AI prompt that will create it for you'));
|
|
21
|
+
console.error(chalk.yellow(' 2. Run "localingos init" again and choose "Create a sample file"'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
let entries;
|
|
16
26
|
try {
|
|
17
27
|
entries = formatter.extract(sourceFilePath);
|
|
@@ -20,6 +30,12 @@ export async function pushCommand(options) {
|
|
|
20
30
|
process.exit(1);
|
|
21
31
|
}
|
|
22
32
|
|
|
33
|
+
if (entries.length === 0) {
|
|
34
|
+
console.error(chalk.red(`\n❌ Source file is empty: ${config.sourceFile}`));
|
|
35
|
+
console.error(chalk.yellow(' Add your translatable strings as key-value pairs, or run "localingos extract" for help.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
console.log(chalk.gray(` Found ${entries.length} translatable strings`));
|
|
24
40
|
|
|
25
41
|
const translatables = entries.map(entry => ({
|
|
@@ -41,7 +57,7 @@ export async function pushCommand(options) {
|
|
|
41
57
|
|
|
42
58
|
// 2. Push to API
|
|
43
59
|
console.log(chalk.blue(`\n⬆️ Pushing ${translatables.length} translatables to family ${config.familyId}...`));
|
|
44
|
-
const api = new LocalingosApi(
|
|
60
|
+
const api = new LocalingosApi(apiUrl, config.apiKey);
|
|
45
61
|
|
|
46
62
|
try {
|
|
47
63
|
await api.postTranslatables(config.familyId, translatables);
|
package/src/commands/sync.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
|
-
import { loadConfig } from '../config.js';
|
|
4
|
+
import { loadConfig, resolveApiUrl } from '../config.js';
|
|
4
5
|
import { LocalingosApi } from '../api.js';
|
|
5
6
|
import { getFormatter } from '../formats/index.js';
|
|
6
7
|
|
|
7
8
|
export async function syncCommand(options) {
|
|
8
9
|
const config = loadConfig(options.config);
|
|
10
|
+
const apiUrl = resolveApiUrl(options.env);
|
|
9
11
|
const formatter = getFormatter(config.format);
|
|
10
12
|
|
|
11
13
|
// 1. Extract source strings
|
|
12
14
|
const sourceFilePath = path.resolve(config.sourceFile);
|
|
13
15
|
console.log(chalk.blue(`📖 Extracting source strings from ${sourceFilePath}`));
|
|
14
16
|
|
|
17
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
18
|
+
console.error(chalk.red(`\n❌ Source file not found: ${config.sourceFile}`));
|
|
19
|
+
console.error(chalk.yellow(' To create it, you have two options:'));
|
|
20
|
+
console.error(chalk.yellow(' 1. Run "localingos extract" to generate an AI prompt that will create it for you'));
|
|
21
|
+
console.error(chalk.yellow(' 2. Run "localingos init" again and choose "Create a sample file"'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
let entries;
|
|
16
26
|
try {
|
|
17
27
|
entries = formatter.extract(sourceFilePath);
|
|
@@ -20,6 +30,12 @@ export async function syncCommand(options) {
|
|
|
20
30
|
process.exit(1);
|
|
21
31
|
}
|
|
22
32
|
|
|
33
|
+
if (entries.length === 0) {
|
|
34
|
+
console.error(chalk.red(`\n❌ Source file is empty: ${config.sourceFile}`));
|
|
35
|
+
console.error(chalk.yellow(' Add your translatable strings as key-value pairs, or run "localingos extract" for help.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
console.log(chalk.gray(` Found ${entries.length} translatable strings`));
|
|
24
40
|
|
|
25
41
|
// Build translatables payload
|
|
@@ -42,7 +58,7 @@ export async function syncCommand(options) {
|
|
|
42
58
|
|
|
43
59
|
// 2. Call /sync endpoint (push + pull in one call)
|
|
44
60
|
console.log(chalk.blue(`\n🔄 Syncing with Localingos (family: ${config.familyId})...`));
|
|
45
|
-
const api = new LocalingosApi(
|
|
61
|
+
const api = new LocalingosApi(apiUrl, config.apiKey);
|
|
46
62
|
|
|
47
63
|
let result;
|
|
48
64
|
try {
|
package/src/config.js
CHANGED
|
@@ -4,6 +4,27 @@ import chalk from 'chalk';
|
|
|
4
4
|
|
|
5
5
|
const DEFAULT_CONFIG_FILE = '.localingos.json';
|
|
6
6
|
|
|
7
|
+
const ENVIRONMENTS = {
|
|
8
|
+
prod: 'https://api.localingos.com',
|
|
9
|
+
gamma: 'https://gamma-api.localingos.com',
|
|
10
|
+
local: 'https://desktop.localingos.com:8080'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the API URL based on --env flag or LOCALINGOS_ENV env variable.
|
|
15
|
+
* Defaults to production.
|
|
16
|
+
*/
|
|
17
|
+
export function resolveApiUrl(envFlag) {
|
|
18
|
+
const env = envFlag || process.env.LOCALINGOS_ENV || 'prod';
|
|
19
|
+
const url = ENVIRONMENTS[env];
|
|
20
|
+
if (!url) {
|
|
21
|
+
const valid = Object.keys(ENVIRONMENTS).join(', ');
|
|
22
|
+
console.error(chalk.red(`Unknown environment "${env}". Valid: ${valid}`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
return url;
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
export function loadConfig(configPath) {
|
|
8
29
|
const resolvedPath = path.resolve(configPath || DEFAULT_CONFIG_FILE);
|
|
9
30
|
|
|
@@ -18,7 +39,7 @@ export function loadConfig(configPath) {
|
|
|
18
39
|
const config = JSON.parse(raw);
|
|
19
40
|
|
|
20
41
|
// Validate required fields
|
|
21
|
-
const required = ['apiKey', 'familyId', '
|
|
42
|
+
const required = ['apiKey', 'familyId', 'sourceLocale', 'format', 'sourceFile', 'outputDir'];
|
|
22
43
|
const missing = required.filter(field => !config[field]);
|
|
23
44
|
if (missing.length > 0) {
|
|
24
45
|
console.error(chalk.red(`Missing required config fields: ${missing.join(', ')}`));
|