Skip to content

Commit b0f1f68

Browse files
authored
1.3.0
* feat: add incognito mode for private translations ## New - Add `--incognito` CLI flag to `translate` and `init` commands - Add `incognito` config option in `lara.yaml` schema - Add interactive prompt for incognito mode during `lara-cli init` - Support incognito across all translate modes (config, --file, --text) - CLI flag overrides config value when both are set - Add 8 integration tests covering all incognito scenarios ## Changed - Bump version from 1.2.0 to 1.3.0 - Export shared mock translate function from test-helpers for call assertions * refactor: rename --incognito to --no-trace Align CLI flag and config naming with the @translated/lara SDK's noTrace option. Commander.js --no-* negation semantics are used: --no-trace sets trace=false, which maps to noTrace=true. ## Changed - Rename --incognito CLI flag to --no-trace on translate and init commands - Rename incognito config field to noTrace in lara.yaml schema - Update all tests, docs, and messages accordingly * refactor: remove no-trace interactive prompt ## Changed - No-trace mode in interactive init is now set only via --no-trace flag, no longer prompted * docs: improve no-trace mode documentation ## Changed - Expand translate.md no-trace section with per-invocation and permanent config subsections - Add noTrace field to config README basic structure example - Add noTrace to schema relations diagram in structure.md ## New - Add "Initialize with No-Trace Mode" example to init.md
1 parent df6277c commit b0f1f68

14 files changed

Lines changed: 279 additions & 12 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Lara Cli automates translation of your i18n files with a single command, preserv
66

77
Supports multiple file formats including JSON, PO (gettext), TypeScript, Vue I18n single-file components, Markdown and MDX files, Android XML string resource files, Xcode localization files (.strings, .stringsdict, .xcstrings), and plain text (.txt) files. See [Supported Formats](docs/config/formats.md) for details.
88

9-
[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](https://github.com/translated/lara-cli)
9+
[![Version](https://img.shields.io/badge/version-1.3.0-blue.svg)](https://github.com/translated/lara-cli)
1010

1111
</div>
1212

docs/commands/init.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ lara-cli init [options]
2121
| `-i, --instruction <instruction>` | Set project instruction for translation quality |
2222
| `-m, --translation-memories <memories>` | Set comma-separated Translation Memory IDs |
2323
| `-g, --glossaries <glossaries>` | Set comma-separated Glossary IDs |
24+
| `--no-trace` | Prevent server-side storage of translated content |
2425
| `-h, --help` | Display help information |
2526

2627
## Operating Modes
@@ -92,6 +93,14 @@ lara-cli init --source "en" --target "de, fr, it" \
9293
lara-cli init --reset-credentials
9394
```
9495

96+
### Initialize with No-Trace Mode
97+
98+
```bash
99+
lara-cli init --no-trace
100+
```
101+
102+
This sets `noTrace: true` in `lara.yaml`, which prevents the translation API from storing or logging your content server-side. All subsequent `lara-cli translate` runs will automatically use no-trace mode. You can also pass `--no-trace` directly to the translate command for one-off usage.
103+
95104
## Related
96105

97106
- [Configuration Reference](../config/README.md)

docs/commands/translate.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ lara-cli translate [options]
1515
| `-t, --target <locales>` | Comma-separated list of target locales to translate to |
1616
| `-p, --paths <paths>` | Comma-separated list of specific file paths to translate (overrides config) |
1717
| `-f, --force` | Force retranslation of all content, even if unchanged |
18+
| `--no-trace` | Prevent server-side storage of translated content |
1819
| `-h, --help` | Display help information |
1920

2021
## Examples
@@ -129,6 +130,38 @@ Use `--force` when you need to:
129130
- Fix translation errors by regenerating all translations
130131
- Reset translations after configuration changes
131132

133+
## No-Trace Mode
134+
135+
Use `--no-trace` to prevent the translation API from storing or logging your content server-side. This is useful when translating sensitive or confidential content.
136+
137+
### Per-Invocation
138+
139+
Pass `--no-trace` to any translate command:
140+
141+
```bash
142+
lara-cli translate --no-trace
143+
lara-cli translate --text "Sensitive data" --source en --target fr --no-trace
144+
lara-cli translate --file "confidential.json" --source en --target de --no-trace
145+
```
146+
147+
### Permanent Configuration
148+
149+
To enable no-trace for all translations in a project, either:
150+
151+
1. Initialize with `--no-trace`:
152+
153+
```bash
154+
lara-cli init --no-trace
155+
```
156+
157+
2. Or add it manually to `lara.yaml`:
158+
159+
```yaml
160+
noTrace: true
161+
```
162+
163+
When set in the config, all `lara-cli translate` runs will use no-trace mode automatically. The `--no-trace` CLI flag always takes precedence over the config value.
164+
132165
## Direct Translation
133166

134167
In addition to config-based translation, the `translate` command supports direct file and text translation. This mode is designed for CI/CD pipelines, scripting, and one-off translations — no `lara.yaml` configuration file is needed.
@@ -175,6 +208,7 @@ Only [supported file formats](../config/formats.md) are accepted (JSON, PO, XML,
175208
| `-o, --output <path>` | Output file path (only with `--file`) |
176209
| `-m, --translation-memories <ids>` | Translation Memory IDs (comma-separated) |
177210
| `-g, --glossaries <ids>` | Glossary IDs (comma-separated) |
211+
| `--no-trace` | Prevent server-side storage of translated content |
178212

179213
### Using Translation Memories & Glossaries
180214

docs/config/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ memories:
2121
- mem_abc123
2222
glossaries:
2323
- gls_xyz789
24+
noTrace: false
2425
files:
2526
json:
2627
include:

docs/config/structure.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ lara.yaml
1313
├── locales # Language settings
1414
├── memories # Translation memory settings
1515
├── glossaries # Terminology settings
16+
├── noTrace # No-trace mode (prevents server-side storage)
1617
└── files # File path and processing rules
1718
```
1819

@@ -46,6 +47,9 @@ glossaries:
4647
- gls_xyz789
4748
- gls_uvw012
4849

50+
# No-trace mode — prevents server-side storage of translated content
51+
noTrace: false
52+
4953
# File path and processing rules
5054
files:
5155
json:
@@ -91,7 +95,9 @@ The diagram below shows how the different configuration sections relate to each
9195
┌─────────────────┐ ┌───────────────────┐
9296
│ memories │ │ files │
9397
│ glossaries │ │ (paths & rules) │
94-
│ (terminology) │ │ │
98+
│ noTrace │ │ │
99+
│ (terminology & │ │ │
100+
│ privacy) │ │ │
95101
└─────────────────┘ └───────────────────┘
96102
```
97103

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@translated/lara-cli",
33
"type": "module",
4-
"version": "1.2.0",
4+
"version": "1.3.0",
55
"description": "CLI tool for automated i18n file translation using Lara Translate",
66
"repository": {
77
"type": "git",

src/__tests__/integration/cli.integration.test.ts

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import path from 'path';
55
import { fileURLToPath } from 'url';
66
import yaml from 'yaml';
77

8-
import { executeCommand } from './test-helpers.js';
8+
import { executeCommand, mockTranslate } from './test-helpers.js';
99
import initCommand from '../../cli/cmd/init/init.js';
1010
import translateCommand from '../../cli/cmd/translate/translate.js';
1111
import { ConfigProvider } from '#modules/config/config.provider.js';
@@ -516,4 +516,137 @@ describe('CLI Integration Tests', () => {
516516
expect(itContent.welcome).toBe('[it] Welcome');
517517
});
518518
});
519+
520+
describe('no-trace mode', () => {
521+
it('should save noTrace: true to lara.yaml when --no-trace flag is passed to init', async () => {
522+
await mkdir(path.join(testDir, 'src', 'i18n'), { recursive: true });
523+
await writeFile(
524+
path.join(testDir, 'src', 'i18n', 'en.json'),
525+
JSON.stringify({ greeting: 'Hello' }, null, 2)
526+
);
527+
528+
await executeCommand(initCommand, [
529+
'--non-interactive',
530+
'--source',
531+
'en',
532+
'--target',
533+
'it',
534+
'--paths',
535+
'src/i18n/[locale].json',
536+
'--no-trace',
537+
]);
538+
539+
const configPath = path.join(testDir, 'lara.yaml');
540+
const configContent = await readFile(configPath, 'utf-8');
541+
const config = yaml.parse(configContent);
542+
543+
expect(config.noTrace).toBe(true);
544+
});
545+
546+
it('should default noTrace to false in lara.yaml when --no-trace flag is not passed', async () => {
547+
await mkdir(path.join(testDir, 'src', 'i18n'), { recursive: true });
548+
await writeFile(
549+
path.join(testDir, 'src', 'i18n', 'en.json'),
550+
JSON.stringify({ greeting: 'Hello' }, null, 2)
551+
);
552+
553+
await executeCommand(initCommand, [
554+
'--non-interactive',
555+
'--source',
556+
'en',
557+
'--target',
558+
'it',
559+
'--paths',
560+
'src/i18n/[locale].json',
561+
]);
562+
563+
const configPath = path.join(testDir, 'lara.yaml');
564+
const configContent = await readFile(configPath, 'utf-8');
565+
const config = yaml.parse(configContent);
566+
567+
expect(config.noTrace).toBe(false);
568+
});
569+
570+
it('should pass noTrace: true when noTrace is enabled in config', async () => {
571+
await mkdir(path.join(testDir, 'src', 'i18n'), { recursive: true });
572+
await writeFile(
573+
path.join(testDir, 'src', 'i18n', 'en.json'),
574+
JSON.stringify({ greeting: 'Hello' }, null, 2)
575+
);
576+
577+
await executeCommand(initCommand, [
578+
'--non-interactive',
579+
'--source',
580+
'en',
581+
'--target',
582+
'it',
583+
'--paths',
584+
'src/i18n/[locale].json',
585+
'--no-trace',
586+
]);
587+
588+
(ConfigProvider as any).instance = null;
589+
mockTranslate.mockClear();
590+
591+
await executeCommand(translateCommand, []);
592+
593+
expect(mockTranslate).toHaveBeenCalled();
594+
const lastCallOptions = mockTranslate.mock.calls[0]![3];
595+
expect(lastCallOptions).toEqual(expect.objectContaining({ noTrace: true }));
596+
});
597+
598+
it('should pass noTrace: true when --no-trace flag is used on translate command', async () => {
599+
await mkdir(path.join(testDir, 'src', 'i18n'), { recursive: true });
600+
await writeFile(
601+
path.join(testDir, 'src', 'i18n', 'en.json'),
602+
JSON.stringify({ greeting: 'Hello' }, null, 2)
603+
);
604+
605+
await executeCommand(initCommand, [
606+
'--non-interactive',
607+
'--source',
608+
'en',
609+
'--target',
610+
'it',
611+
'--paths',
612+
'src/i18n/[locale].json',
613+
]);
614+
615+
(ConfigProvider as any).instance = null;
616+
mockTranslate.mockClear();
617+
618+
await executeCommand(translateCommand, ['--no-trace']);
619+
620+
expect(mockTranslate).toHaveBeenCalled();
621+
const lastCallOptions = mockTranslate.mock.calls[0]![3];
622+
expect(lastCallOptions).toEqual(expect.objectContaining({ noTrace: true }));
623+
});
624+
625+
it('should not pass noTrace when noTrace is disabled', async () => {
626+
await mkdir(path.join(testDir, 'src', 'i18n'), { recursive: true });
627+
await writeFile(
628+
path.join(testDir, 'src', 'i18n', 'en.json'),
629+
JSON.stringify({ greeting: 'Hello' }, null, 2)
630+
);
631+
632+
await executeCommand(initCommand, [
633+
'--non-interactive',
634+
'--source',
635+
'en',
636+
'--target',
637+
'it',
638+
'--paths',
639+
'src/i18n/[locale].json',
640+
]);
641+
642+
(ConfigProvider as any).instance = null;
643+
mockTranslate.mockClear();
644+
645+
await executeCommand(translateCommand, []);
646+
647+
expect(mockTranslate).toHaveBeenCalled();
648+
const lastCallOptions = mockTranslate.mock.calls[0]![3] as Record<string, unknown>;
649+
expect(lastCallOptions.noTrace).toBeUndefined();
650+
});
651+
});
519652
});

src/__tests__/integration/direct-translate.integration.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { existsSync } from 'fs';
44
import path from 'path';
55
import { fileURLToPath } from 'url';
66

7-
import { executeCommand } from './test-helpers.js';
7+
import { executeCommand, mockTranslate } from './test-helpers.js';
88
import translateCommand from '../../cli/cmd/translate/translate.js';
99
import { ConfigProvider } from '#modules/config/config.provider.js';
1010

@@ -597,4 +597,62 @@ describe('Direct Translation Integration Tests', () => {
597597
expect(stdoutCalls).toContainEqual('[fr] Hello\n');
598598
});
599599
});
600+
601+
describe('no-trace mode', () => {
602+
it('should pass noTrace: true when --no-trace is used with --text', async () => {
603+
mockTranslate.mockClear();
604+
605+
await executeCommand(translateCommand, [
606+
'--text',
607+
'Hello',
608+
'--source',
609+
'en',
610+
'--target',
611+
'fr',
612+
'--no-trace',
613+
]);
614+
615+
expect(mockTranslate).toHaveBeenCalled();
616+
const lastCallOptions = mockTranslate.mock.calls[0]![3];
617+
expect(lastCallOptions).toEqual(expect.objectContaining({ noTrace: true }));
618+
});
619+
620+
it('should pass noTrace: true when --no-trace is used with --file', async () => {
621+
const inputFile = path.join(testDir, 'hello.txt');
622+
await writeFile(inputFile, 'Hello');
623+
624+
mockTranslate.mockClear();
625+
626+
await executeCommand(translateCommand, [
627+
'--file',
628+
inputFile,
629+
'--source',
630+
'en',
631+
'--target',
632+
'fr',
633+
'--no-trace',
634+
]);
635+
636+
expect(mockTranslate).toHaveBeenCalled();
637+
const lastCallOptions = mockTranslate.mock.calls[0]![3];
638+
expect(lastCallOptions).toEqual(expect.objectContaining({ noTrace: true }));
639+
});
640+
641+
it('should not pass noTrace when --no-trace is not used with --text', async () => {
642+
mockTranslate.mockClear();
643+
644+
await executeCommand(translateCommand, [
645+
'--text',
646+
'Hello',
647+
'--source',
648+
'en',
649+
'--target',
650+
'fr',
651+
]);
652+
653+
expect(mockTranslate).toHaveBeenCalled();
654+
const lastCallOptions = mockTranslate.mock.calls[0]![3] as Record<string, unknown>;
655+
expect(lastCallOptions.noTrace).toBeUndefined();
656+
});
657+
});
600658
});

src/__tests__/integration/test-helpers.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
import { vi } from 'vitest';
22
import { Command, Option } from 'commander';
33

4+
// Shared mock translate function, exported so tests can assert on call args
5+
export const mockTranslate = vi.fn(
6+
async (
7+
textBlocks: { text: string; translatable: boolean }[],
8+
_sourceLocale: string,
9+
targetLocale: string,
10+
_options?: Record<string, unknown>
11+
) => {
12+
return textBlocks.map((block: { text: string; translatable: boolean }) => ({
13+
text: block.translatable ? `[${targetLocale}] ${block.text}` : block.text,
14+
translatable: block.translatable,
15+
}));
16+
}
17+
);
18+
419
// Mock translation service
520
vi.mock('#modules/translation/translation.service.js', () => {
621
return {
722
TranslationService: {
823
getInstance: vi.fn(() => ({
9-
translate: vi.fn(async (textBlocks, _sourceLocale, targetLocale) => {
10-
// Return mock translations based on target locale
11-
return textBlocks.map((block: { text: string; translatable: boolean }) => ({
12-
text: block.translatable ? `[${targetLocale}] ${block.text}` : block.text,
13-
translatable: block.translatable,
14-
}));
15-
}),
24+
translate: mockTranslate,
1625
})),
1726
},
1827
};

0 commit comments

Comments
 (0)