Skip to content

Commit 4b9f46d

Browse files
authored
feat: add --namespaces support to sync (#151)
1 parent 75eb346 commit 4b9f46d

File tree

14 files changed

+893
-746
lines changed

14 files changed

+893
-746
lines changed

schema.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,15 @@
165165
"type": "boolean"
166166
},
167167
"removeUnused": {
168-
"description": "Delete unused keys from the Tolgee project",
168+
"description": "Delete unused keys from the Tolgee project (within selected namespaces if specified).",
169169
"type": "boolean"
170+
},
171+
"namespaces": {
172+
"description": "Specifies which namespaces should be synchronized.",
173+
"type": "array",
174+
"items": {
175+
"type": "string"
176+
}
170177
}
171178
}
172179
},

src/commands/sync/sync.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type Options = BaseOptions & {
2424
backup?: string | false;
2525
removeUnused?: boolean;
2626
continueOnWarning?: boolean;
27+
namespaces?: string[];
2728
yes?: boolean;
2829
tagNewKeys?: string[];
2930
};
@@ -80,6 +81,15 @@ const syncHandler = (config: Schema) =>
8081
}
8182

8283
const localKeys = filterExtractionResult(rawKeys);
84+
85+
if (opts.namespaces?.length) {
86+
for (const namespace of Object.keys(localKeys)) {
87+
if (!opts.namespaces?.includes(namespace)) {
88+
localKeys[namespace].clear();
89+
}
90+
}
91+
}
92+
8393
const allKeysLoadable = await opts.client.GET(
8494
'/v2/projects/{projectId}/all-keys',
8595
{
@@ -89,9 +99,16 @@ const syncHandler = (config: Schema) =>
8999

90100
handleLoadableError(allKeysLoadable);
91101

92-
const remoteKeys = allKeysLoadable.data?._embedded?.keys ?? [];
102+
let remoteKeys = allKeysLoadable.data?._embedded?.keys ?? [];
103+
104+
if (opts.namespaces?.length) {
105+
remoteKeys = remoteKeys.filter((key) => {
106+
return opts.namespaces?.includes(key.namespace ?? '');
107+
});
108+
}
93109

94110
const diff = compareKeys(localKeys, remoteKeys);
111+
95112
if (!diff.added.length && !diff.removed.length) {
96113
console.log(
97114
ansi.green(
@@ -223,6 +240,12 @@ export default (config: Schema) =>
223240
'Set this flag to continue the sync if warnings are detected during string extraction. By default, as warnings may indicate an invalid extraction, the CLI will abort the sync.'
224241
).default(config.sync?.continueOnWarning ?? false)
225242
)
243+
.addOption(
244+
new Option(
245+
'-n, --namespaces <namespaces...>',
246+
'Specifies which namespaces should be synchronized.'
247+
).default(config.sync?.namespaces)
248+
)
226249
.addOption(
227250
new Option(
228251
'-Y, --yes',
@@ -232,7 +255,7 @@ export default (config: Schema) =>
232255
.addOption(
233256
new Option(
234257
'--remove-unused',
235-
'Delete unused keys from the Tolgee project.'
258+
'Delete unused keys from the Tolgee project (within selected namespaces if specified).'
236259
).default(config.sync?.removeUnused ?? false)
237260
)
238261
.option(

src/commands/sync/syncUtils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ResponseOf } from '../../client/internal/schema.utils.js';
22
import type { Key } from '../../extractor/index.js';
3-
import { type FilteredKeys, NullNamespace } from '../../extractor/runner.js';
3+
import { type FilteredKeys } from '../../extractor/runner.js';
44
import ansi from 'ansi-colors';
55

66
type ResponseAllKeys = ResponseOf<
@@ -54,27 +54,27 @@ export function compareKeys(
5454

5555
// Deleted keys
5656
for (const remoteKey of remote) {
57-
const namespace = remoteKey.namespace || NullNamespace;
57+
const namespace = remoteKey.namespace || '';
5858
const keyExists = local[namespace]?.delete(remoteKey.name);
5959
if (!keyExists) {
6060
result.removed.push({
6161
id: remoteKey.id,
6262
keyName: remoteKey.name,
63-
namespace: remoteKey.namespace || undefined,
63+
namespace: remoteKey.namespace || '',
6464
});
6565
}
6666
}
6767

6868
// Added keys
69-
const namespaces = [NullNamespace, ...Object.keys(local).sort()] as const;
69+
const namespaces = [...Object.keys(local).sort()] as const;
7070
for (const namespace of namespaces) {
7171
if (namespace in local && local[namespace].size) {
7272
const keys = local[namespace];
7373
const keyNames = Array.from(local[namespace].keys()).sort();
7474
for (const keyName of keyNames) {
7575
result.added.push({
7676
keyName: keyName,
77-
namespace: namespace === NullNamespace ? undefined : namespace,
77+
namespace: namespace || '',
7878
defaultValue: keys.get(keyName) || undefined,
7979
});
8080
}

src/extractor/runner.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import { extname } from 'path';
1010
import { callWorker } from './worker.js';
1111
import { exitWithError } from '../utils/logger.js';
1212

13-
export const NullNamespace = Symbol('namespace.null');
14-
1513
export type FilteredKeys = {
16-
[NullNamespace]: Map<string, string | null>;
1714
[key: string]: Map<string, string | null>;
1815
};
1916

@@ -142,7 +139,7 @@ export function filterExtractionResult(data: ExtractionResults): FilteredKeys {
142139
const result: FilteredKeys = Object.create(null);
143140
for (const { keys } of data.values()) {
144141
for (const key of keys) {
145-
const namespace = key.namespace || NullNamespace;
142+
const namespace = key.namespace || '';
146143
if (!(namespace in result)) {
147144
result[namespace] = new Map();
148145
}

src/schema.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,10 @@ export interface Schema {
193193
* Delete unused keys from the Tolgee project
194194
*/
195195
removeUnused?: boolean;
196+
/**
197+
* Specifies which namespaces should be synchronized.
198+
*/
199+
namespaces?: string[];
196200
};
197201
tag?: {
198202
/**

test/__fixtures__/testProjectCode/Test3Mixed.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export default function App() {
77
<Fragment>
88
<section>
99
<h1>
10-
<T keyName="welcome">Welcome!</T>
10+
<T keyName="welcome" ns="new-namespace">
11+
Welcome!
12+
</T>
1113
</h1>
1214
<ul>
1315
<li>
@@ -17,13 +19,7 @@ export default function App() {
1719
<T keyName="chair" ns="furniture" />
1820
</li>
1921
<li>
20-
<T keyName="plate" />
21-
</li>
22-
<li>
23-
<T keyName="fork" />
24-
</li>
25-
<li>
26-
<T keyName="knife" />
22+
<T keyName="spoon" />
2723
</li>
2824
</ul>
2925
</section>
@@ -44,9 +40,6 @@ export default function App() {
4440
<li>
4541
<T keyName="tomato" ns="food" />
4642
</li>
47-
<li>
48-
<T keyName="onions" ns="food" />
49-
</li>
5043
<li>
5144
<T keyName="cookies" ns="food" />
5245
</li>

test/e2e/compare.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,10 @@ describe('Project 3', () => {
173173

174174
expect(out.code).toBe(0);
175175
expect(out.stdout).toContain('out of sync');
176-
expect(out.stdout).toContain('4 new keys found');
177-
expect(out.stdout).toContain('3 unused keys');
176+
expect(out.stdout).toContain('5 new keys found');
177+
expect(out.stdout).toContain('7 unused keys');
178178
expect(out.stdout).toContain('+ cookies (namespace: food)');
179+
expect(out.stdout).toContain('- onions (namespace: food)');
179180
expect(out.stdout).toContain('- soda (namespace: drinks)');
180181
expect(out.stdout).toContain('+ table (namespace: furniture)');
181182
expect(out.stdout).toContain('- table\n');

test/e2e/push.p1.test.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { fileURLToPath } from 'node:url';
2+
import { tolgeeDataToDict } from './utils/data.js';
3+
import { run } from './utils/run.js';
4+
import {
5+
createPak,
6+
createProjectWithClient,
7+
deleteProject,
8+
} from './utils/api/common.js';
9+
import { TolgeeClient } from '#cli/client/TolgeeClient.js';
10+
import { PROJECT_1 } from './utils/api/project1.js';
11+
import { createTmpFolderWithConfig, removeTmpFolder } from './utils/tmp.js';
12+
import { pushFilesConfig } from './utils/pushFilesConfig.js';
13+
14+
const FIXTURES_PATH = new URL('../__fixtures__/', import.meta.url);
15+
16+
const PROJECT_1_DIR = new URL('./updatedProject1/', FIXTURES_PATH);
17+
18+
let client: TolgeeClient;
19+
let pak: string;
20+
21+
describe('project 1', () => {
22+
beforeEach(async () => {
23+
client = await createProjectWithClient('Project 1', PROJECT_1);
24+
pak = await createPak(client);
25+
});
26+
afterEach(async () => {
27+
await deleteProject(client);
28+
await removeTmpFolder();
29+
});
30+
31+
it('pushes updated strings to Tolgee', async () => {
32+
const out = await run([
33+
'--api-key',
34+
pak,
35+
'push',
36+
'--verbose',
37+
'--files-template',
38+
fileURLToPath(new URL(`./{languageTag}.json`, PROJECT_1_DIR)),
39+
]);
40+
41+
expect(out.code).toBe(0);
42+
43+
const keys = await client.GET('/v2/projects/{projectId}/translations', {
44+
params: {
45+
path: { projectId: client.getProjectId() },
46+
query: { search: 'wire' },
47+
},
48+
});
49+
50+
expect(keys.data?.page?.totalElements).toBe(2);
51+
52+
const stored = tolgeeDataToDict(keys.data);
53+
expect(stored).toEqual({
54+
wired: {
55+
__ns: null,
56+
en: 'Wired',
57+
fr: 'Filaire',
58+
},
59+
wireless: {
60+
__ns: null,
61+
en: 'Wireless',
62+
fr: 'Sans-fil',
63+
},
64+
});
65+
});
66+
67+
it('pushes only selected languages (args)', async () => {
68+
const out = await run([
69+
'--api-key',
70+
pak,
71+
'push',
72+
'--files-template',
73+
fileURLToPath(new URL(`./{languageTag}.json`, PROJECT_1_DIR)),
74+
'-l',
75+
'fr',
76+
]);
77+
78+
expect(out.code).toBe(0);
79+
80+
const keys = await client.GET('/v2/projects/{projectId}/translations', {
81+
params: {
82+
path: { projectId: client.getProjectId() },
83+
query: { search: 'wire' },
84+
},
85+
});
86+
expect(keys.data?.page?.totalElements).toBe(2);
87+
88+
const stored = tolgeeDataToDict(keys.data);
89+
expect(stored).toEqual({
90+
wired: {
91+
__ns: null,
92+
fr: 'Filaire',
93+
},
94+
wireless: {
95+
__ns: null,
96+
fr: 'Sans-fil',
97+
},
98+
});
99+
});
100+
101+
it('pushes only selected languages (config)', async () => {
102+
const { configFile } = await createTmpFolderWithConfig({
103+
apiKey: pak,
104+
push: {
105+
files: pushFilesConfig(PROJECT_1_DIR),
106+
languages: ['fr'],
107+
},
108+
});
109+
const out = await run(['--config', configFile, 'push']);
110+
111+
expect(out.code).toBe(0);
112+
113+
const keys = await client.GET('/v2/projects/{projectId}/translations', {
114+
params: {
115+
path: { projectId: client.getProjectId() },
116+
query: { search: 'wire' },
117+
},
118+
});
119+
expect(keys.data?.page?.totalElements).toBe(2);
120+
121+
const stored = tolgeeDataToDict(keys.data);
122+
expect(stored).toEqual({
123+
wired: {
124+
__ns: null,
125+
fr: 'Filaire',
126+
},
127+
wireless: {
128+
__ns: null,
129+
fr: 'Sans-fil',
130+
},
131+
});
132+
});
133+
});

0 commit comments

Comments
 (0)