1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-06 11:43:51 +00:00
This commit is contained in:
Hinton
2025-07-28 10:19:23 +02:00
parent 30af25cf15
commit 9d76924a6d
18 changed files with 2739 additions and 206 deletions

View File

@@ -0,0 +1,344 @@
# Design Document
## Overview
This design outlines the migration from Bitwarden's custom i18n system (I18nService and I18nPipe) to Angular's built-in localization system (@angular/localize) with runtime locale switching support. The migration will replace the custom translation system with Angular's standard localization while maintaining the ability to dynamically switch locales at runtime, which is essential for user preference management.
The current system uses a custom TranslationService and I18nService that loads JSON translation files at runtime and provides a `t()` method for translations and an `i18n` pipe for templates. The new system will use Angular's `$localize` function for TypeScript code and `i18n` attributes for templates, with runtime locale loading and switching capabilities as supported by Angular's recent updates.
## Architecture
### Current System Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Templates │ │ TypeScript │ │ Translation │
│ {{ 'key' | │ │ i18n.t('key') │ │ JSON Files │
│ i18n }} │ │ │ │ (en.json, etc) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
┌─────────────────────┐
│ I18nService │
│ - t() method │
│ - setLocale() │
│ - locale$ │
└─────────────────────┘
┌─────────────────────┐
│ TranslationService │
│ - loadMessages() │
│ - translate() │
└─────────────────────┘
```
### New System Architecture (Runtime Localization)
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Templates │ │ TypeScript │ │ XLIFF Files │
│ <span i18n> │ │ $localize │ │ (messages.xlf │
│ Text</span> │ │ `Text` │ │ per locale) │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
┌─────────────────────┐
│ Angular Runtime │
│ - loadTranslations │
│ - $localize runtime│
│ - Dynamic switching│
└─────────────────────┘
┌─────────────────────┐
│ Runtime Locale │
│ Management │
│ - Locale switching │
│ - Translation load │
└─────────────────────┘
```
## Components and Interfaces
### 1. Translation Extraction and Build Configuration
**Angular CLI Configuration (`angular.json`)**
- Configure i18n extraction and build options
- Define supported locales and source locale
- Set up build configurations for each locale
**Extraction Configuration**
- Use Angular CLI's `ng extract-i18n` command
- Configure extraction to generate XLIFF 2.0 files
- Set up automated extraction in build pipeline
### 2. Template Migration System
**Template Transformer**
- Parse Angular templates using angular-eslint's template parser
- Identify `| i18n` pipe usage patterns
- Transform to `i18n` attributes with proper IDs and descriptions
- Handle complex cases like interpolation and pluralization
**Migration Patterns:**
```html
<!-- Before -->
{{ 'loginWithDevice' | i18n }} {{ 'itemsCount' | i18n: count }}
<!-- After -->
<span i18n="@@loginWithDevice">Log in with device</span>
<span i18n="@@itemsCount" i18n-plural>{count, plural, =1 {1 item} other {{{count}} items}}</span>
```
### 3. TypeScript Code Migration System
**Code Transformer using ts-morph**
- Parse TypeScript files to find I18nService usage
- Replace `i18nService.t()` calls with `$localize` calls
- Handle parameter substitution and maintain type safety
- Update imports and remove I18nService dependencies
**Migration Patterns:**
```typescript
// Before
this.i18nService.t("loginWithDevice");
this.i18nService.t("itemsCount", count.toString());
// After
$localize`Log in with device`;
$localize`${count}:count: items`;
```
### 4. Locale Management System
**Runtime Locale Service**
- Create a service to handle dynamic locale switching at runtime
- Integrate with Angular's runtime localization APIs
- Load translation files dynamically without application restart
- Maintain compatibility with existing locale persistence
- Handle locale switching for SPA scenarios
**Interface:**
```typescript
export interface RuntimeLocaleService {
currentLocale$: Observable<string>;
supportedLocales: string[];
setLocale(locale: string): Promise<void>;
loadTranslations(locale: string): Promise<void>;
getLocaleDisplayName(locale: string): string;
}
```
### 5. Build System Integration
**Webpack Configuration Updates**
- Update webpack configs to support runtime i18n loading
- Configure dynamic import of translation files
- Set up proper chunking for translation resources
- Integrate with existing build pipeline
**Build Scripts**
- Create scripts for extracting translations
- Set up merge process for updated translations
- Configure single build with runtime translation loading
- Implement translation file serving and caching logic
## Data Models
### Translation File Format Migration
**Current JSON Format:**
```json
{
"loginWithDevice": {
"message": "Log in with device"
},
"itemsCount": {
"message": "Items: __$1__",
"placeholders": {
"count": {
"content": "$1"
}
}
}
}
```
**New XLIFF Format:**
```xml
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0">
<file id="ngi18n" source-language="en-US">
<unit id="loginWithDevice">
<segment>
<source>Log in with device</source>
</segment>
</unit>
<unit id="itemsCount">
<segment>
<source>{count, plural, =1 {1 item} other {{count} items}}</source>
</segment>
</unit>
</file>
</xliff>
```
### Locale Configuration Model
```typescript
interface LocaleConfig {
code: string;
name: string;
direction: "ltr" | "rtl";
dateFormat: string;
numberFormat: Intl.NumberFormatOptions;
}
interface I18nConfig {
defaultLocale: string;
supportedLocales: LocaleConfig[];
fallbackLocale: string;
extractionPath: string;
outputPath: string;
}
```
## Error Handling
### Migration Error Handling
**Template Migration Errors**
- Handle malformed template syntax
- Report untranslatable dynamic content
- Provide fallbacks for complex pipe expressions
- Generate migration reports with warnings
**Code Migration Errors**
- Handle complex I18nService usage patterns
- Report dynamic translation key usage
- Provide manual migration guidance for edge cases
- Maintain error context for debugging
### Runtime Error Handling
**Missing Translation Handling**
- Configure Angular's missing translation strategy
- Implement fallback to default locale
- Log missing translations for development
- Provide graceful degradation in production
**Locale Loading Errors**
- Handle failed locale bundle loading
- Implement retry mechanisms
- Fallback to default locale on errors
- Provide user feedback for locale issues
## Testing Strategy
### Migration Testing
**Automated Migration Tests**
- Unit tests for template transformation logic
- Unit tests for TypeScript code transformation
- Integration tests for complete migration workflow
- Regression tests comparing old vs new output
**Translation Completeness Tests**
- Verify all translation keys are migrated
- Check parameter substitution correctness
- Validate XLIFF file structure
- Test locale-specific formatting
### Runtime Testing
**Functional Testing**
- Test all translated UI elements
- Verify locale switching functionality
- Test parameter interpolation
- Validate pluralization rules
**Performance Testing**
- Compare bundle sizes before/after migration
- Measure application startup time
- Test memory usage with different locales
- Benchmark translation rendering performance
### Cross-Platform Testing
**Application Testing**
- Test web application with all locales
- Test browser extension functionality
- Test desktop application localization
- Verify CLI tool translations
**Browser Compatibility**
- Test locale-specific builds in all supported browsers
- Verify proper fallback behavior
- Test right-to-left language support
- Validate accessibility with screen readers
## Implementation Phases
### Phase 1: Foundation Setup
- Install and configure @angular/localize
- Set up build configuration for i18n
- Create migration tooling infrastructure
- Establish testing framework
### Phase 2: Template Migration
- Implement template parsing and transformation
- Migrate core UI components
- Test template migration accuracy
- Create migration validation tools
### Phase 3: TypeScript Migration
- Implement code transformation using ts-morph
- Migrate service and component code
- Update dependency injection patterns
- Test code migration accuracy
### Phase 4: Build System Integration
- Update webpack configurations
- Implement locale-specific builds
- Set up extraction and merge workflows
- Test build pipeline end-to-end
### Phase 5: Translation File Migration
- Convert JSON files to XLIFF format
- Validate translation completeness
- Set up translation workflow
- Test all locale-specific builds
### Phase 6: Legacy System Removal
- Remove I18nService and related code
- Clean up unused dependencies
- Update documentation
- Perform final testing and validation

View File

@@ -0,0 +1,84 @@
# Requirements Document
## Introduction
This feature involves migrating the Bitwarden client applications from the current custom i18n system (I18nService and I18nPipe) to Angular's built-in localization system (@angular/localize). This migration will standardize the internationalization approach, improve build-time optimization, and leverage Angular's mature localization tooling.
## Requirements
### Requirement 1
**User Story:** As a developer, I want to use Angular's standard localization system, so that the codebase follows Angular best practices and benefits from framework optimizations.
#### Acceptance Criteria
1. WHEN the migration is complete THEN the system SHALL use @angular/localize instead of the custom I18nService
2. WHEN building the application THEN Angular's localization build process SHALL extract and process translation strings at build time
3. WHEN the application runs THEN all existing translation functionality SHALL work identically to the current system
4. WHEN developers add new translatable strings THEN they SHALL use Angular's i18n markers instead of the custom i18n pipe
### Requirement 2
**User Story:** As a user, I want the application to continue supporting all current languages and locales, so that my experience remains unchanged after the migration.
#### Acceptance Criteria
1. WHEN the migration is complete THEN all existing translation files SHALL be converted to Angular's XLIFF format
2. WHEN switching languages THEN the application SHALL maintain the same language switching behavior
3. WHEN using locale-specific formatting THEN numbers, dates, and currencies SHALL display correctly for each supported locale
4. WHEN the application loads THEN the user's previously selected language preference SHALL be preserved
### Requirement 3
**User Story:** As a developer, I want template translations to use Angular's i18n attributes, so that translations are extracted and optimized at build time.
#### Acceptance Criteria
1. WHEN templates contain translatable text THEN they SHALL use i18n attributes instead of the i18n pipe
2. WHEN templates have dynamic content THEN they SHALL use Angular's ICU expressions for pluralization and interpolation
3. WHEN building the application THEN Angular's extraction tool SHALL automatically identify all translatable strings
4. WHEN templates are processed THEN the i18n pipe SHALL be completely removed from all template files
### Requirement 4
**User Story:** As a developer, I want TypeScript code translations to use Angular's localize function, so that runtime translations work seamlessly with the new system.
#### Acceptance Criteria
1. WHEN TypeScript code needs translations THEN it SHALL use the $localize function instead of I18nService.t()
2. WHEN the application runs THEN all programmatic translations SHALL work identically to the current system
3. WHEN building the application THEN the $localize calls SHALL be processed and optimized by Angular's build system
4. WHEN code uses translation parameters THEN they SHALL be properly handled by the $localize function
### Requirement 5
**User Story:** As a developer, I want the build system to support multiple locales, so that we can generate locale-specific builds efficiently.
#### Acceptance Criteria
1. WHEN building the application THEN the build system SHALL support generating separate bundles for each locale
2. WHEN configuring builds THEN developers SHALL be able to specify which locales to build
3. WHEN the build completes THEN each locale SHALL have its own optimized bundle with embedded translations
4. WHEN serving the application THEN the correct locale bundle SHALL be served based on user preferences
### Requirement 6
**User Story:** As a developer, I want comprehensive tooling support, so that I can efficiently manage translations throughout the development lifecycle.
#### Acceptance Criteria
1. WHEN extracting translations THEN the Angular CLI SHALL automatically generate XLIFF files from source code
2. WHEN updating translations THEN the system SHALL support merging new strings with existing translation files
3. WHEN building for production THEN unused translation strings SHALL be tree-shaken from the final bundles
4. WHEN developing locally THEN the system SHALL support hot-reloading with translation changes
### Requirement 7
**User Story:** As a developer, I want backward compatibility during the migration, so that the transition can be done incrementally without breaking the application.
#### Acceptance Criteria
1. WHEN migrating components THEN both old and new i18n systems SHALL coexist temporarily
2. WHEN the migration is in progress THEN the application SHALL continue to function normally
3. WHEN components are migrated THEN they SHALL be tested to ensure identical functionality
4. WHEN the migration is complete THEN all legacy i18n code SHALL be removed from the codebase

View File

@@ -0,0 +1,264 @@
# Implementation Plan
- [ ] 1. Set up Angular Localize foundation and runtime configuration
- Install and configure @angular/localize package
- Set up runtime localization configuration in Angular applications
- Configure extraction settings for i18n workflow
- Create basic runtime locale switching infrastructure
- _Requirements: 1.1, 1.2, 5.1_
- [x] 2. Create migration tooling infrastructure
- [x] 2.1 Set up ts-morph for TypeScript code transformation
- Install ts-morph and configure TypeScript project parsing
- Create base transformation utilities for AST manipulation
- Write unit tests for transformation utilities
- _Requirements: 4.1, 4.2_
- [ ] 2.2 Set up angular-eslint for template parsing and transformation
- Configure angular-eslint template parser for HTML processing
- Create template transformation utilities
- Write unit tests for template parsing and transformation
- _Requirements: 3.1, 3.2_
- [ ] 3. Implement TypeScript code migration system
- [ ] 3.1 Create I18nService usage detection and analysis
- Write code to parse TypeScript files and find I18nService imports
- Identify all i18nService.t() method calls and their parameters
- Create analysis report of current usage patterns
- _Requirements: 4.1, 4.3_
- [ ] 3.2 Implement $localize transformation logic
- Transform i18nService.t() calls to $localize template literals
- Handle parameter substitution and interpolation
- Maintain type safety and proper escaping
- Write unit tests for transformation accuracy
- _Requirements: 4.1, 4.2, 4.3_
- [ ] 3.3 Create automated TypeScript migration tool
- Build CLI tool to process TypeScript files in batch
- Add validation and rollback capabilities
- Generate migration reports and statistics
- Test tool on sample codebase sections
- _Requirements: 4.1, 4.2_
- [ ] 4. Implement template migration system
- [ ] 4.1 Create i18n pipe detection and parsing
- Parse Angular templates to find | i18n pipe usage
- Extract translation keys and parameters
- Identify complex cases like nested expressions and pluralization
- _Requirements: 3.1, 3.3_
- [ ] 4.2 Implement i18n attribute transformation
- Transform | i18n pipes to i18n attributes with proper IDs
- Handle parameter interpolation and ICU expressions
- Generate proper i18n descriptions and meanings
- Write unit tests for template transformation
- _Requirements: 3.1, 3.2, 3.3_
- [ ] 4.3 Create automated template migration tool
- Build CLI tool to process HTML template files
- Add validation for transformation accuracy
- Generate before/after comparison reports
- Test tool on sample template files
- _Requirements: 3.1, 3.2_
- [ ] 5. Create runtime locale management service
- [ ] 5.1 Implement RuntimeLocaleService interface
- Create service with currentLocale$, setLocale(), and loadTranslations() methods
- Integrate with Angular's runtime localization APIs
- Maintain compatibility with existing locale persistence
- Write unit tests for service functionality
- _Requirements: 2.1, 2.2, 2.3_
- [ ] 5.2 Implement dynamic translation loading
- Create system to load XLIFF translation files at runtime
- Handle translation file caching and error recovery
- Implement fallback to default locale on load failures
- Write integration tests for translation loading
- _Requirements: 2.1, 2.2, 5.2_
- [ ] 5.3 Integrate with existing locale state management
- Connect RuntimeLocaleService with existing GlobalStateProvider
- Maintain user locale preferences across sessions
- Handle locale switching without application restart
- Test integration with existing user preference system
- _Requirements: 2.3, 2.4_
- [ ] 6. Convert translation files from JSON to XLIFF format
- [ ] 6.1 Create JSON to XLIFF conversion tool
- Parse existing JSON translation files
- Generate XLIFF 2.0 format with proper structure
- Handle placeholders and parameter substitution
- Write unit tests for conversion accuracy
- _Requirements: 2.1, 6.2_
- [ ] 6.2 Migrate all existing translation files
- Convert all locale JSON files to XLIFF format
- Validate translation completeness and accuracy
- Set up file structure for runtime loading
- Test converted files with sample applications
- _Requirements: 2.1, 2.2_
- [ ] 6.3 Set up translation workflow and tooling
- Configure Angular CLI extraction to generate XLIFF files
- Create merge tools for updating existing translations
- Set up validation for translation file integrity
- Document new translation workflow for developers
- _Requirements: 6.1, 6.2, 6.3_
- [ ] 7. Update build system for runtime localization
- [ ] 7.1 Configure webpack for dynamic translation loading
- Update webpack configuration to support runtime i18n
- Set up dynamic imports for translation files
- Configure proper chunking and caching for translations
- Test build output and bundle analysis
- _Requirements: 5.1, 5.3_
- [ ] 7.2 Update Angular build configuration
- Modify angular.json for single build with runtime switching
- Configure i18n extraction settings
- Set up development and production build configurations
- Test build process with all applications
- _Requirements: 5.1, 5.2_
- [ ] 7.3 Create build scripts for translation management
- Create scripts for extracting translations from code
- Set up automated merge process for translation updates
- Implement validation for translation file completeness
- Test scripts with continuous integration pipeline
- _Requirements: 6.1, 6.2, 6.4_
- [ ] 8. Migrate core application components
- [ ] 8.1 Migrate shared UI components library
- Apply TypeScript and template migration tools to libs/components
- Test migrated components for functionality and appearance
- Update component documentation and examples
- Validate translations work correctly in Storybook
- _Requirements: 1.3, 3.4, 4.3_
- [ ] 8.2 Migrate common platform services
- Apply migration tools to libs/common platform services
- Update service interfaces and dependency injection
- Test service functionality with new localization system
- Ensure backward compatibility during transition
- _Requirements: 1.3, 4.3, 7.1_
- [ ] 8.3 Migrate authentication and vault modules
- Apply migration tools to libs/auth and libs/vault
- Test critical user flows with new localization
- Validate error messages and user feedback translations
- Ensure security-related messages are properly translated
- _Requirements: 1.3, 2.2, 7.1_
- [ ] 9. Migrate application-specific code
- [ ] 9.1 Migrate web application
- Apply migration tools to apps/web source code
- Update Angular modules and component imports
- Test web application with runtime locale switching
- Validate all user interface elements are properly translated
- _Requirements: 1.3, 2.2, 2.3_
- [ ] 9.2 Migrate browser extension
- Apply migration tools to apps/browser source code
- Handle extension-specific localization requirements
- Test popup and content script translations
- Validate extension works across all supported browsers
- _Requirements: 1.3, 2.2_
- [ ] 9.3 Migrate desktop application
- Apply migration tools to apps/desktop source code
- Handle Electron-specific localization considerations
- Test desktop application with locale switching
- Validate native menu and dialog translations
- _Requirements: 1.3, 2.2_
- [ ] 10. Comprehensive testing and validation
- [ ] 10.1 Create automated translation testing suite
- Write tests to validate all translation keys are working
- Test parameter interpolation and pluralization
- Validate locale switching functionality
- Create regression tests for translation accuracy
- _Requirements: 1.3, 2.2, 2.3_
- [ ] 10.2 Perform cross-platform testing
- Test all applications with multiple locales
- Validate right-to-left language support
- Test accessibility with screen readers in different languages
- Verify proper date, number, and currency formatting
- _Requirements: 2.2, 2.3_
- [ ] 10.3 Performance and bundle size validation
- Compare bundle sizes before and after migration
- Test application startup time with different locales
- Validate memory usage with runtime locale switching
- Benchmark translation rendering performance
- _Requirements: 1.2, 5.3_
- [ ] 11. Remove legacy i18n system
- [ ] 11.1 Remove I18nService and related abstractions
- Delete I18nService, TranslationService, and I18nPipe classes
- Remove related interfaces and type definitions
- Update dependency injection configurations
- Clean up unused imports and dependencies
- _Requirements: 7.4_
- [ ] 11.2 Remove legacy translation infrastructure
- Delete JSON translation file loading logic
- Remove custom translation state management
- Clean up legacy build configuration
- Remove unused translation utilities
- _Requirements: 7.4_
- [ ] 11.3 Update documentation and developer guides
- Update developer documentation for new i18n system
- Create migration guide for future developers
- Update contribution guidelines for translations
- Document new translation workflow and tooling
- _Requirements: 6.4_
- [ ] 12. Final integration and deployment preparation
- [ ] 12.1 Integration testing across all applications
- Test complete user workflows in all applications
- Validate locale persistence across application restarts
- Test edge cases and error scenarios
- Perform final regression testing
- _Requirements: 1.3, 2.2, 2.3, 7.1_
- [ ] 12.2 Production deployment validation
- Test production builds with all locales
- Validate translation file serving and caching
- Test application performance in production environment
- Verify monitoring and error reporting for i18n issues
- _Requirements: 5.3, 6.3_

473
package-lock.json generated
View File

@@ -18,7 +18,6 @@
"@angular/animations": "19.2.14",
"@angular/cdk": "19.2.18",
"@angular/common": "19.2.14",
"@angular/compiler": "19.2.14",
"@angular/core": "19.2.14",
"@angular/forms": "19.2.14",
"@angular/platform-browser": "19.2.14",
@@ -77,7 +76,9 @@
"devDependencies": {
"@angular-devkit/build-angular": "19.2.14",
"@angular-eslint/schematics": "19.6.0",
"@angular-eslint/template-parser": "20.1.1",
"@angular/cli": "19.2.14",
"@angular/compiler": "19.2.14",
"@angular/compiler-cli": "19.2.14",
"@angular/localize": "19.2.14",
"@babel/core": "7.24.9",
@@ -174,6 +175,7 @@
"tailwindcss": "3.4.17",
"ts-jest": "29.3.4",
"ts-loader": "9.5.2",
"ts-morph": "26.0.0",
"tsconfig-paths-webpack-plugin": "4.2.0",
"type-fest": "2.19.0",
"typescript": "5.5.4",
@@ -968,9 +970,9 @@
"license": "MIT"
},
"node_modules/@angular-devkit/build-angular/node_modules/browserslist": {
"version": "4.25.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
"integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
"version": "4.25.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
"integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==",
"funding": [
{
"type": "opencollective",
@@ -987,8 +989,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001718",
"electron-to-chromium": "^1.5.160",
"caniuse-lite": "^1.0.30001726",
"electron-to-chromium": "^1.5.173",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
},
@@ -1886,13 +1888,13 @@
}
},
"node_modules/@angular-eslint/template-parser": {
"version": "19.6.0",
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.6.0.tgz",
"integrity": "sha512-NGxXUZkI5lXjoKnmL51C8DoJx8AjwF9sonieC2EVxgXycK2MYAamFWYGHMiVemzFsg1nIv+JvhHITgjSjyC3HQ==",
"version": "20.1.1",
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-20.1.1.tgz",
"integrity": "sha512-giIMYORf8P8MbBxh6EUfiR/7Y+omxJtK2C7a8lYTtLSOIGO0D8c8hXx9hTlPcdupVX+xZXDuZ85c9JDen+JSSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-eslint/bundled-angular-compiler": "19.6.0",
"@angular-eslint/bundled-angular-compiler": "20.1.1",
"eslint-scope": "^8.0.2"
},
"peerDependencies": {
@@ -1900,6 +1902,13 @@
"typescript": "*"
}
},
"node_modules/@angular-eslint/template-parser/node_modules/@angular-eslint/bundled-angular-compiler": {
"version": "20.1.1",
"resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-20.1.1.tgz",
"integrity": "sha512-hEWh/upyTj2bhyRmbNnGtlOXhBSEHwLg8/9YYhwmiNApQwKcvcg7lkstZMEVrKievNHZT6Wh4dWZvjRjMqLNSg==",
"dev": true,
"license": "MIT"
},
"node_modules/@angular-eslint/utils": {
"version": "19.6.0",
"resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.6.0.tgz",
@@ -5054,6 +5063,18 @@
"semver": "bin/semver.js"
}
},
"node_modules/@compodoc/compodoc/node_modules/@ts-morph/common": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz",
"integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimatch": "^9.0.4",
"path-browserify": "^1.0.1",
"tinyglobby": "^0.2.9"
}
},
"node_modules/@compodoc/compodoc/node_modules/babel-plugin-polyfill-corejs3": {
"version": "0.10.6",
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz",
@@ -5110,6 +5131,22 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/@compodoc/compodoc/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@compodoc/compodoc/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -5140,6 +5177,17 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@compodoc/compodoc/node_modules/ts-morph": {
"version": "24.0.0",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-24.0.0.tgz",
"integrity": "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ts-morph/common": "~0.25.0",
"code-block-writer": "^13.0.3"
}
},
"node_modules/@compodoc/live-server": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@compodoc/live-server/-/live-server-1.2.3.tgz",
@@ -8309,9 +8357,9 @@
}
},
"node_modules/@napi-rs/nice": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz",
"integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.4.tgz",
"integrity": "sha512-Sqih1YARrmMoHlXGgI9JrrgkzxcaaEso0AH+Y7j8NHonUs+xe4iDsgC3IBIDNdzEewbNpccNN6hip+b5vmyRLw==",
"license": "MIT",
"optional": true,
"engines": {
@@ -8322,28 +8370,28 @@
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@napi-rs/nice-android-arm-eabi": "1.0.1",
"@napi-rs/nice-android-arm64": "1.0.1",
"@napi-rs/nice-darwin-arm64": "1.0.1",
"@napi-rs/nice-darwin-x64": "1.0.1",
"@napi-rs/nice-freebsd-x64": "1.0.1",
"@napi-rs/nice-linux-arm-gnueabihf": "1.0.1",
"@napi-rs/nice-linux-arm64-gnu": "1.0.1",
"@napi-rs/nice-linux-arm64-musl": "1.0.1",
"@napi-rs/nice-linux-ppc64-gnu": "1.0.1",
"@napi-rs/nice-linux-riscv64-gnu": "1.0.1",
"@napi-rs/nice-linux-s390x-gnu": "1.0.1",
"@napi-rs/nice-linux-x64-gnu": "1.0.1",
"@napi-rs/nice-linux-x64-musl": "1.0.1",
"@napi-rs/nice-win32-arm64-msvc": "1.0.1",
"@napi-rs/nice-win32-ia32-msvc": "1.0.1",
"@napi-rs/nice-win32-x64-msvc": "1.0.1"
"@napi-rs/nice-android-arm-eabi": "1.0.4",
"@napi-rs/nice-android-arm64": "1.0.4",
"@napi-rs/nice-darwin-arm64": "1.0.4",
"@napi-rs/nice-darwin-x64": "1.0.4",
"@napi-rs/nice-freebsd-x64": "1.0.4",
"@napi-rs/nice-linux-arm-gnueabihf": "1.0.4",
"@napi-rs/nice-linux-arm64-gnu": "1.0.4",
"@napi-rs/nice-linux-arm64-musl": "1.0.4",
"@napi-rs/nice-linux-ppc64-gnu": "1.0.4",
"@napi-rs/nice-linux-riscv64-gnu": "1.0.4",
"@napi-rs/nice-linux-s390x-gnu": "1.0.4",
"@napi-rs/nice-linux-x64-gnu": "1.0.4",
"@napi-rs/nice-linux-x64-musl": "1.0.4",
"@napi-rs/nice-win32-arm64-msvc": "1.0.4",
"@napi-rs/nice-win32-ia32-msvc": "1.0.4",
"@napi-rs/nice-win32-x64-msvc": "1.0.4"
}
},
"node_modules/@napi-rs/nice-android-arm-eabi": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz",
"integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.4.tgz",
"integrity": "sha512-OZFMYUkih4g6HCKTjqJHhMUlgvPiDuSLZPbPBWHLjKmFTv74COzRlq/gwHtmEVaR39mJQ6ZyttDl2HNMUbLVoA==",
"cpu": [
"arm"
],
@@ -8357,9 +8405,9 @@
}
},
"node_modules/@napi-rs/nice-android-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz",
"integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.4.tgz",
"integrity": "sha512-k8u7cjeA64vQWXZcRrPbmwjH8K09CBnNaPnI9L1D5N6iMPL3XYQzLcN6WwQonfcqCDv5OCY3IqX89goPTV4KMw==",
"cpu": [
"arm64"
],
@@ -8373,9 +8421,9 @@
}
},
"node_modules/@napi-rs/nice-darwin-arm64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz",
"integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-GsLdQvUcuVzoyzmtjsThnpaVEizAqH5yPHgnsBmq3JdVoVZHELFo7PuJEdfOH1DOHi2mPwB9sCJEstAYf3XCJA==",
"cpu": [
"arm64"
],
@@ -8389,9 +8437,9 @@
}
},
"node_modules/@napi-rs/nice-darwin-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz",
"integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.4.tgz",
"integrity": "sha512-1y3gyT3e5zUY5SxRl3QDtJiWVsbkmhtUHIYwdWWIQ3Ia+byd/IHIEpqAxOGW1nhhnIKfTCuxBadHQb+yZASVoA==",
"cpu": [
"x64"
],
@@ -8405,9 +8453,9 @@
}
},
"node_modules/@napi-rs/nice-freebsd-x64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz",
"integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.4.tgz",
"integrity": "sha512-06oXzESPRdXUuzS8n2hGwhM2HACnDfl3bfUaSqLGImM8TA33pzDXgGL0e3If8CcFWT98aHows5Lk7xnqYNGFeA==",
"cpu": [
"x64"
],
@@ -8421,9 +8469,9 @@
}
},
"node_modules/@napi-rs/nice-linux-arm-gnueabihf": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz",
"integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.4.tgz",
"integrity": "sha512-CgklZ6g8WL4+EgVVkxkEvvsi2DSLf9QIloxWO0fvQyQBp6VguUSX3eHLeRpqwW8cRm2Hv/Q1+PduNk7VK37VZw==",
"cpu": [
"arm"
],
@@ -8437,9 +8485,9 @@
}
},
"node_modules/@napi-rs/nice-linux-arm64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz",
"integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.4.tgz",
"integrity": "sha512-wdAJ7lgjhAlsANUCv0zi6msRwq+D4KDgU+GCCHssSxWmAERZa2KZXO0H2xdmoJ/0i03i6YfK/sWaZgUAyuW2oQ==",
"cpu": [
"arm64"
],
@@ -8453,9 +8501,9 @@
}
},
"node_modules/@napi-rs/nice-linux-arm64-musl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz",
"integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.4.tgz",
"integrity": "sha512-4b1KYG+sriufhFrpUS9uNOEYYJqSfcbnwGx6uGX7JjrH8tELG90cOpCawz5THNIwlS3DhLgnCOcn0+4p6z26QA==",
"cpu": [
"arm64"
],
@@ -8469,9 +8517,9 @@
}
},
"node_modules/@napi-rs/nice-linux-ppc64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz",
"integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.4.tgz",
"integrity": "sha512-iaf3vMRgr23oe1PUaKpxaH3DS0IMN0+N9iEiWVwYPm/U15vZFYdqVegGfN2PzrZLUl5lc8ZxbmEKDfuqslhAMA==",
"cpu": [
"ppc64"
],
@@ -8485,9 +8533,9 @@
}
},
"node_modules/@napi-rs/nice-linux-riscv64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz",
"integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.4.tgz",
"integrity": "sha512-UXoREY6Yw6rHrGuTwQgBxpfjK34t6mTjibE9/cXbefL9AuUCJ9gEgwNKZiONuR5QGswChqo9cnthjdKkYyAdDg==",
"cpu": [
"riscv64"
],
@@ -8501,9 +8549,9 @@
}
},
"node_modules/@napi-rs/nice-linux-s390x-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz",
"integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.4.tgz",
"integrity": "sha512-eFbgYCRPmsqbYPAlLYU5hYTNbogmIDUvknilehHsFhCH1+0/kN87lP+XaLT0Yeq4V/rpwChSd9vlz4muzFArtw==",
"cpu": [
"s390x"
],
@@ -8517,9 +8565,9 @@
}
},
"node_modules/@napi-rs/nice-linux-x64-gnu": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz",
"integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.4.tgz",
"integrity": "sha512-4T3E6uTCwWT6IPnwuPcWVz3oHxvEp/qbrCxZhsgzwTUBEwu78EGNXGdHfKJQt3soth89MLqZJw+Zzvnhrsg1mQ==",
"cpu": [
"x64"
],
@@ -8533,9 +8581,9 @@
}
},
"node_modules/@napi-rs/nice-linux-x64-musl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz",
"integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.4.tgz",
"integrity": "sha512-NtbBkAeyBPLvCBkWtwkKXkNSn677eaT0cX3tygq+2qVv71TmHgX4gkX6o9BXjlPzdgPGwrUudavCYPT9tzkEqQ==",
"cpu": [
"x64"
],
@@ -8549,9 +8597,9 @@
}
},
"node_modules/@napi-rs/nice-win32-arm64-msvc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz",
"integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.4.tgz",
"integrity": "sha512-vubOe3i+YtSJGEk/++73y+TIxbuVHi+W8ZzrRm2eETCjCRwNlgbfToQZ85dSA+4iBB/NJRGNp+O4hfdbbttZWA==",
"cpu": [
"arm64"
],
@@ -8565,9 +8613,9 @@
}
},
"node_modules/@napi-rs/nice-win32-ia32-msvc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz",
"integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.4.tgz",
"integrity": "sha512-BMOVrUDZeg1RNRKVlh4eyLv5djAAVLiSddfpuuQ47EFjBcklg0NUeKMFKNrKQR4UnSn4HAiACLD7YK7koskwmg==",
"cpu": [
"ia32"
],
@@ -8581,9 +8629,9 @@
}
},
"node_modules/@napi-rs/nice-win32-x64-msvc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz",
"integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.4.tgz",
"integrity": "sha512-kCNk6HcRZquhw/whwh4rHsdPyOSCQCgnVDVik+Y9cuSVTDy3frpiCJTScJqPPS872h4JgZKkr/+CwcwttNEo9Q==",
"cpu": [
"x64"
],
@@ -10041,6 +10089,20 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.0.tgz",
"integrity": "sha512-wJaRvcT17PoOK6Ggcfo3nouFlybHvARBS4jzT0PC/lg17fIJHcDS2fZz3sD+iA4nRlho2zE6OGbU0HvwATdokQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz",
@@ -10055,9 +10117,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz",
"integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.0.tgz",
"integrity": "sha512-7CjPw6FflFsVOUfWOrVrREiV3IYXG4RzZ1ZQUaT3BtSK8YXN6x286o+sruPZJESIaPebYuFowmg54ZdrkVBYog==",
"cpu": [
"riscv64"
],
@@ -11920,28 +11982,28 @@
}
},
"node_modules/@ts-morph/common": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz",
"integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==",
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz",
"integrity": "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimatch": "^9.0.4",
"path-browserify": "^1.0.1",
"tinyglobby": "^0.2.9"
"fast-glob": "^3.3.3",
"minimatch": "^10.0.1",
"path-browserify": "^1.0.1"
}
},
"node_modules/@ts-morph/common/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -14388,6 +14450,21 @@
"typescript-eslint": "^8.0.0"
}
},
"node_modules/angular-eslint/node_modules/@angular-eslint/template-parser": {
"version": "19.6.0",
"resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.6.0.tgz",
"integrity": "sha512-NGxXUZkI5lXjoKnmL51C8DoJx8AjwF9sonieC2EVxgXycK2MYAamFWYGHMiVemzFsg1nIv+JvhHITgjSjyC3HQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-eslint/bundled-angular-compiler": "19.6.0",
"eslint-scope": "^8.0.2"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": "*"
}
},
"node_modules/ansi-colors": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
@@ -16363,9 +16440,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001724",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz",
"integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==",
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"funding": [
{
"type": "opencollective",
@@ -18938,9 +19015,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.172",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.172.tgz",
"integrity": "sha512-fnKW9dGgmBfsebbYognQSv0CGGLFH1a5iV9EDYTBwmAQn+whbzHbLFlC+3XbHc8xaNtpO0etm8LOcRXs1qMRkQ==",
"version": "1.5.191",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.191.tgz",
"integrity": "sha512-xcwe9ELcuxYLUFqZZxL19Z6HVKcvNkIwhbHUz7L3us6u12yR+7uY89dSl570f/IqNthx8dAw3tojG7i4Ni4tDA==",
"license": "ISC"
},
"node_modules/electron-updater": {
@@ -26525,13 +26602,6 @@
"@lmdb/lmdb-win32-x64": "3.2.6"
}
},
"node_modules/lmdb/node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
"license": "MIT",
"optional": true
},
"node_modules/loader-runner": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -28484,9 +28554,9 @@
"license": "MIT"
},
"node_modules/msgpackr": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.4.tgz",
"integrity": "sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==",
"version": "1.11.5",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
"integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
"license": "MIT",
"optional": true,
"optionalDependencies": {
@@ -28820,6 +28890,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
"license": "MIT",
"optional": true
},
"node_modules/node-api-version": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz",
@@ -30636,9 +30713,9 @@
}
},
"node_modules/ordered-binary": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz",
"integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.0.tgz",
"integrity": "sha512-IQh2aMfMIDbPjI/8a3Edr+PiOpcsB7yo8NdW7aHWVaoR/pcDldunMvnnwbk/auPGqmKeAdxtZl7MHX/QmPwhvQ==",
"license": "MIT",
"optional": true
},
@@ -36138,13 +36215,13 @@
}
},
"node_modules/ts-morph": {
"version": "24.0.0",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-24.0.0.tgz",
"integrity": "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==",
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz",
"integrity": "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ts-morph/common": "~0.25.0",
"@ts-morph/common": "~0.27.0",
"code-block-writer": "^13.0.3"
}
},
@@ -37596,9 +37673,9 @@
}
},
"node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz",
"integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.0.tgz",
"integrity": "sha512-9f3nSTFI2ivfxc7/tHBHcJ8pRnp8ROrELvsVprlQPVvcZ+j5zztYd+PTJGpyIOAdTvNwNrpCXswKSeoQcyGjMQ==",
"cpu": [
"arm"
],
@@ -37610,9 +37687,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-android-arm64": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz",
"integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.0.tgz",
"integrity": "sha512-tFZSEhqJ8Yrpe50TzOdeoYi72gi/jsnT7y8Qrozf3cNu28WX+s6I3XzEPUAqoaT9SAS8Xz9AzGTFlxxCH/w20w==",
"cpu": [
"arm64"
],
@@ -37624,9 +37701,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz",
"integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.0.tgz",
"integrity": "sha512-+DikIIs+p6yU2hF51UaWG8BnHbq90X0QIOt5zqSKSZxY+G3qqdLih214e9InJal21af2PuuxkDectetGfbVPJw==",
"cpu": [
"arm64"
],
@@ -37638,9 +37715,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-darwin-x64": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz",
"integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.0.tgz",
"integrity": "sha512-5a+NofhdEB/WimSlFMskbFQn1vqz1FWryYpA99trmZGO6qEmiS0IsX6w4B3d91U878Q2ZQdiaFF1gxX4P147og==",
"cpu": [
"x64"
],
@@ -37652,9 +37729,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz",
"integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.0.tgz",
"integrity": "sha512-igr/RlKPS3OCy4jD3XBmAmo3UAcNZkJSubRsw1JeM8bAbwf15k/3eMZXD91bnjheijJiOJcga3kfCLKjV8IXNg==",
"cpu": [
"arm64"
],
@@ -37666,9 +37743,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz",
"integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.0.tgz",
"integrity": "sha512-MdigWzPSHlQzB1xZ+MdFDWTAH+kcn7UxjEBoOKuaso7z1DRlnAnrknB1mTtNOQ+GdPI8xgExAGwHeqQjntR0Cg==",
"cpu": [
"x64"
],
@@ -37680,9 +37757,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz",
"integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.0.tgz",
"integrity": "sha512-dmZseE0ZwA/4yy1+BwFrDqFTjjNg24GO9xSrb1weVbt6AFkhp5pz1gVS7IMtfIvoWy8yp6q/zN0bKnefRUImvQ==",
"cpu": [
"arm"
],
@@ -37694,9 +37771,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz",
"integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.0.tgz",
"integrity": "sha512-fzhfn6p9Cfm3W8UrWKIa4l7Wfjs/KGdgaswMBBE3KY3Ta43jg2XsPrAtfezHpsRk0Nx+TFuS3hZk/To2N5kFPQ==",
"cpu": [
"arm"
],
@@ -37708,9 +37785,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz",
"integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.0.tgz",
"integrity": "sha512-vVDD+iPDPmJQ5nAQ5Tifq3ywdv60FartglFI8VOCK+hcU9aoG0qlQTsDJP97O5yiTaTqlneZWoARMcVC5nyUoQ==",
"cpu": [
"arm64"
],
@@ -37722,9 +37799,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz",
"integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.0.tgz",
"integrity": "sha512-0d0jx08fzDHCzXqrtCMEEyxKU0SvJrWmUjUDE2/KDQ2UDJql0tfiwYvEx1oHELClKO8CNdE+AGJj+RqXscZpdQ==",
"cpu": [
"arm64"
],
@@ -37736,9 +37813,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz",
"integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.0.tgz",
"integrity": "sha512-XBYu9oW9eKJadWn8M7hkTZsD4yG+RrsTrVEgyKwb4L72cpJjRbRboTG9Lg9fec8MxJp/cfTHAocg4mnismQR8A==",
"cpu": [
"loong64"
],
@@ -37749,24 +37826,10 @@
],
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz",
"integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz",
"integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.0.tgz",
"integrity": "sha512-GZ5bkMFteAGkcmh8x0Ok4LSa+L62Ez0tMsHPX6JtR0wl4Xc3bQcrFHDiR5DGLEDFtGrXih4Nd/UDaFqs968/wA==",
"cpu": [
"riscv64"
],
@@ -37778,9 +37841,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz",
"integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.0.tgz",
"integrity": "sha512-nmvnl0ZiuysltcB/cKjUh40Rx4FbSyueERDsl2FLvLYr6pCgSsvGr3SocUT84svSpmloS7f1DRWqtRha74Gi1w==",
"cpu": [
"s390x"
],
@@ -37792,9 +37855,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz",
"integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.0.tgz",
"integrity": "sha512-Cv+moII5C8RM6gZbR3cb21o6rquVDZrN2o81maROg1LFzBz2dZUwIQSxFA8GtGZ/F2KtsqQ2z3eFPBb6akvQNg==",
"cpu": [
"x64"
],
@@ -37806,9 +37869,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz",
"integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.0.tgz",
"integrity": "sha512-PHcMG8DZTM9RCIjp8QIfN0VYtX0TtBPnWOTRurFhoCDoi9zptUZL2k7pCs+5rgut7JAiUsYy+huyhVKPcmxoog==",
"cpu": [
"x64"
],
@@ -37820,9 +37883,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz",
"integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.0.tgz",
"integrity": "sha512-1SI/Rd47e8aQJeFWMDg16ET+fjvCcD/CzeaRmIEPmb05hx+3cCcwIF4ebUag4yTt/D1peE+Mgp0+Po3M358cAA==",
"cpu": [
"arm64"
],
@@ -37834,9 +37897,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz",
"integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.0.tgz",
"integrity": "sha512-JwOCYxmumFDfDhx4kNyz6kTVK3gWzBIvVdMNzQMRDubcoGRDniOOmo6DDNP42qwZx3Bp9/6vWJ+kNzNqXoHmeA==",
"cpu": [
"ia32"
],
@@ -37848,9 +37911,9 @@
"peer": true
},
"node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz",
"integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.0.tgz",
"integrity": "sha512-IPMIfrfkG1GaEXi+JSsQEx8x9b4b+hRZXO7KYc2pKio3zO2/VDXDs6B9Ts/nnO+25Fk1tdAVtUn60HKKPPzDig==",
"cpu": [
"x64"
],
@@ -37862,9 +37925,9 @@
"peer": true
},
"node_modules/vite/node_modules/rollup": {
"version": "4.44.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz",
"integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==",
"version": "4.46.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.0.tgz",
"integrity": "sha512-ONmkT3Ud3IfW15nl7l4qAZko5/2iZ5ALVBDh02ZSZ5IGVLJSYkRcRa3iB58VyEIyoofs9m2xdVrm+lTi97+3pw==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -37878,26 +37941,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.44.0",
"@rollup/rollup-android-arm64": "4.44.0",
"@rollup/rollup-darwin-arm64": "4.44.0",
"@rollup/rollup-darwin-x64": "4.44.0",
"@rollup/rollup-freebsd-arm64": "4.44.0",
"@rollup/rollup-freebsd-x64": "4.44.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.44.0",
"@rollup/rollup-linux-arm-musleabihf": "4.44.0",
"@rollup/rollup-linux-arm64-gnu": "4.44.0",
"@rollup/rollup-linux-arm64-musl": "4.44.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.44.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.44.0",
"@rollup/rollup-linux-riscv64-gnu": "4.44.0",
"@rollup/rollup-linux-riscv64-musl": "4.44.0",
"@rollup/rollup-linux-s390x-gnu": "4.44.0",
"@rollup/rollup-linux-x64-gnu": "4.44.0",
"@rollup/rollup-linux-x64-musl": "4.44.0",
"@rollup/rollup-win32-arm64-msvc": "4.44.0",
"@rollup/rollup-win32-ia32-msvc": "4.44.0",
"@rollup/rollup-win32-x64-msvc": "4.44.0",
"@rollup/rollup-android-arm-eabi": "4.46.0",
"@rollup/rollup-android-arm64": "4.46.0",
"@rollup/rollup-darwin-arm64": "4.46.0",
"@rollup/rollup-darwin-x64": "4.46.0",
"@rollup/rollup-freebsd-arm64": "4.46.0",
"@rollup/rollup-freebsd-x64": "4.46.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.46.0",
"@rollup/rollup-linux-arm-musleabihf": "4.46.0",
"@rollup/rollup-linux-arm64-gnu": "4.46.0",
"@rollup/rollup-linux-arm64-musl": "4.46.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.46.0",
"@rollup/rollup-linux-ppc64-gnu": "4.46.0",
"@rollup/rollup-linux-riscv64-gnu": "4.46.0",
"@rollup/rollup-linux-riscv64-musl": "4.46.0",
"@rollup/rollup-linux-s390x-gnu": "4.46.0",
"@rollup/rollup-linux-x64-gnu": "4.46.0",
"@rollup/rollup-linux-x64-musl": "4.46.0",
"@rollup/rollup-win32-arm64-msvc": "4.46.0",
"@rollup/rollup-win32-ia32-msvc": "4.46.0",
"@rollup/rollup-win32-x64-msvc": "4.46.0",
"fsevents": "~2.3.2"
}
},

View File

@@ -45,7 +45,9 @@
"devDependencies": {
"@angular-devkit/build-angular": "19.2.14",
"@angular-eslint/schematics": "19.6.0",
"@angular-eslint/template-parser": "20.1.1",
"@angular/cli": "19.2.14",
"@angular/compiler": "19.2.14",
"@angular/compiler-cli": "19.2.14",
"@angular/localize": "19.2.14",
"@babel/core": "7.24.9",
@@ -142,6 +144,7 @@
"tailwindcss": "3.4.17",
"ts-jest": "29.3.4",
"ts-loader": "9.5.2",
"ts-morph": "26.0.0",
"tsconfig-paths-webpack-plugin": "4.2.0",
"type-fest": "2.19.0",
"typescript": "5.5.4",
@@ -160,7 +163,6 @@
"@angular/animations": "19.2.14",
"@angular/cdk": "19.2.18",
"@angular/common": "19.2.14",
"@angular/compiler": "19.2.14",
"@angular/core": "19.2.14",
"@angular/forms": "19.2.14",
"@angular/platform-browser": "19.2.14",

View File

@@ -0,0 +1,14 @@
# Angular Localize Migration Tools
This directory contains tools for migrating from the custom I18nService to Angular's @angular/localize system.
## Structure
- `typescript/` - TypeScript code transformation utilities using ts-morph
- `templates/` - Angular template transformation utilities using angular-eslint
- `shared/` - Shared utilities and types
- `tests/` - Unit tests for migration tools
## Usage
The migration tools are designed to be run as part of the overall migration process defined in the spec.

View File

@@ -0,0 +1,21 @@
const { createCjsPreset } = require("jest-preset-angular/presets");
const presetConfig = createCjsPreset({
tsconfig: "<rootDir>/tsconfig.spec.json",
astTransformers: {
before: ["<rootDir>/../../../libs/shared/es2020-transformer.ts"],
},
diagnostics: {
ignoreCodes: ["TS151001"],
},
});
module.exports = {
...presetConfig,
displayName: "i18n-migration-tools",
preset: "../../../jest.preset.js",
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../../coverage/scripts/migration/i18n",
testMatch: ["<rootDir>/tests/**/*.test.ts"],
collectCoverageFrom: ["typescript/**/*.ts", "templates/**/*.ts", "shared/**/*.ts", "!**/*.d.ts"],
};

View File

@@ -0,0 +1,38 @@
/**
* Shared types for the i18n migration tools
*/
export interface TransformationResult {
success: boolean;
filePath: string;
changes: TransformationChange[];
errors: string[];
}
export interface TransformationChange {
type: "replace" | "add" | "remove";
location: {
line: number;
column: number;
};
original?: string;
replacement?: string;
description: string;
}
export interface MigrationConfig {
sourceRoot: string;
tsConfigPath: string;
dryRun: boolean;
verbose: boolean;
}
export interface I18nUsage {
filePath: string;
line: number;
column: number;
method: "t" | "pipe";
key: string;
parameters?: string[];
context?: string;
}

View File

@@ -0,0 +1,206 @@
/* eslint-disable no-console */
import { readFileSync, writeFileSync } from "fs";
import { MigrationConfig, TransformationResult, I18nUsage } from "../shared/types";
import { TemplateTransformer } from "./template-transformer";
/**
* Main class for template migration from i18n pipes to i18n attributes
*/
export class TemplateMigrator {
private transformer: TemplateTransformer;
constructor(private config: MigrationConfig) {
this.transformer = new TemplateTransformer();
}
/**
* Analyze i18n pipe usage in a template file
*/
analyzeTemplate(filePath: string): I18nUsage[] {
try {
const templateContent = readFileSync(filePath, "utf-8");
return this.transformer.findI18nPipeUsage(templateContent, filePath);
} catch (error) {
if (this.config.verbose) {
console.error(`Error reading template file ${filePath}:`, error);
}
return [];
}
}
/**
* Migrate a single template file
*/
async migrateTemplate(filePath: string): Promise<TransformationResult> {
try {
const templateContent = readFileSync(filePath, "utf-8");
const result = this.transformer.transformTemplate(templateContent, filePath);
if (result.success && result.changes.length > 0) {
// Get the transformed content by applying all changes
const transformedContent = this.applyChangesToContent(templateContent, result.changes);
// Validate the transformation
if (this.transformer.validateTransformation(templateContent, transformedContent)) {
if (!this.config.dryRun) {
writeFileSync(filePath, transformedContent, "utf-8");
}
} else {
result.success = false;
result.errors.push("Transformation validation failed");
}
}
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
success: false,
filePath,
changes: [],
errors: [`Error processing template file: ${errorMessage}`],
};
}
}
/**
* Migrate multiple template files
*/
async migrateTemplates(filePaths: string[]): Promise<TransformationResult[]> {
const results: TransformationResult[] = [];
for (const filePath of filePaths) {
if (this.config.verbose) {
console.log(`Processing template: ${filePath}`);
}
const result = await this.migrateTemplate(filePath);
results.push(result);
if (!result.success) {
console.error(`Failed to process ${filePath}:`, result.errors);
}
}
return results;
}
/**
* Generate analysis report for template usage
*/
generateTemplateAnalysisReport(filePaths: string[]): string {
const allUsages: I18nUsage[] = [];
for (const filePath of filePaths) {
const usages = this.analyzeTemplate(filePath);
allUsages.push(...usages);
}
const fileCount = new Set(allUsages.map((u) => u.filePath)).size;
const keyCount = new Set(allUsages.map((u) => u.key)).size;
let report = `# Template i18n Pipe Usage Analysis Report\n\n`;
report += `## Summary\n`;
report += `- Total pipe usage count: ${allUsages.length}\n`;
report += `- Template files affected: ${fileCount}\n`;
report += `- Unique translation keys: ${keyCount}\n\n`;
report += `## Usage by File\n`;
const usagesByFile = allUsages.reduce(
(acc, usage) => {
if (!acc[usage.filePath]) {
acc[usage.filePath] = [];
}
acc[usage.filePath].push(usage);
return acc;
},
{} as Record<string, I18nUsage[]>,
);
Object.entries(usagesByFile).forEach(([filePath, fileUsages]) => {
report += `\n### ${filePath}\n`;
fileUsages.forEach((usage) => {
report += `- Line ${usage.line}: \`${usage.key}\``;
if (usage.parameters) {
report += ` (with parameters: ${usage.parameters.join(", ")})`;
}
if (usage.context) {
report += ` - Context: \`${usage.context.trim()}\``;
}
report += `\n`;
});
});
report += `\n## Most Common Keys\n`;
const keyCounts = allUsages.reduce(
(acc, usage) => {
acc[usage.key] = (acc[usage.key] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
Object.entries(keyCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.forEach(([key, count]) => {
report += `- \`${key}\`: ${count} usage(s)\n`;
});
return report;
}
/**
* Generate migration statistics
*/
generateMigrationStats(results: TransformationResult[]): string {
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
const totalChanges = results.reduce((sum, r) => sum + r.changes.length, 0);
let stats = `# Template Migration Statistics\n\n`;
stats += `- Templates processed: ${results.length}\n`;
stats += `- Successful: ${successful}\n`;
stats += `- Failed: ${failed}\n`;
stats += `- Total transformations: ${totalChanges}\n\n`;
if (failed > 0) {
stats += `## Failed Templates\n`;
results
.filter((r) => !r.success)
.forEach((result) => {
stats += `- ${result.filePath}\n`;
result.errors.forEach((error) => {
stats += ` - ${error}\n`;
});
});
}
return stats;
}
/**
* Apply transformation changes to content
*/
private applyChangesToContent(content: string, changes: TransformationResult["changes"]): string {
let transformedContent = content;
// Sort changes by position (descending) to avoid position shifts
const sortedChanges = changes.sort((a, b) => {
if (a.location.line !== b.location.line) {
return b.location.line - a.location.line;
}
return b.location.column - a.location.column;
});
for (const change of sortedChanges) {
if (change.type === "replace" && change.original && change.replacement) {
transformedContent = transformedContent.replace(change.original, change.replacement);
}
}
return transformedContent;
}
}

View File

@@ -0,0 +1,371 @@
/* eslint-disable no-console */
import { parseTemplate, TmplAstNode, TmplAstElement, TmplAstBoundText } from "@angular/compiler";
import { I18nUsage } from "../shared/types";
/**
* Utility class for parsing Angular templates using Angular compiler
*/
export class TemplateParser {
/**
* Find all i18n pipe usage in a template
*/
findI18nPipeUsage(templateContent: string, filePath: string): I18nUsage[] {
const usages: I18nUsage[] = [];
try {
// Parse template using Angular compiler
const parseResult = parseTemplate(templateContent, filePath);
if (parseResult.nodes) {
this.traverseNodes(parseResult.nodes, usages, filePath);
}
// Also use regex as fallback for edge cases
this.findWithRegex(templateContent, filePath, usages);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.warn(`Warning: Could not parse template ${filePath}:`, errorMessage);
// Fallback to regex parsing
this.findWithRegex(templateContent, filePath, usages);
}
return usages;
}
/**
* Recursively traverse template AST nodes to find i18n pipe usage
*/
private traverseNodes(nodes: TmplAstNode[], usages: I18nUsage[], filePath: string): void {
for (const node of nodes) {
this.processNode(node, usages, filePath);
// Recursively process child nodes
if ("children" in node && Array.isArray(node.children)) {
this.traverseNodes(node.children, usages, filePath);
}
}
}
/**
* Process a single template AST node to find i18n pipe usage
*/
private processNode(node: TmplAstNode, usages: I18nUsage[], filePath: string): void {
// Handle bound text nodes (interpolations)
if (this.isBoundText(node)) {
const expression = node.value;
if (expression && "source" in expression) {
const expressionText = (expression.source as string) || "";
if (this.containsI18nPipe(expressionText)) {
const pipeUsage = this.extractI18nPipeUsage(expressionText);
if (pipeUsage) {
usages.push({
filePath,
line: node.sourceSpan.start.line + 1,
column: node.sourceSpan.start.col,
method: "pipe",
key: pipeUsage.key,
parameters: pipeUsage.parameters,
context: `{{ ${expressionText} }}`,
});
}
}
}
}
// Handle element nodes with attributes
if (this.isElement(node)) {
// Check bound attributes (property bindinxgs)
for (const input of node.inputs || []) {
if (input.value && "source" in input.value) {
const inputValue = (input.value.source as string) || "";
if (this.containsI18nPipe(inputValue)) {
const pipeUsage = this.extractI18nPipeUsage(inputValue);
if (pipeUsage) {
usages.push({
filePath,
line: input.sourceSpan.start.line + 1,
column: input.sourceSpan.start.col,
method: "pipe",
key: pipeUsage.key,
parameters: pipeUsage.parameters,
context: `[${input.name}]="${inputValue}"`,
});
}
}
}
}
// Check regular attributes
for (const attr of node.attributes || []) {
if (attr.value && this.containsI18nPipe(attr.value)) {
const pipeUsage = this.extractI18nPipeUsage(attr.value);
if (pipeUsage) {
usages.push({
filePath,
line: attr.sourceSpan.start.line + 1,
column: attr.sourceSpan.start.col,
method: "pipe",
key: pipeUsage.key,
parameters: pipeUsage.parameters,
context: `${attr.name}="${attr.value}"`,
});
}
}
}
}
}
/**
* Fallback regex-based parsing for edge cases
*/
private findWithRegex(templateContent: string, filePath: string, usages: I18nUsage[]): void {
// Find interpolation usage: {{ 'key' | i18n }}
this.findInterpolationUsage(templateContent, filePath, usages);
// Find attribute usage: [attr]="'key' | i18n"
this.findAttributeUsage(templateContent, filePath, usages);
}
/**
* Find i18n pipe usage in interpolations {{ }}
*/
private findInterpolationUsage(
templateContent: string,
filePath: string,
usages: I18nUsage[],
): void {
// Pattern to match {{ 'key' | i18n }} or {{ "key" | i18n }} with optional parameters
const interpolationPattern = /\{\{\s*['"`]([^'"`]+)['"`]\s*\|\s*i18n(?::([^}]+))?\s*\}\}/g;
let match;
while ((match = interpolationPattern.exec(templateContent)) !== null) {
const key = match[1];
const paramString = match[2];
const parameters = paramString
? paramString
.split(":")
.map((p) => p.trim())
.filter((p) => p)
: undefined;
// Check if we already found this usage via AST parsing
const position = this.getPositionInfo(templateContent, match.index);
const alreadyFound = usages.some(
(usage) =>
usage.line === position.line && usage.column === position.column && usage.key === key,
);
if (!alreadyFound) {
usages.push({
filePath,
line: position.line,
column: position.column,
method: "pipe",
key,
parameters,
context: match[0],
});
}
}
// Also handle variable interpolations: {{ variable | i18n }}
const variableInterpolationPattern =
/\{\{\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\s*\|\s*i18n(?::([^}]+))?\s*\}\}/g;
while ((match = variableInterpolationPattern.exec(templateContent)) !== null) {
const key = match[1];
const paramString = match[2];
const parameters = paramString
? paramString
.split(":")
.map((p) => p.trim())
.filter((p) => p)
: undefined;
const position = this.getPositionInfo(templateContent, match.index);
const alreadyFound = usages.some(
(usage) =>
usage.line === position.line && usage.column === position.column && usage.key === key,
);
if (!alreadyFound) {
usages.push({
filePath,
line: position.line,
column: position.column,
method: "pipe",
key,
parameters,
context: match[0],
});
}
}
}
/**
* Find i18n pipe usage in attributes
*/
private findAttributeUsage(templateContent: string, filePath: string, usages: I18nUsage[]): void {
// Pattern to match [attr]="'key' | i18n" or attr="{{ 'key' | i18n }}"
const attributePattern = /(\[?[\w-]+\]?)\s*=\s*["']([^"']*\|\s*i18n[^"']*)["']/g;
let match;
while ((match = attributePattern.exec(templateContent)) !== null) {
const attrValue = match[2];
// Extract the key from the pipe expression
const keyMatch = attrValue.match(/['"`]([^'"`]+)['"`]\s*\|\s*i18n(?::([^"'|]+))?/);
if (keyMatch) {
const key = keyMatch[1];
const paramString = keyMatch[2];
const parameters = paramString
? paramString
.split(":")
.map((p) => p.trim())
.filter((p) => p)
: undefined;
const position = this.getPositionInfo(templateContent, match.index);
const alreadyFound = usages.some(
(usage) =>
usage.line === position.line && usage.column === position.column && usage.key === key,
);
if (!alreadyFound) {
usages.push({
filePath,
line: position.line,
column: position.column,
method: "pipe",
key,
parameters,
context: match[0],
});
}
}
// Also handle variable attributes
const variableMatch = attrValue.match(
/([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\s*\|\s*i18n(?::([^"'|]+))?/,
);
if (variableMatch && !keyMatch) {
const key = variableMatch[1];
const paramString = variableMatch[2];
const parameters = paramString
? paramString
.split(":")
.map((p) => p.trim())
.filter((p) => p)
: undefined;
const position = this.getPositionInfo(templateContent, match.index);
const alreadyFound = usages.some(
(usage) =>
usage.line === position.line && usage.column === position.column && usage.key === key,
);
if (!alreadyFound) {
usages.push({
filePath,
line: position.line,
column: position.column,
method: "pipe",
key,
parameters,
context: match[0],
});
}
}
}
}
/**
* Check if a node is a bound text node
*/
private isBoundText(node: TmplAstNode): node is TmplAstBoundText {
return node.constructor.name === "BoundText" || "value" in node;
}
/**
* Check if a node is an element node
*/
private isElement(node: TmplAstNode): node is TmplAstElement {
return node.constructor.name === "Element" || ("inputs" in node && "attributes" in node);
}
/**
* Check if an expression contains i18n pipe usage
*/
private containsI18nPipe(expression: string): boolean {
return /\|\s*i18n\b/.test(expression);
}
/**
* Extract i18n pipe usage details from an expression
*/
private extractI18nPipeUsage(expression: string): { key: string; parameters?: string[] } | null {
// Match patterns like: 'key' | i18n or 'key' | i18n:param1:param2
const pipeMatch = expression.match(/['"`]([^'"`]+)['"`]\s*\|\s*i18n(?::([^|}]+))?/);
if (pipeMatch) {
const key = pipeMatch[1];
const paramString = pipeMatch[2];
const parameters = paramString
? paramString
.split(":")
.map((p) => p.trim())
.filter((p) => p)
: undefined;
return { key, parameters };
}
// Match more complex patterns with variables
const complexMatch = expression.match(/([^|]+)\s*\|\s*i18n(?::([^|}]+))?/);
if (complexMatch) {
const keyExpression = complexMatch[1].trim();
const paramString = complexMatch[2];
const parameters = paramString
? paramString
.split(":")
.map((p) => p.trim())
.filter((p) => p)
: undefined;
// For complex expressions, use the full expression as the key
return { key: keyExpression, parameters };
}
return null;
}
/**
* Get line and column information for a position in the template
*/
getPositionInfo(templateContent: string, position: number): { line: number; column: number } {
const lines = templateContent.substring(0, position).split("\n");
return {
line: lines.length,
column: lines[lines.length - 1].length + 1,
};
}
/**
* Check if a template contains any i18n pipe usage
*/
hasI18nPipeUsage(templateContent: string): boolean {
return /\|\s*i18n\b/.test(templateContent);
}
/**
* Extract all unique translation keys from a template
*/
extractTranslationKeys(templateContent: string): string[] {
const usages = this.findI18nPipeUsage(templateContent, "");
const keys = new Set(usages.map((usage) => usage.key));
return Array.from(keys);
}
}

View File

@@ -0,0 +1,235 @@
import { TransformationResult, TransformationChange, I18nUsage } from "../shared/types";
import { TemplateParser } from "./template-parser";
/**
* Template transformation utilities for migrating i18n pipes to i18n attributes
*/
export class TemplateTransformer {
private parser: TemplateParser;
constructor() {
this.parser = new TemplateParser();
}
/**
* Find all i18n pipe usage in a template file
*/
findI18nPipeUsage(templateContent: string, filePath: string): I18nUsage[] {
return this.parser.findI18nPipeUsage(templateContent, filePath);
}
/**
* Transform i18n pipes to i18n attributes in a template
*/
transformTemplate(templateContent: string, filePath: string): TransformationResult {
const changes: TransformationChange[] = [];
const errors: string[] = [];
try {
let transformedContent = templateContent;
// Transform interpolations: {{ 'key' | i18n }} -> <span i18n="@@key">key</span>
transformedContent = this.transformInterpolations(transformedContent, changes);
// Transform attributes: [title]="'key' | i18n" -> [title]="'key'" i18n-title="@@key"
transformedContent = this.transformAttributes(transformedContent, changes);
return {
success: true,
filePath,
changes,
errors,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errors.push(`Error transforming template: ${errorMessage}`);
return {
success: false,
filePath,
changes,
errors,
};
}
}
/**
* Transform interpolation usage: {{ 'key' | i18n }} -> <span i18n="@@key">key</span>
*/
private transformInterpolations(
templateContent: string,
changes: TransformationChange[],
): string {
let transformedContent = templateContent;
// Pattern for string literal interpolations
const stringInterpolationPattern =
/\{\{\s*['"`]([^'"`]+)['"`]\s*\|\s*i18n(?::([^}]+))?\s*\}\}/g;
let match;
while ((match = stringInterpolationPattern.exec(templateContent)) !== null) {
const original = match[0];
const key = match[1];
const i18nId = this.generateI18nId(key);
const replacement = `<span i18n="@@${i18nId}">${key}</span>`;
transformedContent = transformedContent.replace(original, replacement);
const position = this.getPositionInfo(templateContent, match.index);
changes.push({
type: "replace",
location: position,
original,
replacement,
description: `Transformed interpolation '${key}' to i18n attribute`,
});
}
// Pattern for variable interpolations
const variableInterpolationPattern =
/\{\{\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\s*\|\s*i18n(?::([^}]+))?\s*\}\}/g;
while ((match = variableInterpolationPattern.exec(templateContent)) !== null) {
const original = match[0];
const variable = match[1];
const i18nId = this.generateI18nId(variable);
const replacement = `<span i18n="@@${i18nId}">{{${variable}}}</span>`;
transformedContent = transformedContent.replace(original, replacement);
const position = this.getPositionInfo(templateContent, match.index);
changes.push({
type: "replace",
location: position,
original,
replacement,
description: `Transformed variable interpolation '${variable}' to i18n attribute`,
});
}
return transformedContent;
}
/**
* Transform attribute usage: [attr]="'key' | i18n" -> [attr]="'key'" i18n-attr="@@key"
*/
private transformAttributes(templateContent: string, changes: TransformationChange[]): string {
let transformedContent = templateContent;
// Pattern for attributes with i18n pipe
const attributePattern = /(\[?[\w-]+\]?)\s*=\s*["']([^"']*\|\s*i18n[^"']*)["']/g;
let match;
while ((match = attributePattern.exec(templateContent)) !== null) {
const original = match[0];
const attrName = match[1];
const attrValue = match[2];
// Extract the key from the pipe expression
const keyMatch = attrValue.match(/['"`]([^'"`]+)['"`]\s*\|\s*i18n(?::([^"'|]+))?/);
if (keyMatch) {
const key = keyMatch[1];
const i18nId = this.generateI18nId(key);
// Remove brackets if present for i18n attribute
const baseAttrName = attrName.replace(/[\[\]]/g, "");
const replacement = `${attrName}="${key}" i18n-${baseAttrName}="@@${i18nId}"`;
transformedContent = transformedContent.replace(original, replacement);
const position = this.getPositionInfo(templateContent, match.index);
changes.push({
type: "replace",
location: position,
original,
replacement,
description: `Transformed attribute '${attrName}' with key '${key}' to i18n attribute`,
});
}
// Handle variable attributes
const variableMatch = attrValue.match(
/([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*)\s*\|\s*i18n(?::([^"'|]+))?/,
);
if (variableMatch && !keyMatch) {
const variable = variableMatch[1];
const i18nId = this.generateI18nId(variable);
const baseAttrName = attrName.replace(/[\[\]]/g, "");
const replacement = `${attrName}="${variable}" i18n-${baseAttrName}="@@${i18nId}"`;
transformedContent = transformedContent.replace(original, replacement);
const position = this.getPositionInfo(templateContent, match.index);
changes.push({
type: "replace",
location: position,
original,
replacement,
description: `Transformed variable attribute '${attrName}' with variable '${variable}' to i18n attribute`,
});
}
}
return transformedContent;
}
/**
* Generate i18n ID from a translation key
*/
private generateI18nId(key: string): string {
// Convert camelCase or snake_case to kebab-case for i18n IDs
return key
.replace(/([a-z])([A-Z])/g, "$1-$2")
.replace(/_/g, "-")
.replace(/\./g, "-")
.toLowerCase();
}
/**
* Get line and column information for a position in the template
*/
private getPositionInfo(
templateContent: string,
position: number,
): { line: number; column: number } {
const lines = templateContent.substring(0, position).split("\n");
return {
line: lines.length,
column: lines[lines.length - 1].length + 1,
};
}
/**
* Validate that a transformation is correct
*/
validateTransformation(original: string, transformed: string): boolean {
try {
// Basic validation - ensure the transformed template is still valid HTML-like
const hasMatchingBrackets = this.validateBrackets(transformed);
const hasValidI18nAttributes = this.validateI18nAttributes(transformed);
const hasNoRemainingPipes = !this.parser.hasI18nPipeUsage(transformed);
return hasMatchingBrackets && hasValidI18nAttributes && hasNoRemainingPipes;
} catch (error) {
return false;
}
}
/**
* Validate that brackets are properly matched
*/
private validateBrackets(content: string): boolean {
const openBrackets = (content.match(/\{/g) || []).length;
const closeBrackets = (content.match(/\}/g) || []).length;
return openBrackets === closeBrackets;
}
/**
* Validate that i18n attributes are properly formatted
*/
private validateI18nAttributes(content: string): boolean {
const i18nAttrs = content.match(/i18n(-[\w-]+)?="@@[\w-]+"/g) || [];
return i18nAttrs.every((attr) => attr.includes("@@"));
}
}

View File

@@ -0,0 +1,209 @@
import { TemplateParser } from "../templates/template-parser";
import { TemplateTransformer } from "../templates/template-transformer";
describe("Template Migration Tools", () => {
describe("TemplateParser", () => {
let parser: TemplateParser;
beforeEach(() => {
parser = new TemplateParser();
});
it("should find i18n pipe usage in interpolations", () => {
const template = `
<div>
<h1>{{ 'welcome' | i18n }}</h1>
<p>{{ 'itemCount' | i18n:count }}</p>
</div>
`;
const usages = parser.findI18nPipeUsage(template, "test.html");
expect(usages).toHaveLength(2);
expect(usages[0].key).toBe("welcome");
expect(usages[0].method).toBe("pipe");
expect(usages[1].key).toBe("itemCount");
expect(usages[1].parameters).toEqual(["count"]);
});
it("should find i18n pipe usage in attributes", () => {
const template = `
<button [title]="'clickMe' | i18n">
Click
</button>
<input placeholder="{{ 'enterText' | i18n }}">
`;
const usages = parser.findI18nPipeUsage(template, "test.html");
expect(usages).toHaveLength(2);
expect(usages[0].key).toBe("clickMe");
expect(usages[1].key).toBe("enterText");
});
it("should handle templates without i18n pipe usage", () => {
const template = `
<div>
<h1>Static Text</h1>
<p>{{ someVariable }}</p>
</div>
`;
const usages = parser.findI18nPipeUsage(template, "test.html");
expect(usages).toHaveLength(0);
});
it("should handle malformed templates gracefully", () => {
const template = `
<div>
<h1>{{ 'test' | i18n
<p>Incomplete template
`;
// Should not throw an error
const usages = parser.findI18nPipeUsage(template, "test.html");
// May or may not find usages depending on parser robustness
expect(Array.isArray(usages)).toBe(true);
});
});
describe("TemplateTransformer", () => {
let transformer: TemplateTransformer;
beforeEach(() => {
transformer = new TemplateTransformer();
});
it("should transform simple interpolation to i18n attribute", () => {
const template = `<h1>{{ 'welcome' | i18n }}</h1>`;
const result = transformer.transformTemplate(template, "test.html");
expect(result.success).toBe(true);
expect(result.changes).toHaveLength(1);
expect(result.changes[0].replacement).toContain('i18n="@@welcome"');
expect(result.changes[0].replacement).toContain("<span");
});
it("should transform attribute with i18n pipe", () => {
const template = `<button [title]="'clickMe' | i18n">Click</button>`;
const result = transformer.transformTemplate(template, "test.html");
expect(result.success).toBe(true);
expect(result.changes).toHaveLength(1);
expect(result.changes[0].replacement).toContain('i18n-title="@@click-me"');
});
it("should handle multiple i18n pipe usages", () => {
const template = `
<div>
<h1>{{ 'title' | i18n }}</h1>
<p>{{ 'description' | i18n }}</p>
<button [title]="'buttonTitle' | i18n">{{ 'buttonText' | i18n }}</button>
</div>
`;
const result = transformer.transformTemplate(template, "test.html");
expect(result.success).toBe(true);
expect(result.changes.length).toBeGreaterThan(0);
});
it("should generate proper i18n IDs from keys", () => {
const template = `{{ 'camelCaseKey' | i18n }}`;
const result = transformer.transformTemplate(template, "test.html");
expect(result.success).toBe(true);
expect(result.changes[0].replacement).toContain("@@camel-case-key");
});
it("should handle templates without i18n pipes", () => {
const template = `
<div>
<h1>Static Title</h1>
<p>{{ someVariable }}</p>
</div>
`;
const result = transformer.transformTemplate(template, "test.html");
expect(result.success).toBe(true);
expect(result.changes).toHaveLength(0);
});
it("should validate transformations", () => {
const original = `<h1>{{ 'test' | i18n }}</h1>`;
const validTransformed = `<h1><span i18n="@@test">test</span></h1>`;
const invalidTransformed = `<h1><span i18n="invalid">test</span></h1>`;
expect(transformer.validateTransformation(original, validTransformed)).toBe(true);
expect(transformer.validateTransformation(original, invalidTransformed)).toBe(false);
});
});
describe("Integration Tests", () => {
it("should handle complex template transformation", () => {
const transformer = new TemplateTransformer();
const template = `
<div class="container">
<header>
<h1>{{ 'appTitle' | i18n }}</h1>
<nav>
<a [title]="'homeLink' | i18n" href="/">{{ 'home' | i18n }}</a>
<a [title]="'aboutLink' | i18n" href="/about">{{ 'about' | i18n }}</a>
</nav>
</header>
<main>
<p>{{ 'welcomeMessage' | i18n }}</p>
<button [disabled]="loading" [title]="'submitButton' | i18n">
{{ 'submit' | i18n }}
</button>
</main>
</div>
`;
const result = transformer.transformTemplate(template, "complex-test.html");
expect(result.success).toBe(true);
expect(result.changes.length).toBeGreaterThan(0);
// Verify that all i18n pipes were found and transformed
const originalPipeCount = (template.match(/\|\s*i18n/g) || []).length;
expect(result.changes.length).toBe(originalPipeCount);
});
it("should preserve template structure during transformation", () => {
const transformer = new TemplateTransformer();
const template = `
<div>
<p>Before: {{ 'message' | i18n }}</p>
<span>Static content</span>
<p>After: {{ 'anotherMessage' | i18n }}</p>
</div>
`;
const result = transformer.transformTemplate(template, "structure-test.html");
expect(result.success).toBe(true);
// Apply transformations to see the result
let transformedContent = template;
for (const change of result.changes.reverse()) {
if (change.original && change.replacement) {
transformedContent = transformedContent.replace(change.original, change.replacement);
}
}
// Verify structure is preserved
expect(transformedContent).toContain("<div>");
expect(transformedContent).toContain("</div>");
expect(transformedContent).toContain("Static content");
expect(transformedContent).toContain("Before:");
expect(transformedContent).toContain("After:");
});
});
});

View File

@@ -0,0 +1,194 @@
import { Project, SourceFile } from "ts-morph";
import { ASTTransformer } from "../typescript/ast-transformer";
import { MigrationConfig } from "../shared/types";
describe("TypeScript Migration Tools", () => {
let project: Project;
beforeEach(() => {
project = new Project({
useInMemoryFileSystem: true,
});
});
describe("ASTTransformer", () => {
let transformer: ASTTransformer;
let sourceFile: SourceFile;
beforeEach(() => {
transformer = new ASTTransformer();
});
it("should find I18nService.t() calls", () => {
const code = `
import { I18nService } from '@bitwarden/common/platform/services/i18n.service';
class TestComponent {
constructor(private i18nService: I18nService) {}
test() {
const message = this.i18nService.t('loginWithDevice');
const countMessage = this.i18nService.t('itemsCount', count.toString());
}
}
`;
sourceFile = project.createSourceFile("test.ts", code);
const usages = transformer.findI18nServiceCalls(sourceFile);
expect(usages).toHaveLength(2);
expect(usages[0].key).toBe("loginWithDevice");
expect(usages[0].method).toBe("t");
expect(usages[1].key).toBe("itemsCount");
expect(usages[1].parameters).toEqual(["count.toString()"]);
});
it("should transform I18nService.t() to $localize but keep import due to constructor usage", () => {
const code = `
import { I18nService } from '@bitwarden/common/platform/services/i18n.service';
class TestComponent {
constructor(private i18nService: I18nService) {}
test() {
const message = this.i18nService.t('loginWithDevice');
}
}
`;
sourceFile = project.createSourceFile("test.ts", code);
const result = transformer.transformI18nServiceCalls(sourceFile);
expect(result.success).toBe(true);
expect(result.changes).toHaveLength(1); // Only transformation, import kept due to constructor usage
expect(result.changes[0].replacement).toBe("$localize`loginWithDevice`");
expect(sourceFile.getFullText()).toContain("$localize`loginWithDevice`");
expect(sourceFile.getFullText()).toContain("I18nService"); // Import should still be there
});
it("should handle parameters in I18nService.t() calls", () => {
const code = `
class TestComponent {
test() {
const message = this.i18nService.t('itemsCount', count.toString());
}
}
`;
sourceFile = project.createSourceFile("test.ts", code);
const result = transformer.transformI18nServiceCalls(sourceFile);
expect(result.success).toBe(true);
expect(result.changes[0].replacement).toBe(
"$localize`itemsCount\${count.toString()}:param0:`",
);
});
it("should handle files without I18nService usage", () => {
const code = `
import { Component } from '@angular/core';
@Component({})
class TestComponent {
test() {
console.log('no i18n here');
}
}
`;
sourceFile = project.createSourceFile("test.ts", code);
const result = transformer.transformI18nServiceCalls(sourceFile);
expect(result.success).toBe(true);
expect(result.changes).toHaveLength(0);
});
it("should remove I18nService import when no longer used", () => {
const code = `
import { I18nService } from '@bitwarden/common/platform/services/i18n.service';
import { Component } from '@angular/core';
@Component({})
class TestComponent {
test() {
const message = this.i18nService.t('loginWithDevice');
}
}
`;
sourceFile = project.createSourceFile("test.ts", code);
const result = transformer.transformI18nServiceCalls(sourceFile);
expect(result.success).toBe(true);
expect(result.changes).toHaveLength(2); // One for transformation, one for import removal
expect(sourceFile.getFullText()).not.toContain("I18nService");
});
});
describe("Integration Tests", () => {
it("should handle complex transformation scenarios", () => {
const transformer = new ASTTransformer();
const code = `
import { I18nService } from '@bitwarden/common/platform/services/i18n.service';
import { Component } from '@angular/core';
@Component({})
class TestComponent {
constructor(private i18nService: I18nService) {}
getMessage() {
return this.i18nService.t('simpleMessage');
}
getParameterizedMessage(count: number) {
return this.i18nService.t('itemCount', count.toString());
}
getMultipleMessages() {
const msg1 = this.i18nService.t('message1');
const msg2 = this.i18nService.t('message2', 'param');
return [msg1, msg2];
}
}
`;
const sourceFile = project.createSourceFile("complex-test.ts", code);
const result = transformer.transformI18nServiceCalls(sourceFile);
expect(result.success).toBe(true);
expect(result.changes.length).toBe(4); // 4 transformations, no import removal due to constructor
const transformedCode = sourceFile.getFullText();
expect(transformedCode).toContain("$localize`simpleMessage`");
expect(transformedCode).toContain("$localize`itemCount\${count.toString()}:param0:`");
expect(transformedCode).toContain("$localize`message1`");
expect(transformedCode).toContain("$localize`message2\${'param'}:param0:`");
// Should keep the I18nService import due to constructor usage
expect(transformedCode).toContain("I18nService");
});
it("should remove import when only method calls are used (no constructor)", () => {
const transformer = new ASTTransformer();
const code = `
import { I18nService } from '@bitwarden/common/platform/services/i18n.service';
class TestComponent {
test() {
const message = this.i18nService.t('testMessage');
}
}
`;
const sourceFile = project.createSourceFile("no-constructor-test.ts", code);
const result = transformer.transformI18nServiceCalls(sourceFile);
expect(result.success).toBe(true);
expect(result.changes.length).toBe(2); // 1 transformation + 1 import removal
const transformedCode = sourceFile.getFullText();
expect(transformedCode).toContain("$localize`testMessage`");
expect(transformedCode).not.toContain("I18nService");
});
});
});

View File

@@ -0,0 +1,19 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"lib": ["es2020"],
"declaration": false,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"importHelpers": true,
"types": ["node", "jest"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,19 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"target": "es2020",
"lib": ["es2020"],
"declaration": false,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"importHelpers": true,
"types": ["node", "jest"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,197 @@
import { SourceFile, Node } from "ts-morph";
import { TransformationResult, TransformationChange, I18nUsage } from "../shared/types";
/**
* AST transformation utilities for TypeScript code migration
*/
export class ASTTransformer {
/**
* Find all I18nService.t() method calls in a source file
*/
findI18nServiceCalls(sourceFile: SourceFile): I18nUsage[] {
const usages: I18nUsage[] = [];
sourceFile.forEachDescendant((node) => {
if (Node.isCallExpression(node)) {
const expression = node.getExpression();
if (Node.isPropertyAccessExpression(expression)) {
const object = expression.getExpression();
const property = expression.getName();
// Check if this is a call to i18nService.t() or this.i18n.t()
if (property === "t" && this.isI18nServiceAccess(object)) {
const args = node.getArguments();
if (args.length > 0) {
const keyArg = args[0];
const key = this.extractStringLiteral(keyArg);
if (key) {
const parameters = args.slice(1).map((arg) => arg.getText());
const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());
usages.push({
filePath: sourceFile.getFilePath(),
line,
column,
method: "t",
key,
parameters: parameters.length > 0 ? parameters : undefined,
});
}
}
}
}
}
});
return usages;
}
/**
* Transform I18nService.t() calls to $localize calls
*/
transformI18nServiceCalls(sourceFile: SourceFile): TransformationResult {
const changes: TransformationChange[] = [];
const errors: string[] = [];
try {
// Find and replace I18nService calls
sourceFile.forEachDescendant((node) => {
if (Node.isCallExpression(node)) {
const expression = node.getExpression();
if (Node.isPropertyAccessExpression(expression)) {
const object = expression.getExpression();
const property = expression.getName();
if (property === "t" && this.isI18nServiceAccess(object)) {
const args = node.getArguments();
if (args.length > 0) {
const keyArg = args[0];
const key = this.extractStringLiteral(keyArg);
if (key) {
const { line, column } = sourceFile.getLineAndColumnAtPos(node.getStart());
const original = node.getText();
// Generate $localize replacement
const replacement = this.generateLocalizeCall(key, args.slice(1));
// Replace the node
node.replaceWithText(replacement);
changes.push({
type: "replace",
location: { line, column },
original,
replacement,
description: `Replaced i18nService.t('${key}') with $localize`,
});
}
}
}
}
}
});
// Remove I18nService imports if no longer used
this.removeUnusedI18nImports(sourceFile, changes);
return {
success: true,
filePath: sourceFile.getFilePath(),
changes,
errors,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
errors.push(`Error transforming file: ${errorMessage}`);
return {
success: false,
filePath: sourceFile.getFilePath(),
changes,
errors,
};
}
}
/**
* Check if a node represents access to I18nService
*/
private isI18nServiceAccess(node: Node): boolean {
const text = node.getText();
return text.includes("i18nService") || text.includes("i18n") || text.includes("this.i18n");
}
/**
* Extract string literal value from a node
*/
private extractStringLiteral(node: Node): string | null {
if (Node.isStringLiteral(node)) {
return node.getLiteralValue();
}
if (Node.isNoSubstitutionTemplateLiteral(node)) {
return node.getLiteralValue();
}
return null;
}
/**
* Generate $localize call with parameters
*/
private generateLocalizeCall(key: string, paramArgs: Node[]): string {
if (paramArgs.length === 0) {
return `$localize\`${key}\``;
}
// 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("")}\``;
}
/**
* Remove unused I18nService imports
*/
private removeUnusedI18nImports(sourceFile: SourceFile, changes: TransformationChange[]): void {
const imports = sourceFile.getImportDeclarations();
imports.forEach((importDecl) => {
const moduleSpecifier = importDecl.getModuleSpecifierValue();
if (moduleSpecifier.includes("i18n.service")) {
// Check if I18nService is still used in the file
const text = sourceFile.getFullText();
// Look for actual I18nService usage (constructor parameters, type annotations, etc.)
// but exclude the .t() method calls since we've transformed those
const hasI18nServiceType =
text.includes(": I18nService") ||
text.includes("<I18nService>") ||
text.includes("I18nService>") ||
text.includes("I18nService,") ||
text.includes("I18nService)");
// Check for remaining .t() calls that weren't transformed
const hasRemainingTCalls = text.match(/\bi18nService\.t\s*\(/);
// Only remove if there are no type references and no remaining method calls
if (!hasI18nServiceType && !hasRemainingTCalls) {
const { line, column } = sourceFile.getLineAndColumnAtPos(importDecl.getStart());
const original = importDecl.getText();
importDecl.remove();
changes.push({
type: "remove",
location: { line, column },
original,
description: "Removed unused I18nService import",
});
}
}
});
}
}

View File

@@ -0,0 +1,79 @@
import { Project, SourceFile } from "ts-morph";
import { MigrationConfig } from "../shared/types";
/**
* Utility class for parsing TypeScript projects using ts-morph
*/
export class ProjectParser {
private project: Project;
constructor(private config: MigrationConfig) {
this.project = new Project({
tsConfigFilePath: config.tsConfigPath,
skipAddingFilesFromTsConfig: false,
});
}
/**
* Get all source files in the project
*/
getSourceFiles(): SourceFile[] {
return this.project.getSourceFiles();
}
/**
* Get a specific source file by path
*/
getSourceFile(filePath: string): SourceFile | undefined {
return this.project.getSourceFile(filePath);
}
/**
* Add a source file to the project
*/
addSourceFile(filePath: string): SourceFile {
return this.project.addSourceFileAtPath(filePath);
}
/**
* Save all changes to disk
*/
async saveChanges(): Promise<void> {
if (!this.config.dryRun) {
await this.project.save();
}
}
/**
* Get the underlying ts-morph Project instance
*/
getProject(): Project {
return this.project;
}
/**
* Find files that import I18nService
*/
findI18nServiceImports(): SourceFile[] {
return this.project.getSourceFiles().filter((sourceFile) => {
return sourceFile.getImportDeclarations().some((importDecl) => {
const moduleSpecifier = importDecl.getModuleSpecifierValue();
return (
moduleSpecifier.includes("i18n.service") ||
moduleSpecifier.includes("@bitwarden/common/platform/services/i18n.service")
);
});
});
}
/**
* Find files that use the i18n pipe in template strings
*/
findI18nPipeUsage(): SourceFile[] {
return this.project.getSourceFiles().filter((sourceFile) => {
const text = sourceFile.getFullText();
return text.includes("| i18n") || text.includes("|i18n");
});
}
}

View File

@@ -0,0 +1,174 @@
/* eslint-disable no-console */
import { MigrationConfig, TransformationResult, I18nUsage } from "../shared/types";
import { ASTTransformer } from "./ast-transformer";
import { ProjectParser } from "./project-parser";
/**
* Main class for TypeScript code migration from I18nService to $localize
*/
export class TypeScriptMigrator {
private parser: ProjectParser;
private transformer: ASTTransformer;
constructor(private config: MigrationConfig) {
this.parser = new ProjectParser(config);
this.transformer = new ASTTransformer();
}
/**
* Analyze current I18nService usage across the project
*/
analyzeUsage(): I18nUsage[] {
const sourceFiles = this.parser.findI18nServiceImports();
const allUsages: I18nUsage[] = [];
sourceFiles.forEach((sourceFile) => {
const usages = this.transformer.findI18nServiceCalls(sourceFile);
allUsages.push(...usages);
});
return allUsages;
}
/**
* Generate analysis report of current usage patterns
*/
generateAnalysisReport(): string {
const usages = this.analyzeUsage();
const fileCount = new Set(usages.map((u) => u.filePath)).size;
const keyCount = new Set(usages.map((u) => u.key)).size;
let report = `# I18nService Usage Analysis Report\n\n`;
report += `## Summary\n`;
report += `- Total usage count: ${usages.length}\n`;
report += `- Files affected: ${fileCount}\n`;
report += `- Unique translation keys: ${keyCount}\n\n`;
report += `## Usage by File\n`;
const usagesByFile = usages.reduce(
(acc, usage) => {
if (!acc[usage.filePath]) {
acc[usage.filePath] = [];
}
acc[usage.filePath].push(usage);
return acc;
},
{} as Record<string, I18nUsage[]>,
);
Object.entries(usagesByFile).forEach(([filePath, fileUsages]) => {
report += `\n### ${filePath}\n`;
fileUsages.forEach((usage) => {
report += `- Line ${usage.line}: \`${usage.key}\``;
if (usage.parameters) {
report += ` (with parameters: ${usage.parameters.join(", ")})`;
}
report += `\n`;
});
});
report += `\n## Most Common Keys\n`;
const keyCounts = usages.reduce(
(acc, usage) => {
acc[usage.key] = (acc[usage.key] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
Object.entries(keyCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.forEach(([key, count]) => {
report += `- \`${key}\`: ${count} usage(s)\n`;
});
return report;
}
/**
* Migrate all TypeScript files in the project
*/
async migrateAll(): Promise<TransformationResult[]> {
const sourceFiles = this.parser.findI18nServiceImports();
const results: TransformationResult[] = [];
if (this.config.verbose) {
console.log(`Found ${sourceFiles.length} files with I18nService imports`);
}
for (const sourceFile of sourceFiles) {
if (this.config.verbose) {
console.log(`Processing: ${sourceFile.getFilePath()}`);
}
const result = this.transformer.transformI18nServiceCalls(sourceFile);
results.push(result);
if (!result.success) {
console.error(`Failed to process ${result.filePath}:`, result.errors);
}
}
// Save changes if not in dry run mode
if (!this.config.dryRun) {
await this.parser.saveChanges();
}
return results;
}
/**
* Migrate a specific file
*/
async migrateFile(filePath: string): Promise<TransformationResult> {
const sourceFile = this.parser.getSourceFile(filePath);
if (!sourceFile) {
return {
success: false,
filePath,
changes: [],
errors: [`File not found: ${filePath}`],
};
}
const result = this.transformer.transformI18nServiceCalls(sourceFile);
if (!this.config.dryRun && result.success) {
await this.parser.saveChanges();
}
return result;
}
/**
* Generate migration statistics
*/
generateMigrationStats(results: TransformationResult[]): string {
const successful = results.filter((r) => r.success).length;
const failed = results.filter((r) => !r.success).length;
const totalChanges = results.reduce((sum, r) => sum + r.changes.length, 0);
let stats = `# Migration Statistics\n\n`;
stats += `- Files processed: ${results.length}\n`;
stats += `- Successful: ${successful}\n`;
stats += `- Failed: ${failed}\n`;
stats += `- Total changes: ${totalChanges}\n\n`;
if (failed > 0) {
stats += `## Failed Files\n`;
results
.filter((r) => !r.success)
.forEach((result) => {
stats += `- ${result.filePath}\n`;
result.errors.forEach((error) => {
stats += ` - ${error}\n`;
});
});
}
return stats;
}
}