Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,15 @@
"type": "boolean"
},
"removeUnused": {
"description": "Delete unused keys from the Tolgee project",
"description": "Delete unused keys from the Tolgee project (within selected namespaces if specified).",
"type": "boolean"
},
"namespaces": {
"description": "Specifies which namespaces should be synchronized.",
"type": "array",
"items": {
"type": "string"
}
}
}
},
Expand Down
27 changes: 25 additions & 2 deletions 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,15 @@ const syncHandler = (config: Schema) =>
}

const localKeys = filterExtractionResult(rawKeys);

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 +99,16 @@ const syncHandler = (config: Schema) =>

handleLoadableError(allKeysLoadable);

const remoteKeys = allKeysLoadable.data?._embedded?.keys ?? [];
let remoteKeys = allKeysLoadable.data?._embedded?.keys ?? [];

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

const diff = compareKeys(localKeys, remoteKeys);

if (!diff.added.length && !diff.removed.length) {
console.log(
ansi.green(
Expand Down Expand Up @@ -223,6 +240,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 All @@ -232,7 +255,7 @@ export default (config: Schema) =>
.addOption(
new Option(
'--remove-unused',
'Delete unused keys from the Tolgee project.'
'Delete unused keys from the Tolgee project (within selected namespaces if specified).'
).default(config.sync?.removeUnused ?? false)
)
.option(
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 @@ -193,6 +193,10 @@ export interface Schema {
* Delete unused keys from the Tolgee project
*/
removeUnused?: boolean;
/**
* Specifies which namespaces should be synchronized.
*/
namespaces?: string[];
};
tag?: {
/**
Expand Down
15 changes: 4 additions & 11 deletions test/__fixtures__/testProjectCode/Test3Mixed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export default function App() {
<Fragment>
<section>
<h1>
<T keyName="welcome">Welcome!</T>
<T keyName="welcome" ns="new-namespace">
Welcome!
</T>
</h1>
<ul>
<li>
Expand All @@ -17,13 +19,7 @@ export default function App() {
<T keyName="chair" ns="furniture" />
</li>
<li>
<T keyName="plate" />
</li>
<li>
<T keyName="fork" />
</li>
<li>
<T keyName="knife" />
<T keyName="spoon" />
</li>
</ul>
</section>
Expand All @@ -44,9 +40,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
5 changes: 3 additions & 2 deletions test/e2e/compare.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ 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('5 new keys found');
expect(out.stdout).toContain('7 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
133 changes: 133 additions & 0 deletions test/e2e/push.p1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { fileURLToPath } from 'node:url';
import { tolgeeDataToDict } from './utils/data.js';
import { run } from './utils/run.js';
import {
createPak,
createProjectWithClient,
deleteProject,
} from './utils/api/common.js';
import { TolgeeClient } from '#cli/client/TolgeeClient.js';
import { PROJECT_1 } from './utils/api/project1.js';
import { createTmpFolderWithConfig, removeTmpFolder } from './utils/tmp.js';
import { pushFilesConfig } from './utils/pushFilesConfig.js';

const FIXTURES_PATH = new URL('../__fixtures__/', import.meta.url);

const PROJECT_1_DIR = new URL('./updatedProject1/', FIXTURES_PATH);

let client: TolgeeClient;
let pak: string;

describe('project 1', () => {
beforeEach(async () => {
client = await createProjectWithClient('Project 1', PROJECT_1);
pak = await createPak(client);
});
afterEach(async () => {
await deleteProject(client);
await removeTmpFolder();
});

it('pushes updated strings to Tolgee', async () => {
const out = await run([
'--api-key',
pak,
'push',
'--verbose',
'--files-template',
fileURLToPath(new URL(`./{languageTag}.json`, PROJECT_1_DIR)),
]);

expect(out.code).toBe(0);

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

expect(keys.data?.page?.totalElements).toBe(2);

const stored = tolgeeDataToDict(keys.data);
expect(stored).toEqual({
wired: {
__ns: null,
en: 'Wired',
fr: 'Filaire',
},
wireless: {
__ns: null,
en: 'Wireless',
fr: 'Sans-fil',
},
});
});

it('pushes only selected languages (args)', async () => {
const out = await run([
'--api-key',
pak,
'push',
'--files-template',
fileURLToPath(new URL(`./{languageTag}.json`, PROJECT_1_DIR)),
'-l',
'fr',
]);

expect(out.code).toBe(0);

const keys = await client.GET('/v2/projects/{projectId}/translations', {
params: {
path: { projectId: client.getProjectId() },
query: { search: 'wire' },
},
});
expect(keys.data?.page?.totalElements).toBe(2);

const stored = tolgeeDataToDict(keys.data);
expect(stored).toEqual({
wired: {
__ns: null,
fr: 'Filaire',
},
wireless: {
__ns: null,
fr: 'Sans-fil',
},
});
});

it('pushes only selected languages (config)', async () => {
const { configFile } = await createTmpFolderWithConfig({
apiKey: pak,
push: {
files: pushFilesConfig(PROJECT_1_DIR),
languages: ['fr'],
},
});
const out = await run(['--config', configFile, 'push']);

expect(out.code).toBe(0);

const keys = await client.GET('/v2/projects/{projectId}/translations', {
params: {
path: { projectId: client.getProjectId() },
query: { search: 'wire' },
},
});
expect(keys.data?.page?.totalElements).toBe(2);

const stored = tolgeeDataToDict(keys.data);
expect(stored).toEqual({
wired: {
__ns: null,
fr: 'Filaire',
},
wireless: {
__ns: null,
fr: 'Sans-fil',
},
});
});
});
Loading
Loading