mirror of
https://github.com/bitwarden/browser
synced 2026-02-06 03:33:30 +00:00
Handle params
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import * as fs from "fs";
|
||||
|
||||
import { CombinedTranslations, TranslationCombiner } from "./translation-combiner";
|
||||
import {
|
||||
CombinedTranslations,
|
||||
TranslationCombiner,
|
||||
TranslationEntry,
|
||||
} from "./translation-combiner";
|
||||
|
||||
/**
|
||||
* Service for looking up translations during template migration
|
||||
@@ -49,6 +53,17 @@ export class TranslationLookup {
|
||||
return entry ? entry.message : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full translation entry for a key
|
||||
*/
|
||||
getTranslationEntry(key: string): TranslationEntry | null {
|
||||
if (!this.isLoaded) {
|
||||
throw new Error("Translations not loaded. Call loadTranslations() first.");
|
||||
}
|
||||
|
||||
return this.translations[key] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation with fallback to key if not found
|
||||
*/
|
||||
|
||||
@@ -7,11 +7,58 @@ describe("ASTTransformer", () => {
|
||||
let transformer: ASTTransformer;
|
||||
let sourceFile: SourceFile;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
project = new Project({
|
||||
useInMemoryFileSystem: true,
|
||||
});
|
||||
transformer = new ASTTransformer();
|
||||
|
||||
// Initialize with mock translations for testing
|
||||
await transformer.initialize();
|
||||
|
||||
// Mock the translation lookup to return predictable results for tests
|
||||
const mockTranslationEntries: Record<string, any> = {
|
||||
loginWithDevice: { message: "loginWithDevice" },
|
||||
itemsCount: {
|
||||
message: "itemsCount $COUNT$",
|
||||
placeholders: {
|
||||
count: { content: "$1" },
|
||||
},
|
||||
},
|
||||
testMessage: { message: "testMessage" },
|
||||
simpleMessage: { message: "simpleMessage" },
|
||||
itemCount: {
|
||||
message: "itemCount $COUNT$",
|
||||
placeholders: {
|
||||
count: { content: "$1" },
|
||||
},
|
||||
},
|
||||
message1: { message: "message1" },
|
||||
message2: {
|
||||
message: "message2 $PARAM$",
|
||||
placeholders: {
|
||||
param: { content: "$1" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "getTranslation")
|
||||
.mockImplementation((key: string) => {
|
||||
return mockTranslationEntries[key]?.message || null;
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "getTranslationEntry")
|
||||
.mockImplementation((key: string) => {
|
||||
return mockTranslationEntries[key] || null;
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "hasTranslation")
|
||||
.mockImplementation((key: string) => {
|
||||
return key in mockTranslationEntries;
|
||||
});
|
||||
});
|
||||
|
||||
it("should find I18nService.t() calls", () => {
|
||||
@@ -58,7 +105,7 @@ describe("ASTTransformer", () => {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
test() {
|
||||
const message = $localize\`loginWithDevice\`;
|
||||
const message = $localize\`:@@loginWithDevice:loginWithDevice\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -81,7 +128,7 @@ describe("ASTTransformer", () => {
|
||||
const expected = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`itemsCount\${count.toString()}:param0:\`;
|
||||
const message = $localize\`:@@itemsCount:itemsCount \${count.toString()}:count:\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -140,7 +187,7 @@ describe("ASTTransformer", () => {
|
||||
@Component({})
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`loginWithDevice\`;
|
||||
const message = $localize\`:@@loginWithDevice:loginWithDevice\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -185,16 +232,16 @@ describe("ASTTransformer", () => {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
getMessage() {
|
||||
return $localize\`simpleMessage\`;
|
||||
return $localize\`:@@simpleMessage:simpleMessage\`;
|
||||
}
|
||||
|
||||
getParameterizedMessage(count: number) {
|
||||
return $localize\`itemCount\${count.toString()}:param0:\`;
|
||||
return $localize\`:@@itemCount:itemCount \${count.toString()}:count:\`;
|
||||
}
|
||||
|
||||
getMultipleMessages() {
|
||||
const msg1 = $localize\`message1\`;
|
||||
const msg2 = $localize\`message2\${'param'}:param0:\`;
|
||||
const msg1 = $localize\`:@@message1:message1\`;
|
||||
const msg2 = $localize\`:@@message2:message2 \${'param'}:param:\`;
|
||||
return [msg1, msg2];
|
||||
}
|
||||
}
|
||||
@@ -220,7 +267,7 @@ describe("ASTTransformer", () => {
|
||||
const expected = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`testMessage\`;
|
||||
const message = $localize\`:@@testMessage:testMessage\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -230,4 +277,86 @@ describe("ASTTransformer", () => {
|
||||
|
||||
expect(sourceFile.getFullText().trim()).toBe(expected.trim());
|
||||
});
|
||||
|
||||
it("should use translation lookup to generate proper $localize calls with actual text", () => {
|
||||
const code = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = this.i18nService.t('loginWithDevice');
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`:@@loginWithDevice:loginWithDevice\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
sourceFile = project.createSourceFile("translation-lookup-test.ts", code);
|
||||
transformer.transformI18nServiceCalls(sourceFile);
|
||||
|
||||
expect(sourceFile.getFullText().trim()).toBe(expected.trim());
|
||||
});
|
||||
|
||||
it("should handle parameter substitution with translation lookup", () => {
|
||||
// Mock translation with parameter placeholder in $VAR$ format
|
||||
const mockTranslationEntry = {
|
||||
message: "Items: $COUNT$",
|
||||
placeholders: {
|
||||
count: { content: "$1" },
|
||||
},
|
||||
};
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "getTranslationEntry")
|
||||
.mockReturnValue(mockTranslationEntry);
|
||||
jest.spyOn(transformer["translationLookup"], "hasTranslation").mockReturnValue(true);
|
||||
|
||||
const code = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = this.i18nService.t('itemsCount', count.toString());
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`:@@itemsCount:Items: \${count.toString()}:count:\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
sourceFile = project.createSourceFile("param-translation-test.ts", code);
|
||||
transformer.transformI18nServiceCalls(sourceFile);
|
||||
|
||||
expect(sourceFile.getFullText().trim()).toBe(expected.trim());
|
||||
});
|
||||
|
||||
it("should fallback to key when translation is not found", () => {
|
||||
const code = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = this.i18nService.t('unknownKey');
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const expected = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`:@@unknownKey:unknownKey\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
sourceFile = project.createSourceFile("fallback-test.ts", code);
|
||||
const result = transformer.transformI18nServiceCalls(sourceFile);
|
||||
|
||||
expect(sourceFile.getFullText().trim()).toBe(expected.trim());
|
||||
expect(result.errors).toContain("Warning: No translation found for key 'unknownKey' at line 4");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { SourceFile, Node } from "ts-morph";
|
||||
|
||||
import { TranslationLookup } from "../shared/translation-lookup";
|
||||
import { TransformationResult, TransformationChange, I18nUsage } from "../shared/types";
|
||||
|
||||
/**
|
||||
* AST transformation utilities for TypeScript code migration
|
||||
*/
|
||||
export class ASTTransformer {
|
||||
private translationLookup: TranslationLookup;
|
||||
|
||||
constructor(rootPath?: string) {
|
||||
this.translationLookup = new TranslationLookup(rootPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the translation lookup system
|
||||
*/
|
||||
async initialize(combinedFilePath?: string): Promise<void> {
|
||||
await this.translationLookup.loadTranslations(combinedFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all I18nService.t() method calls in a source file
|
||||
*/
|
||||
@@ -79,6 +93,12 @@ export class ASTTransformer {
|
||||
// Generate $localize replacement
|
||||
const replacement = this.generateLocalizeCall(key, args.slice(1));
|
||||
|
||||
// Check if translation was found
|
||||
const hasTranslation = this.translationLookup.hasTranslation(key);
|
||||
if (!hasTranslation) {
|
||||
errors.push(`Warning: No translation found for key '${key}' at line ${line}`);
|
||||
}
|
||||
|
||||
// Replace the node
|
||||
node.replaceWithText(replacement);
|
||||
|
||||
@@ -87,7 +107,7 @@ export class ASTTransformer {
|
||||
location: { line, column },
|
||||
original,
|
||||
replacement,
|
||||
description: `Replaced i18nService.t('${key}') with $localize`,
|
||||
description: `Replaced i18nService.t('${key}') with $localize${hasTranslation ? "" : " (translation not found)"}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -139,17 +159,77 @@ export class ASTTransformer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate $localize call with parameters
|
||||
* Generate $localize call with parameters using actual translation text
|
||||
*/
|
||||
private generateLocalizeCall(key: string, paramArgs: Node[]): string {
|
||||
// Get the full translation entry from the lookup
|
||||
const translationEntry = this.translationLookup.getTranslationEntry(key);
|
||||
const messageText = translationEntry?.message || key; // Fallback to key if translation not found
|
||||
|
||||
if (paramArgs.length === 0) {
|
||||
return `$localize\`${key}\``;
|
||||
// Simple case: no parameters
|
||||
return `$localize\`:@@${key}:${this.escapeForTemplate(messageText)}\``;
|
||||
}
|
||||
|
||||
// For now, handle simple parameter substitution
|
||||
// This will need to be enhanced for complex cases
|
||||
const params = paramArgs.map((arg, index) => `\${${arg.getText()}}:param${index}:`);
|
||||
return `$localize\`${key}${params.join("")}\``;
|
||||
// Handle parameter substitution using the placeholders object
|
||||
let processedMessage = messageText;
|
||||
const placeholders = translationEntry?.placeholders || {};
|
||||
|
||||
// Create a map of parameter positions to arguments based on placeholders
|
||||
const paramMap = new Map<string, { arg: string; paramName: string }>();
|
||||
|
||||
// Map placeholders to parameter arguments
|
||||
Object.entries(placeholders).forEach(([placeholderName, placeholderInfo]) => {
|
||||
const content = placeholderInfo.content;
|
||||
if (content && content.startsWith("$") && content.length > 1) {
|
||||
// Extract parameter number from content like "$1", "$2", etc.
|
||||
const paramNumber = parseInt(content.substring(1));
|
||||
if (!isNaN(paramNumber) && paramNumber > 0 && paramNumber <= paramArgs.length) {
|
||||
const argIndex = paramNumber - 1;
|
||||
paramMap.set(placeholderName.toUpperCase(), {
|
||||
arg: paramArgs[argIndex].getText(),
|
||||
paramName: placeholderName,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Replace $VAR$ placeholders in the message with $localize parameter syntax
|
||||
paramMap.forEach(({ arg, paramName }, placeholderName) => {
|
||||
const placeholder = `$${placeholderName}$`;
|
||||
if (processedMessage.includes(placeholder)) {
|
||||
processedMessage = processedMessage.replace(placeholder, `\${${arg}}:${paramName}:`);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any remaining parameters that weren't mapped through placeholders
|
||||
// This is a fallback for cases where placeholders might not be properly defined
|
||||
paramArgs.forEach((arg, index) => {
|
||||
const paramName = `param${index}`;
|
||||
const genericPlaceholder = `$${index + 1}$`;
|
||||
if (processedMessage.includes(genericPlaceholder)) {
|
||||
processedMessage = processedMessage.replace(
|
||||
genericPlaceholder,
|
||||
`\${${arg.getText()}}:${paramName}:`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return `$localize\`:@@${key}:${this.escapeForTemplate(processedMessage)}\``;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special characters for template literal usage
|
||||
* Preserves $localize parameter syntax like ${param}:name:
|
||||
*/
|
||||
private escapeForTemplate(text: string): string {
|
||||
return (
|
||||
text
|
||||
.replace(/\\/g, "\\\\") // Escape backslashes
|
||||
.replace(/`/g, "\\`") // Escape backticks
|
||||
// Don't escape $ that are part of ${...}: parameter syntax
|
||||
.replace(/\$(?!\{[^}]+\}:[^:]*:)/g, "\\$")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -152,11 +152,11 @@ describe("BatchMigrator", () => {
|
||||
|
||||
// Verify files were transformed
|
||||
const transformedFile1 = fs.readFileSync(testFiles[0].path, "utf8");
|
||||
expect(transformedFile1).toContain("$localize`message1`");
|
||||
expect(transformedFile1).toContain("$localize`:@@message1:message1`");
|
||||
expect(transformedFile1).not.toContain("i18nService.t(");
|
||||
|
||||
const transformedFile2 = fs.readFileSync(testFiles[1].path, "utf8");
|
||||
expect(transformedFile2).toContain("$localize`message2${");
|
||||
expect(transformedFile2).toContain("$localize`:@@message2:message2");
|
||||
expect(transformedFile2).not.toContain("I18nService");
|
||||
});
|
||||
|
||||
@@ -211,7 +211,7 @@ describe("BatchMigrator", () => {
|
||||
|
||||
// Valid file should be processed
|
||||
const validContent = fs.readFileSync(validFile, "utf8");
|
||||
expect(validContent).toContain("$localize`valid`");
|
||||
expect(validContent).toContain("$localize`:@@valid:valid`");
|
||||
});
|
||||
|
||||
it("should validate migration results", async () => {
|
||||
@@ -338,14 +338,14 @@ describe("BatchMigrator", () => {
|
||||
|
||||
// Step 3: Verify transformed content
|
||||
const authContent = fs.readFileSync(files[0].path, "utf8");
|
||||
expect(authContent).toContain("$localize`loginRequired`");
|
||||
expect(authContent).toContain("$localize`errorCount${count.toString()}:param0:`");
|
||||
expect(authContent).toContain("$localize`:@@loginRequired:loginRequired`");
|
||||
expect(authContent).toContain("$localize`:@@errorCount:errorCount");
|
||||
expect(authContent).not.toContain("i18nService.t(");
|
||||
|
||||
const vaultContent = fs.readFileSync(files[1].path, "utf8");
|
||||
expect(vaultContent).toContain("$localize`vaultLocked`");
|
||||
expect(vaultContent).toContain("$localize`vaultUnlocked`");
|
||||
expect(vaultContent).toContain("$localize`unknownStatus${status}:param0:`");
|
||||
expect(vaultContent).toContain("$localize`:@@vaultLocked:vaultLocked`");
|
||||
expect(vaultContent).toContain("$localize`:@@vaultUnlocked:vaultUnlocked`");
|
||||
expect(vaultContent).toContain("$localize`:@@unknownStatus:unknownStatus");
|
||||
expect(vaultContent).not.toContain("i18n.t(");
|
||||
|
||||
// Step 4: Verify reports were generated
|
||||
|
||||
@@ -57,6 +57,7 @@ program
|
||||
.option("-f, --file <path>", "Migrate specific file only")
|
||||
.option("-d, --dry-run", "Preview changes without applying them")
|
||||
.option("-o, --output <path>", "Output directory for migration reports")
|
||||
.option("-t, --translations <path>", "Path to combined translations file")
|
||||
.option("-v, --verbose", "Enable verbose logging")
|
||||
.option("--backup", "Create backup files before migration")
|
||||
.action(async (options) => {
|
||||
@@ -68,7 +69,7 @@ program
|
||||
verbose: options.verbose || false,
|
||||
};
|
||||
|
||||
const migrator = new TypeScriptMigrator(config);
|
||||
const migrator = new TypeScriptMigrator(config, options.translations);
|
||||
|
||||
if (options.backup && !options.dryRun) {
|
||||
console.log(chalk.yellow("📦 Creating backups..."));
|
||||
|
||||
78
scripts/migration/i18n/typescript/demo-parameter-handling.ts
Normal file
78
scripts/migration/i18n/typescript/demo-parameter-handling.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { Project } from "ts-morph";
|
||||
import { ASTTransformer } from "./ast-transformer";
|
||||
|
||||
async function demonstrateParameterHandling() {
|
||||
console.log("🔧 Demonstrating Parameter Handling with Translation Lookup\n");
|
||||
|
||||
const project = new Project({
|
||||
useInMemoryFileSystem: true,
|
||||
});
|
||||
|
||||
const transformer = new ASTTransformer();
|
||||
await transformer.initialize();
|
||||
|
||||
// Mock a real translation entry like those found in the actual translation files
|
||||
const mockTranslationEntry = {
|
||||
message: "Data last updated: $DATE$",
|
||||
placeholders: {
|
||||
date: {
|
||||
content: "$1",
|
||||
example: "2021-01-01",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Mock the translation lookup
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "getTranslationEntry")
|
||||
.mockReturnValue(mockTranslationEntry);
|
||||
jest.spyOn(transformer["translationLookup"], "hasTranslation").mockReturnValue(true);
|
||||
|
||||
const code = `
|
||||
class DataComponent {
|
||||
updateStatus() {
|
||||
const message = this.i18nService.t('dataLastUpdated', this.lastUpdateDate);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
console.log("📝 Original Code:");
|
||||
console.log(code);
|
||||
|
||||
const sourceFile = project.createSourceFile("demo.ts", code);
|
||||
const result = transformer.transformI18nServiceCalls(sourceFile);
|
||||
|
||||
console.log("\n✨ Transformed Code:");
|
||||
console.log(sourceFile.getFullText());
|
||||
|
||||
console.log("\n📊 Transformation Result:");
|
||||
console.log(`- Success: ${result.success}`);
|
||||
console.log(`- Changes: ${result.changes.length}`);
|
||||
console.log(`- Errors: ${result.errors.length}`);
|
||||
|
||||
if (result.changes.length > 0) {
|
||||
console.log("\n🔄 Changes Made:");
|
||||
result.changes.forEach((change, index) => {
|
||||
console.log(` ${index + 1}. ${change.description}`);
|
||||
console.log(` Original: ${change.original}`);
|
||||
console.log(` Replacement: ${change.replacement}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log("\n✅ Key Features Demonstrated:");
|
||||
console.log("- ✅ Uses actual translation text from lookup");
|
||||
console.log("- ✅ Handles $VAR$ placeholder format correctly");
|
||||
console.log("- ✅ Maps placeholders to parameter names");
|
||||
console.log("- ✅ Generates proper $localize syntax with @@ID");
|
||||
console.log("- ✅ Preserves parameter order and names");
|
||||
}
|
||||
|
||||
// Only run if this file is executed directly
|
||||
if (require.main === module) {
|
||||
demonstrateParameterHandling().catch(console.error);
|
||||
}
|
||||
|
||||
export { demonstrateParameterHandling };
|
||||
@@ -15,8 +15,53 @@ describe("TypeScript Migration Tools", () => {
|
||||
let transformer: ASTTransformer;
|
||||
let sourceFile: SourceFile;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
transformer = new ASTTransformer();
|
||||
await transformer.initialize();
|
||||
|
||||
// Mock the translation lookup to return predictable results for tests
|
||||
const mockTranslationEntries: Record<string, any> = {
|
||||
loginWithDevice: { message: "loginWithDevice" },
|
||||
itemsCount: {
|
||||
message: "itemsCount $COUNT$",
|
||||
placeholders: {
|
||||
count: { content: "$1" },
|
||||
},
|
||||
},
|
||||
testMessage: { message: "testMessage" },
|
||||
simpleMessage: { message: "simpleMessage" },
|
||||
itemCount: {
|
||||
message: "itemCount $COUNT$",
|
||||
placeholders: {
|
||||
count: { content: "$1" },
|
||||
},
|
||||
},
|
||||
message1: { message: "message1" },
|
||||
message2: {
|
||||
message: "message2 $PARAM$",
|
||||
placeholders: {
|
||||
param: { content: "$1" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "getTranslation")
|
||||
.mockImplementation((key: string) => {
|
||||
return mockTranslationEntries[key]?.message || null;
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "getTranslationEntry")
|
||||
.mockImplementation((key: string) => {
|
||||
return mockTranslationEntries[key] || null;
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "hasTranslation")
|
||||
.mockImplementation((key: string) => {
|
||||
return key in mockTranslationEntries;
|
||||
});
|
||||
});
|
||||
|
||||
it("should find I18nService.t() calls", () => {
|
||||
@@ -63,7 +108,7 @@ describe("TypeScript Migration Tools", () => {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
test() {
|
||||
const message = $localize\`loginWithDevice\`;
|
||||
const message = $localize\`:@@loginWithDevice:loginWithDevice\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -86,7 +131,7 @@ describe("TypeScript Migration Tools", () => {
|
||||
const expected = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`itemsCount\${count.toString()}:param0:\`;
|
||||
const message = $localize\`:@@itemsCount:itemsCount \${count.toString()}:count:\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -145,7 +190,7 @@ describe("TypeScript Migration Tools", () => {
|
||||
@Component({})
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`loginWithDevice\`;
|
||||
const message = $localize\`:@@loginWithDevice:loginWithDevice\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -158,8 +203,55 @@ describe("TypeScript Migration Tools", () => {
|
||||
});
|
||||
|
||||
describe("Integration Tests", () => {
|
||||
it("should handle complex transformation scenarios", () => {
|
||||
function setupMocks(transformer: ASTTransformer) {
|
||||
const mockTranslationEntries: Record<string, any> = {
|
||||
loginWithDevice: { message: "loginWithDevice" },
|
||||
itemsCount: {
|
||||
message: "itemsCount $COUNT$",
|
||||
placeholders: {
|
||||
count: { content: "$1" },
|
||||
},
|
||||
},
|
||||
testMessage: { message: "testMessage" },
|
||||
simpleMessage: { message: "simpleMessage" },
|
||||
itemCount: {
|
||||
message: "itemCount $COUNT$",
|
||||
placeholders: {
|
||||
count: { content: "$1" },
|
||||
},
|
||||
},
|
||||
message1: { message: "message1" },
|
||||
message2: {
|
||||
message: "message2 $PARAM$",
|
||||
placeholders: {
|
||||
param: { content: "$1" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "getTranslation")
|
||||
.mockImplementation((key: string) => {
|
||||
return mockTranslationEntries[key]?.message || null;
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "getTranslationEntry")
|
||||
.mockImplementation((key: string) => {
|
||||
return mockTranslationEntries[key] || null;
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(transformer["translationLookup"], "hasTranslation")
|
||||
.mockImplementation((key: string) => {
|
||||
return key in mockTranslationEntries;
|
||||
});
|
||||
}
|
||||
|
||||
it("should handle complex transformation scenarios", async () => {
|
||||
const transformer = new ASTTransformer();
|
||||
await transformer.initialize();
|
||||
setupMocks(transformer);
|
||||
const code = `
|
||||
import { I18nService } from '@bitwarden/common/platform/services/i18n.service';
|
||||
import { Component } from '@angular/core';
|
||||
@@ -193,16 +285,16 @@ describe("TypeScript Migration Tools", () => {
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
getMessage() {
|
||||
return $localize\`simpleMessage\`;
|
||||
return $localize\`:@@simpleMessage:simpleMessage\`;
|
||||
}
|
||||
|
||||
getParameterizedMessage(count: number) {
|
||||
return $localize\`itemCount\${count.toString()}:param0:\`;
|
||||
return $localize\`:@@itemCount:itemCount \${count.toString()}:count:\`;
|
||||
}
|
||||
|
||||
getMultipleMessages() {
|
||||
const msg1 = $localize\`message1\`;
|
||||
const msg2 = $localize\`message2\${'param'}:param0:\`;
|
||||
const msg1 = $localize\`:@@message1:message1\`;
|
||||
const msg2 = $localize\`:@@message2:message2 \${'param'}:param:\`;
|
||||
return [msg1, msg2];
|
||||
}
|
||||
}
|
||||
@@ -214,8 +306,10 @@ describe("TypeScript Migration Tools", () => {
|
||||
expect(sourceFile.getFullText().trim()).toBe(expected.trim());
|
||||
});
|
||||
|
||||
it("should remove import when only method calls are used (no constructor)", () => {
|
||||
it("should remove import when only method calls are used (no constructor)", async () => {
|
||||
const transformer = new ASTTransformer();
|
||||
await transformer.initialize();
|
||||
setupMocks(transformer);
|
||||
const code = `
|
||||
import { I18nService } from '@bitwarden/common/platform/services/i18n.service';
|
||||
|
||||
@@ -229,7 +323,7 @@ describe("TypeScript Migration Tools", () => {
|
||||
const expected = `
|
||||
class TestComponent {
|
||||
test() {
|
||||
const message = $localize\`testMessage\`;
|
||||
const message = $localize\`:@@testMessage:testMessage\`;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -11,7 +11,10 @@ export class TypeScriptMigrator {
|
||||
private parser: ProjectParser;
|
||||
private transformer: ASTTransformer;
|
||||
|
||||
constructor(private config: MigrationConfig) {
|
||||
constructor(
|
||||
private config: MigrationConfig,
|
||||
private translationsPath?: string,
|
||||
) {
|
||||
this.parser = new ProjectParser(config);
|
||||
this.transformer = new ASTTransformer();
|
||||
}
|
||||
@@ -91,6 +94,8 @@ export class TypeScriptMigrator {
|
||||
* Migrate all TypeScript files in the project
|
||||
*/
|
||||
async migrateAll(): Promise<TransformationResult[]> {
|
||||
await this.transformer.initialize(this.translationsPath);
|
||||
|
||||
const sourceFiles = this.parser.findI18nServiceImports();
|
||||
const results: TransformationResult[] = [];
|
||||
|
||||
@@ -123,6 +128,8 @@ export class TypeScriptMigrator {
|
||||
* Migrate a specific file
|
||||
*/
|
||||
async migrateFile(filePath: string): Promise<TransformationResult> {
|
||||
await this.transformer.initialize(this.translationsPath);
|
||||
|
||||
const sourceFile = this.parser.getSourceFile(filePath);
|
||||
|
||||
if (!sourceFile) {
|
||||
|
||||
Reference in New Issue
Block a user