Skip to content

Commit 67da483

Browse files
committed
Add tests and bones of paths input
1 parent b19693e commit 67da483

2 files changed

Lines changed: 217 additions & 3 deletions

File tree

scripts/generateTranslations.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import dedent from '@libs/StringUtils/dedent';
1414
import hashStr from '@libs/StringUtils/hash';
1515
import {isTranslationTargetLocale, LOCALES, TRANSLATION_TARGET_LOCALES} from '@src/CONST/LOCALES';
1616
import type {Locale, TranslationTargetLocale} from '@src/CONST/LOCALES';
17+
import type {TranslationPaths} from '@src/languages/types';
1718
import CLI from './utils/CLI';
1819
import Prettier from './utils/Prettier';
1920
import PromisePool from './utils/PromisePool';
@@ -87,6 +88,11 @@ class TranslationGenerator {
8788
*/
8889
private readonly compareRef: string;
8990

91+
/**
92+
* Specific paths to retranslate (supersedes compareRef).
93+
*/
94+
private readonly paths: TranslationPaths[] | undefined;
95+
9096
/**
9197
* Should we print verbose logs?
9298
*/
@@ -99,13 +105,22 @@ class TranslationGenerator {
99105
*/
100106
private readonly translatedSpanHashToEnglishSpanHash = new Map<number, number>();
101107

102-
constructor(config: {targetLanguages: TranslationTargetLocale[]; languagesDir: string; sourceFile: string; translator: Translator; compareRef: string; verbose: boolean}) {
108+
constructor(config: {
109+
targetLanguages: TranslationTargetLocale[];
110+
languagesDir: string;
111+
sourceFile: string;
112+
translator: Translator;
113+
compareRef: string;
114+
paths?: TranslationPaths[];
115+
verbose: boolean;
116+
}) {
103117
this.targetLanguages = config.targetLanguages;
104118
this.languagesDir = config.languagesDir;
105119
const sourceCode = fs.readFileSync(config.sourceFile, 'utf8');
106120
this.sourceFile = ts.createSourceFile(config.sourceFile, sourceCode, ts.ScriptTarget.Latest, true);
107121
this.translator = config.translator;
108122
this.compareRef = config.compareRef;
123+
this.paths = config.paths;
109124
this.verbose = config.verbose;
110125
}
111126

@@ -115,8 +130,11 @@ class TranslationGenerator {
115130
// map of translations for each locale
116131
const translations = new Map<TranslationTargetLocale, Map<number, string>>();
117132

118-
// If a compareRef is provided, fetch the old version of the files, and traverse the ASTs in parallel to extract existing translations
119-
if (this.compareRef) {
133+
// If paths are specified, we only retranslate those paths and skip comparing with existing translations
134+
const shouldUseExistingTranslations = !this.paths && this.compareRef;
135+
136+
// If a compareRef is provided and we're not filtering by paths, fetch the old version of the files, and traverse the ASTs in parallel to extract existing translations
137+
if (shouldUseExistingTranslations) {
120138
const allLocales: Locale[] = [LOCALES.EN, ...this.targetLanguages];
121139

122140
// An array of labeled "translation nodes", where "translations node" refers to the main object in en.ts and
@@ -192,6 +210,7 @@ class TranslationGenerator {
192210
const translationsForLocale = translations.get(targetLanguage) ?? new Map<number, string>();
193211

194212
// Extract strings to translate
213+
// TODO: filter stringsToTranslate based on paths
195214
const stringsToTranslate = new Map<number, StringWithContext>();
196215
this.extractStringsToTranslate(this.sourceFile, stringsToTranslate);
197216

@@ -597,6 +616,17 @@ async function main(): Promise<void> {
597616
'For incremental translations, this ref is the previous version of the codebase to compare to. Only strings that changed or had their context changed since this ref will be retranslated.',
598617
default: '',
599618
},
619+
paths: {
620+
description: 'Comma-separated list of specific translation paths to retranslate (e.g., "common.save,errors.generic").',
621+
parse: (val: string): TranslationPaths[] => {
622+
const rawPaths = val.split(',').map((translationPath) => translationPath.trim());
623+
const validatedPaths: TranslationPaths[] = [];
624+
625+
// TODO: validate paths
626+
},
627+
supersedes: ['compare-ref'],
628+
required: false,
629+
},
600630
},
601631
} as const);
602632
/* eslint-enable @typescript-eslint/naming-convention */
@@ -626,6 +656,7 @@ async function main(): Promise<void> {
626656
sourceFile: enSourceFile,
627657
translator,
628658
compareRef: cli.namedArgs['compare-ref'],
659+
paths: cli.namedArgs.paths,
629660
verbose: cli.flags.verbose,
630661
});
631662

tests/unit/generateTranslationsTest.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,4 +561,187 @@ describe('generateTranslations', () => {
561561

562562
fs.rmSync(oldDir, {recursive: true});
563563
});
564+
565+
it('translates only specified paths when --paths is provided', async () => {
566+
fs.writeFileSync(
567+
EN_PATH,
568+
dedent(`
569+
const strings = {
570+
greeting: 'Hello',
571+
farewell: 'Goodbye',
572+
common: {
573+
save: 'Save',
574+
cancel: 'Cancel',
575+
},
576+
errors: {
577+
generic: 'An error occurred',
578+
network: 'Network error',
579+
},
580+
};
581+
export default strings;
582+
`),
583+
'utf8',
584+
);
585+
586+
// Override process.argv to specify only certain paths
587+
process.argv = ['ts-node', 'generateTranslations.ts', '--dry-run', '--verbose', '--locales', 'it', '--paths', 'common.save,errors.generic'];
588+
589+
const translateSpy = jest.spyOn(Translator.prototype, 'translate');
590+
591+
await generateTranslations();
592+
const itContent = fs.readFileSync(IT_PATH, 'utf8');
593+
594+
// Only the specified paths should be translated
595+
expect(itContent).toContain('[it] Save');
596+
expect(itContent).toContain('[it] An error occurred');
597+
598+
// Other paths should not be translated
599+
expect(itContent).not.toContain('[it] Hello');
600+
expect(itContent).not.toContain('[it] Goodbye');
601+
expect(itContent).not.toContain('[it] Cancel');
602+
expect(itContent).not.toContain('[it] Network error');
603+
604+
expect(translateSpy).toHaveBeenCalledTimes(2);
605+
expect(translateSpy).toHaveBeenCalledWith('it', 'Save', undefined);
606+
expect(translateSpy).toHaveBeenCalledWith('it', 'An error occurred', undefined);
607+
});
608+
609+
it('translates nested paths when parent path is specified', async () => {
610+
fs.writeFileSync(
611+
EN_PATH,
612+
dedent(`
613+
const strings = {
614+
greeting: 'Hello',
615+
common: {
616+
save: 'Save',
617+
cancel: 'Cancel',
618+
nested: {
619+
deep: 'Deep value',
620+
},
621+
},
622+
errors: {
623+
generic: 'An error occurred',
624+
},
625+
};
626+
export default strings;
627+
`),
628+
'utf8',
629+
);
630+
631+
// Override process.argv to specify parent path
632+
process.argv = ['ts-node', 'generateTranslations.ts', '--dry-run', '--verbose', '--locales', 'it', '--paths', 'common'];
633+
634+
const translateSpy = jest.spyOn(Translator.prototype, 'translate');
635+
636+
await generateTranslations();
637+
const itContent = fs.readFileSync(IT_PATH, 'utf8');
638+
639+
// All nested paths under 'common' should be translated
640+
expect(itContent).toContain('[it] Save');
641+
expect(itContent).toContain('[it] Cancel');
642+
expect(itContent).toContain('[it] Deep value');
643+
644+
// Other paths should not be translated
645+
expect(itContent).not.toContain('[it] Hello');
646+
expect(itContent).not.toContain('[it] An error occurred');
647+
648+
expect(translateSpy).toHaveBeenCalledTimes(3);
649+
expect(translateSpy).toHaveBeenCalledWith('it', 'Save', undefined);
650+
expect(translateSpy).toHaveBeenCalledWith('it', 'Cancel', undefined);
651+
expect(translateSpy).toHaveBeenCalledWith('it', 'Deep value', undefined);
652+
});
653+
654+
it('ignores --compare-ref when --paths is provided', async () => {
655+
fs.writeFileSync(
656+
EN_PATH,
657+
dedent(`
658+
const strings = {
659+
greeting: 'Hello',
660+
common: {
661+
save: 'Save',
662+
},
663+
};
664+
export default strings;
665+
`),
666+
'utf8',
667+
);
668+
669+
// Override process.argv to specify both paths and compare-ref
670+
process.argv = ['ts-node', 'generateTranslations.ts', '--dry-run', '--verbose', '--locales', 'it', '--paths', 'common.save', '--compare-ref', 'main'];
671+
672+
const translateSpy = jest.spyOn(Translator.prototype, 'translate');
673+
const githubSpy = jest.spyOn(GitHubUtils, 'getFileContents');
674+
675+
await generateTranslations();
676+
const itContent = fs.readFileSync(IT_PATH, 'utf8');
677+
678+
// Only the specified path should be translated
679+
expect(itContent).toContain('[it] Save');
680+
expect(itContent).not.toContain('[it] Hello');
681+
682+
// GitHubUtils.getFileContents should not be called since --compare-ref is ignored
683+
expect(githubSpy).not.toHaveBeenCalled();
684+
expect(translateSpy).toHaveBeenCalledTimes(1);
685+
expect(translateSpy).toHaveBeenCalledWith('it', 'Save', undefined);
686+
});
687+
688+
it('throws error for invalid paths', async () => {
689+
fs.writeFileSync(
690+
EN_PATH,
691+
dedent(`
692+
const strings = {
693+
greeting: 'Hello',
694+
common: {
695+
save: 'Save',
696+
},
697+
};
698+
export default strings;
699+
`),
700+
'utf8',
701+
);
702+
703+
// Override process.argv to specify a non-existent path
704+
process.argv = ['ts-node', 'generateTranslations.ts', '--dry-run', '--verbose', '--locales', 'it', '--paths', 'nonexistent.path'];
705+
706+
// Expect the script to throw an error during CLI parsing
707+
await expect(generateTranslations()).rejects.toThrow('Translation path not found: nonexistent.path');
708+
});
709+
710+
it('validates paths against actual translation structure', async () => {
711+
fs.writeFileSync(
712+
EN_PATH,
713+
dedent(`
714+
const strings = {
715+
greeting: 'Hello',
716+
common: {
717+
save: 'Save',
718+
cancel: 'Cancel',
719+
},
720+
errors: {
721+
generic: 'An error occurred',
722+
},
723+
};
724+
export default strings;
725+
`),
726+
'utf8',
727+
);
728+
729+
// Test that valid paths work
730+
process.argv = ['ts-node', 'generateTranslations.ts', '--dry-run', '--verbose', '--locales', 'it', '--paths', 'greeting,common.save'];
731+
732+
const translateSpy = jest.spyOn(Translator.prototype, 'translate');
733+
734+
await generateTranslations();
735+
const itContent = fs.readFileSync(IT_PATH, 'utf8');
736+
737+
// Should translate the specified paths
738+
expect(itContent).toContain('[it] Hello');
739+
expect(itContent).toContain('[it] Save');
740+
741+
// Should not translate other paths
742+
expect(itContent).not.toContain('[it] Cancel');
743+
expect(itContent).not.toContain('[it] An error occurred');
744+
745+
expect(translateSpy).toHaveBeenCalledTimes(2);
746+
});
564747
});

0 commit comments

Comments
 (0)