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 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
- ### 3. Sync translations
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localingos",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI tool to sync translations with Localingos",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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
+ }
@@ -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
- // Use createRequire for inquirer since it needs CJS-style dynamic import
9
- const require = createRequire(import.meta.url);
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 (your project/family in Localingos):',
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
- console.log(chalk.blue('\nRun "localingos sync" to push source strings and pull translations.\n'));
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
  }
@@ -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(config.apiUrl, config.apiKey);
13
+ const api = new LocalingosApi(apiUrl, config.apiKey);
13
14
 
14
15
  let translations;
15
16
  try {
@@ -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(config.apiUrl, config.apiKey);
60
+ const api = new LocalingosApi(apiUrl, config.apiKey);
45
61
 
46
62
  try {
47
63
  await api.postTranslatables(config.familyId, translatables);
@@ -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(config.apiUrl, config.apiKey);
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', 'apiUrl', 'sourceLocale', 'format', 'sourceFile', 'outputDir'];
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(', ')}`));