Skip to content

Commit

Permalink
feat: add --namespaces support to sync
Browse files Browse the repository at this point in the history
  • Loading branch information
gu-stav authored and stepan662 committed Jan 22, 2025
1 parent bcc2461 commit 89eba40
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 20 deletions.
7 changes: 7 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,13 @@
"removeUnused": {
"description": "Delete unused keys from the Tolgee project",
"type": "boolean"
},
"namespaces": {
"description": "Specifies which namespaces should be synchronized.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
Expand Down
29 changes: 28 additions & 1 deletion src/commands/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Options = BaseOptions & {
backup?: string | false;
removeUnused?: boolean;
continueOnWarning?: boolean;
namespaces?: string[];
yes?: boolean;
tagNewKeys?: string[];
};
Expand Down Expand Up @@ -80,6 +81,16 @@ const syncHandler = (config: Schema) =>
}

const localKeys = filterExtractionResult(rawKeys);
console.dir({ localKeys });

if (opts.namespaces?.length) {
for (const namespace of Object.keys(localKeys)) {
if (!opts.namespaces?.includes(namespace)) {
localKeys[namespace].clear();
}
}
}

const allKeysLoadable = await opts.client.GET(
'/v2/projects/{projectId}/all-keys',
{
Expand All @@ -89,9 +100,19 @@ const syncHandler = (config: Schema) =>

handleLoadableError(allKeysLoadable);

const remoteKeys = allKeysLoadable.data?._embedded?.keys ?? [];
let remoteKeys = allKeysLoadable.data?._embedded?.keys ?? [];
console.dir({ remoteKeys });

if (opts.namespaces?.length) {
remoteKeys = remoteKeys.filter(
(key) => key.namespace && opts.namespaces?.includes(key.namespace ?? '')
);
}

const diff = compareKeys(localKeys, remoteKeys);

console.log('diff', JSON.stringify(diff, null, 2));

if (!diff.added.length && !diff.removed.length) {
console.log(
ansi.green(
Expand Down Expand Up @@ -223,6 +244,12 @@ export default (config: Schema) =>
'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.'
).default(config.sync?.continueOnWarning ?? false)
)
.addOption(
new Option(
'-n, --namespaces <namespaces...>',
'Specifies which namespaces should be synchronized.'
).default(config.sync?.namespaces)
)
.addOption(
new Option(
'-Y, --yes',
Expand Down
10 changes: 5 additions & 5 deletions src/commands/sync/syncUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ResponseOf } from '../../client/internal/schema.utils.js';
import type { Key } from '../../extractor/index.js';
import { type FilteredKeys, NullNamespace } from '../../extractor/runner.js';
import { type FilteredKeys } from '../../extractor/runner.js';
import ansi from 'ansi-colors';

type ResponseAllKeys = ResponseOf<
Expand Down Expand Up @@ -54,27 +54,27 @@ export function compareKeys(

// Deleted keys
for (const remoteKey of remote) {
const namespace = remoteKey.namespace || NullNamespace;
const namespace = remoteKey.namespace || '';
const keyExists = local[namespace]?.delete(remoteKey.name);
if (!keyExists) {
result.removed.push({
id: remoteKey.id,
keyName: remoteKey.name,
namespace: remoteKey.namespace || undefined,
namespace: remoteKey.namespace || '',
});
}
}

// Added keys
const namespaces = [NullNamespace, ...Object.keys(local).sort()] as const;
const namespaces = [...Object.keys(local).sort()] as const;
for (const namespace of namespaces) {
if (namespace in local && local[namespace].size) {
const keys = local[namespace];
const keyNames = Array.from(local[namespace].keys()).sort();
for (const keyName of keyNames) {
result.added.push({
keyName: keyName,
namespace: namespace === NullNamespace ? undefined : namespace,
namespace: namespace || '',
defaultValue: keys.get(keyName) || undefined,
});
}
Expand Down
5 changes: 1 addition & 4 deletions src/extractor/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import { extname } from 'path';
import { callWorker } from './worker.js';
import { exitWithError } from '../utils/logger.js';

export const NullNamespace = Symbol('namespace.null');

export type FilteredKeys = {
[NullNamespace]: Map<string, string | null>;
[key: string]: Map<string, string | null>;
};

Expand Down Expand Up @@ -142,7 +139,7 @@ export function filterExtractionResult(data: ExtractionResults): FilteredKeys {
const result: FilteredKeys = Object.create(null);
for (const { keys } of data.values()) {
for (const key of keys) {
const namespace = key.namespace || NullNamespace;
const namespace = key.namespace || '';
if (!(namespace in result)) {
result[namespace] = new Map();
}
Expand Down
4 changes: 4 additions & 0 deletions src/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ export interface Schema {
* Delete unused keys from the Tolgee project
*/
removeUnused?: boolean;
/**
* Specifies which namespaces should be synchronized.
*/
namespaces?: string[];
};
tag?: {
/**
Expand Down
3 changes: 0 additions & 3 deletions test/__fixtures__/testProjectCode/Test3Mixed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@ export default function App() {
<li>
<T keyName="tomato" ns="food" />
</li>
<li>
<T keyName="onions" ns="food" />
</li>
<li>
<T keyName="cookies" ns="food" />
</li>
Expand Down
3 changes: 2 additions & 1 deletion test/e2e/compare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ describe('Project 3', () => {
expect(out.code).toBe(0);
expect(out.stdout).toContain('out of sync');
expect(out.stdout).toContain('4 new keys found');
expect(out.stdout).toContain('3 unused keys');
expect(out.stdout).toContain('4 unused keys');
expect(out.stdout).toContain('+ cookies (namespace: food)');
expect(out.stdout).toContain('- onions (namespace: food)');
expect(out.stdout).toContain('- soda (namespace: drinks)');
expect(out.stdout).toContain('+ table (namespace: furniture)');
expect(out.stdout).toContain('- table\n');
Expand Down
138 changes: 133 additions & 5 deletions test/e2e/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ const CODE_PROJECT_2_COMPLETE = `${CODE_PATH}/Test2Complete.tsx`;
const CODE_PROJECT_2_ADDED = `${CODE_PATH}/Test2New.tsx`;
const CODE_PROJECT_2_DELETED = `${CODE_PATH}/Test2Incomplete.tsx`;
const CODE_PROJECT_2_WARNING = `${CODE_PATH}/Test2Warning.tsx`;
const CODE_PROJECT_3 = `${CODE_PATH}/Test3SingleDiff.tsx`;
const CODE_PROJECT_3_DIFF = `${CODE_PATH}/Test3SingleDiff.tsx`;
const CODE_PROJECT_3_MIXED = `${CODE_PATH}/Test3Mixed.tsx`;

setupTemporaryFolder();

Expand Down Expand Up @@ -143,7 +144,7 @@ describe('Project 2', () => {
});
}, 30e3);

it('deletes keys that no longer exist via --remove-unused', async () => {
it('deletes keys that no longer exist (args)', async () => {
const pakWithDelete = await createPak(client, [
...DEFAULT_SCOPES,
'keys.delete',
Expand Down Expand Up @@ -176,7 +177,7 @@ describe('Project 2', () => {
expect(keys.data?.page?.totalElements).toBe(0);
}, 30e3);

it('deletes keys that no longer exist via config', async () => {
it('deletes keys that no longer exist (config)', async () => {
const pakWithDelete = await createPak(client, [
...DEFAULT_SCOPES,
'keys.delete',
Expand Down Expand Up @@ -296,7 +297,7 @@ describe('Project 3', () => {

it('handles namespaces properly (args)', async () => {
const out = await run(
['sync', '--yes', '--api-key', pak, '--patterns', CODE_PROJECT_3],
['sync', '--yes', '--api-key', pak, '--patterns', CODE_PROJECT_3_DIFF],
undefined,
20e3
);
Expand Down Expand Up @@ -325,7 +326,7 @@ describe('Project 3', () => {
it('handles namespaces properly (config)', async () => {
const { configFile } = await createTmpFolderWithConfig({
apiKey: pak,
patterns: [CODE_PROJECT_3],
patterns: [CODE_PROJECT_3_DIFF],
});
const out = await run(['-c', configFile, 'sync', '--yes'], undefined, 20e3);

Expand All @@ -349,4 +350,131 @@ describe('Project 3', () => {
},
});
}, 30e3);

it('synchronizes the defined namespaces only (args)', async () => {
const out = await run(
[
'sync',
'--yes',
'--api-key',
pak,
'--patterns',
CODE_PROJECT_3_MIXED,
'--namespaces',
'food',
],
undefined,
20e3
);

expect(out.code).toBe(0);
expect(out.stdout).toContain('+ 1 string');
expect(out.stdout).toContain('1 unused key could be deleted.');

const keys = await client.GET('/v2/projects/{projectId}/translations', {
params: {
path: { projectId: client.getProjectId() },
},
});

const stored = tolgeeDataToDict(keys.data);

expect(Object.keys(stored)).toContain('table');
expect(Object.keys(stored)).not.toContain('welcome');
expect(Object.keys(stored).length).toEqual(11);
}, 30e3);

it('synchronizes the defined namespaces only (config)', async () => {
const { configFile } = await createTmpFolderWithConfig({
apiKey: pak,
patterns: [CODE_PROJECT_3_MIXED],
sync: {
namespaces: ['food'],
},
});
const out = await run(['-c', configFile, 'sync', '--yes'], undefined, 20e3);

expect(out.code).toBe(0);
expect(out.stdout).toContain('+ 1 string');
expect(out.stdout).toContain('1 unused key could be deleted.');

const keys = await client.GET('/v2/projects/{projectId}/translations', {
params: {
path: { projectId: client.getProjectId() },
},
});

const stored = tolgeeDataToDict(keys.data);

expect(Object.keys(stored)).toContain('table');
expect(Object.keys(stored)).not.toContain('welcome');
expect(Object.keys(stored).length).toEqual(11);
}, 30e3);

it('deletes only keys within namespace when using namespace selector (args)', async () => {
const pakWithDelete = await createPak(client, [
...DEFAULT_SCOPES,
'keys.delete',
]);

const out = await run(
[
'sync',
'--yes',
'--remove-unused',
'--api-key',
pakWithDelete,
'--namespaces',
'food',
'--patterns',
CODE_PROJECT_3_MIXED,
],
undefined,
20e3
);

expect(out.code).toBe(0);
expect(out.stdout).toContain('- 1 string');
expect(out.stdout).toContain('+ 1 string');

const keys = await client.GET('/v2/projects/{projectId}/translations', {
params: {
path: { projectId: client.getProjectId() },
query: { filterKeyName: ['onions'] },
},
});

expect(keys.data?.page?.totalElements).toBe(0);
}, 30e3);

it('deletes only keys within namespace when using namespace selector (config)', async () => {
const pakWithDelete = await createPak(client, [
...DEFAULT_SCOPES,
'keys.delete',
]);

const { configFile } = await createTmpFolderWithConfig({
apiKey: pakWithDelete,
patterns: [CODE_PROJECT_3_MIXED],
sync: {
namespaces: ['food'],
removeUnused: true,
},
});

const out = await run(['-c', configFile, 'sync', '--yes'], undefined, 20e3);

expect(out.code).toBe(0);
expect(out.stdout).toContain('- 1 string');
expect(out.stdout).toContain('+ 1 string');

const keys = await client.GET('/v2/projects/{projectId}/translations', {
params: {
path: { projectId: client.getProjectId() },
query: { filterKeyName: ['onions'] },
},
});

expect(keys.data?.page?.totalElements).toBe(0);
}, 30e3);
});
2 changes: 1 addition & 1 deletion test/e2e/utils/data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export function tolgeeDataToDict(data: any) {
return Object.fromEntries(
data._embedded.keys.map((k: any) => [
(data._embedded?.keys ?? []).map((k: any) => [
k.keyName,
{
__ns: k.keyNamespace,
Expand Down

0 comments on commit 89eba40

Please sign in to comment.