# @isa/oms/utils/translation A lightweight translation utility library for OMS receipt types providing human-readable German translations through both service-based and pipe-based interfaces. ## Overview The OMS Translation utility library provides a simple, extensible translation system for the OMS `ReceiptType` enum. It offers a clean abstraction for translating receipt type codes (numeric enums) into human-readable German text, with support for custom translation overrides via Angular's dependency injection system. The library includes both a service for programmatic translations and an Angular pipe for declarative template usage. ## Table of Contents - [Features](#features) - [Quick Start](#quick-start) - [Core Concepts](#core-concepts) - [API Reference](#api-reference) - [Usage Examples](#usage-examples) - [Receipt Types](#receipt-types) - [Custom Translations](#custom-translations) - [Testing](#testing) - [Architecture Notes](#architecture-notes) ## Features - **13 receipt type translations** - Complete German translation coverage for all OMS receipt types - **Service-based translation** - `ReceiptTypeTranslationService` for programmatic use - **Angular pipe** - `omsReceiptTypeTranslation` pipe for template-based translation - **Dependency injection** - Full DI support with `providedIn: 'root'` - **Custom overrides** - Replace default translations with custom implementations - **Fallback handling** - Automatic enum name fallback for missing translations - **Type-safe** - Strongly typed translation dictionary with TypeScript - **Zero configuration** - Works out of the box with sensible defaults - **Lightweight** - Minimal dependencies, purely translation focused ## Quick Start ### 1. Import and Use the Service ```typescript import { Component, inject } from '@angular/core'; import { ReceiptTypeTranslationService } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; @Component({ selector: 'app-receipt-display', template: '...' }) export class ReceiptDisplayComponent { #translationService = inject(ReceiptTypeTranslationService); displayReceiptType(type: ReceiptType): void { const translation = this.#translationService.translate(type); console.log(translation); // Output: "Lieferschein" } } ``` ### 2. Use the Pipe in Templates ```typescript import { Component } from '@angular/core'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; @Component({ selector: 'app-receipt-list', standalone: true, imports: [ReceiptTypeTranslationPipe], template: `
{{ receiptType | omsReceiptTypeTranslation }}
` }) export class ReceiptListComponent { receiptType = ReceiptType.ShippingNote; // Displays: "Lieferschein" } ``` ### 3. Simple Example with Multiple Types ```typescript import { Component, inject } from '@angular/core'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; @Component({ selector: 'app-receipt-types', standalone: true, imports: [ReceiptTypeTranslationPipe], template: ` ` }) export class ReceiptTypesComponent { receiptTypes = [ ReceiptType.ShippingNote, // "Lieferschein" ReceiptType.Invoice, // "Rechnung" ReceiptType.CreditNote, // "Gutschrift" ReceiptType.ReturnReceipt // "Retourenbeleg" ]; } ``` ## Core Concepts ### Translation Dictionary The library provides a default translation dictionary mapping each `ReceiptType` enum value to its German translation: ```typescript const receiptTypeTranslations = { [ReceiptType.NotSet]: 'Nicht gesetzt', [ReceiptType.ShippingNote]: 'Lieferschein', [ReceiptType.CreditNote]: 'Gutschrift', [ReceiptType.CollectiveShippingNote]: 'Sammellieferschein', [ReceiptType.CollectiveCreditNote]: 'Sammelgutschrift', [ReceiptType.BonusCardCollectiveShippingNote]: 'Bonuskarte Sammellieferschein', [ReceiptType.BonusCardCollectiveCreditNote]: 'Bonuskarte Sammelgutschrift', [ReceiptType.PaymentReceipt]: 'Zahlungsbeleg', [ReceiptType.Invoice]: 'Rechnung', [ReceiptType.CollectiveInvoice]: 'Sammelrechnung', [ReceiptType.ProformaInvoice]: 'Proforma-Rechnung', [ReceiptType.CashReceipt]: 'Kassenbeleg', [ReceiptType.ReturnReceipt]: 'Retourenbeleg', }; ``` ### ReceiptType Enum Structure The `ReceiptType` enum from `@isa/oms/data-access` uses bit flag values: ```typescript export enum ReceiptType { NotSet = 0, // No receipt type set ShippingNote = 1, // Standard shipping document CreditNote = 2, // Credit/refund document CollectiveShippingNote = 4, // Batch shipping document CollectiveCreditNote = 8, // Batch credit document BonusCardCollectiveShippingNote = 16, // Bonus card batch shipping BonusCardCollectiveCreditNote = 32, // Bonus card batch credit PaymentReceipt = 64, // Payment confirmation Invoice = 128, // Customer invoice CollectiveInvoice = 256, // Batch invoice ProformaInvoice = 512, // Pro forma invoice CashReceipt = 1024, // Cash register receipt ReturnReceipt = 2048, // Return/remission receipt } ``` **Note**: While the enum uses bit flag values (powers of 2), the current implementation treats them as discrete values, not as combinable flags. Each receipt has a single type. ### Dependency Injection Pattern The library uses Angular's dependency injection system with an `InjectionToken`: ```typescript export const RECEIPT_TYPE_TRANSLATION = new InjectionToken( 'RECEIPT_TYPE_TRANSLATION', { factory() { return receiptTypeTranslations; // Default translations }, } ); ``` This pattern enables: 1. **Default behavior** - Automatic injection of default translations 2. **Custom overrides** - Provide custom translations at any level (root, module, component) 3. **Testability** - Easy mocking in unit tests 4. **Type safety** - TypeScript ensures translation completeness ### Fallback Mechanism The service includes intelligent fallback handling: ```typescript translate(type: ReceiptType): string { return this.#translation[type] ?? ReceiptType[type]; } ``` **Behavior**: - **Translation exists**: Returns the German translation - **Translation missing**: Returns the enum name as a string (e.g., "ShippingNote") - **Invalid type**: Returns the numeric value as a string (e.g., "999") This ensures the system never crashes, even with unexpected or new receipt types. ## API Reference ### ReceiptTypeTranslationService Main service for programmatic receipt type translation. #### `translate(type: ReceiptType): string` Translates a receipt type enum value to its German text representation. **Parameters:** - `type: ReceiptType` - The receipt type enum value to translate **Returns:** `string` - German translation, or enum name if translation missing **Example:** ```typescript import { inject } from '@angular/core'; import { ReceiptTypeTranslationService } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; const service = inject(ReceiptTypeTranslationService); service.translate(ReceiptType.Invoice); // "Rechnung" service.translate(ReceiptType.ShippingNote); // "Lieferschein" service.translate(ReceiptType.NotSet); // "Nicht gesetzt" ``` **Fallback behavior:** ```typescript // Missing translation service.translate(999 as ReceiptType); // "999" // All standard types have translations service.translate(ReceiptType.CashReceipt); // "Kassenbeleg" ``` ### ReceiptTypeTranslationPipe Angular pipe for declarative template-based translation. #### `transform(value?: ReceiptType): string` **Pipe Name:** `omsReceiptTypeTranslation` **Parameters:** - `value?: ReceiptType` - Receipt type to translate (defaults to `ReceiptType.NotSet`) **Returns:** `string` - German translation **Example:** ```typescript import { Component } from '@angular/core'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; @Component({ selector: 'app-example', standalone: true, imports: [ReceiptTypeTranslationPipe], template: ` {{ receiptType | omsReceiptTypeTranslation }} {{ undefined | omsReceiptTypeTranslation }} @if (receipt) { {{ receipt.type | omsReceiptTypeTranslation }} } ` }) export class ExampleComponent { receiptType = ReceiptType.Invoice; } ``` ### Types and Tokens #### `ReceiptTypeTranslation` Type representing the translation dictionary structure. ```typescript export type ReceiptTypeTranslation = { [ReceiptType.NotSet]: string; [ReceiptType.ShippingNote]: string; [ReceiptType.CreditNote]: string; [ReceiptType.CollectiveShippingNote]: string; [ReceiptType.CollectiveCreditNote]: string; [ReceiptType.BonusCardCollectiveShippingNote]: string; [ReceiptType.BonusCardCollectiveCreditNote]: string; [ReceiptType.PaymentReceipt]: string; [ReceiptType.Invoice]: string; [ReceiptType.CollectiveInvoice]: string; [ReceiptType.ProformaInvoice]: string; [ReceiptType.CashReceipt]: string; [ReceiptType.ReturnReceipt]: string; }; ``` #### `RECEIPT_TYPE_TRANSLATION` Injection token for providing custom translations. ```typescript export const RECEIPT_TYPE_TRANSLATION: InjectionToken; ``` ### Provider Functions #### `provideReceiptTypeTranslation(translation: ReceiptTypeTranslation): Provider[]` Creates providers for custom receipt type translations. **Parameters:** - `translation: ReceiptTypeTranslation` - Custom translation dictionary **Returns:** `Provider[]` - Angular providers array **Example:** ```typescript import { provideReceiptTypeTranslation } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; const customTranslations = { [ReceiptType.NotSet]: 'Kein Typ', [ReceiptType.ShippingNote]: 'Versandschein', [ReceiptType.Invoice]: 'Faktura', // ... other translations }; export const appConfig = { providers: [ provideReceiptTypeTranslation(customTranslations) ] }; ``` ## Usage Examples ### Basic Template Usage ```typescript import { Component } from '@angular/core'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; @Component({ selector: 'app-receipt-header', standalone: true, imports: [ReceiptTypeTranslationPipe], template: `

{{ receiptType | omsReceiptTypeTranslation }}

Receipt #{{ receiptNumber }}

` }) export class ReceiptHeaderComponent { receiptType = ReceiptType.Invoice; receiptNumber = '12345'; } ``` ### Service-Based Translation in Component Logic ```typescript import { Component, inject, OnInit } from '@angular/core'; import { ReceiptTypeTranslationService } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; interface Receipt { id: number; type: ReceiptType; amount: number; } @Component({ selector: 'app-receipt-summary', template: `

Receipt Summary

{{ summaryText }}

` }) export class ReceiptSummaryComponent implements OnInit { #translationService = inject(ReceiptTypeTranslationService); summaryText = ''; receipts: Receipt[] = [ { id: 1, type: ReceiptType.Invoice, amount: 100 }, { id: 2, type: ReceiptType.CreditNote, amount: 50 }, { id: 3, type: ReceiptType.ShippingNote, amount: 0 } ]; ngOnInit(): void { this.summaryText = this.generateSummary(); } private generateSummary(): string { return this.receipts .map(r => { const typeText = this.#translationService.translate(r.type); return `${typeText}: €${r.amount}`; }) .join(', '); // Output: "Rechnung: €100, Gutschrift: €50, Lieferschein: €0" } } ``` ### Dynamic Receipt Type Display ```typescript import { Component, input, computed } from '@angular/core'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; @Component({ selector: 'app-receipt-badge', standalone: true, imports: [ReceiptTypeTranslationPipe], template: ` {{ type() | omsReceiptTypeTranslation }} `, styles: [` .badge { padding: 4px 8px; border-radius: 4px; } .badge-invoice { background: #e3f2fd; color: #1976d2; } .badge-credit { background: #e8f5e9; color: #388e3c; } .badge-shipping { background: #fff3e0; color: #f57c00; } .badge-return { background: #fce4ec; color: #c2185b; } `] }) export class ReceiptBadgeComponent { type = input.required(); badgeClass = computed(() => { const typeValue = this.type(); switch (typeValue) { case ReceiptType.Invoice: case ReceiptType.CollectiveInvoice: return 'badge badge-invoice'; case ReceiptType.CreditNote: case ReceiptType.CollectiveCreditNote: return 'badge badge-credit'; case ReceiptType.ShippingNote: case ReceiptType.CollectiveShippingNote: return 'badge badge-shipping'; case ReceiptType.ReturnReceipt: return 'badge badge-return'; default: return 'badge'; } }); } ``` ### List Display with Filtering ```typescript import { Component, signal, computed } from '@angular/core'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; interface ReceiptListItem { id: number; type: ReceiptType; date: Date; customer: string; } @Component({ selector: 'app-receipt-list', standalone: true, imports: [ReceiptTypeTranslationPipe], template: `

Receipts

@for (receipt of filteredReceipts(); track receipt.id) { }
ID Type Customer Date
{{ receipt.id }} {{ receipt.type | omsReceiptTypeTranslation }} {{ receipt.customer }} {{ receipt.date | date }}
` }) export class ReceiptListComponent { receiptTypes = [ { value: ReceiptType.Invoice }, { value: ReceiptType.CreditNote }, { value: ReceiptType.ShippingNote }, { value: ReceiptType.ReturnReceipt } ]; allReceipts = signal([ { id: 1, type: ReceiptType.Invoice, date: new Date(), customer: 'Customer A' }, { id: 2, type: ReceiptType.CreditNote, date: new Date(), customer: 'Customer B' }, { id: 3, type: ReceiptType.ShippingNote, date: new Date(), customer: 'Customer C' }, ]); selectedFilter = signal(null); filteredReceipts = computed(() => { const filter = this.selectedFilter(); if (filter === null) return this.allReceipts(); return this.allReceipts().filter(r => r.type === filter); }); onFilterChange(event: Event): void { const value = (event.target as HTMLSelectElement).value; this.selectedFilter.set(value ? Number(value) as ReceiptType : null); } } ``` ### Integration with OMS Return Details Real-world example from the OMS feature library: ```typescript import { Component, input } from '@angular/core'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; interface ReturnData { receiptType: ReceiptType; receiptNumber: string; // ... other properties } @Component({ selector: 'oms-feature-return-details-data', standalone: true, imports: [ReceiptTypeTranslationPipe], template: `
{{ data().receiptType | omsReceiptTypeTranslation }}
{{ data().receiptNumber }}
` }) export class ReturnDetailsDataComponent { data = input.required(); } ``` ### Programmatic Translation with Type Guards ```typescript import { Component, inject } from '@angular/core'; import { ReceiptTypeTranslationService } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; @Component({ selector: 'app-receipt-validator', template: '...' }) export class ReceiptValidatorComponent { #translationService = inject(ReceiptTypeTranslationService); validateAndDescribeReceipt(type: ReceiptType | number): string { // Type guard to ensure valid ReceiptType if (!this.isValidReceiptType(type)) { return `Invalid receipt type: ${type}`; } const translation = this.#translationService.translate(type); const category = this.categorizeReceipt(type); return `${translation} (Category: ${category})`; } private isValidReceiptType(type: ReceiptType | number): type is ReceiptType { return Object.values(ReceiptType).includes(type as ReceiptType); } private categorizeReceipt(type: ReceiptType): string { // Categorize based on receipt type if (type === ReceiptType.Invoice || type === ReceiptType.CollectiveInvoice) { return 'Billing'; } if (type === ReceiptType.CreditNote || type === ReceiptType.CollectiveCreditNote) { return 'Refund'; } if (type === ReceiptType.ShippingNote || type === ReceiptType.CollectiveShippingNote) { return 'Shipping'; } return 'Other'; } } ``` ## Receipt Types ### Complete Receipt Type Reference | Enum Value | Numeric Value | German Translation | Description | |------------|--------------|-------------------|-------------| | `NotSet` | 0 | Nicht gesetzt | No receipt type assigned | | `ShippingNote` | 1 | Lieferschein | Standard delivery/shipping document | | `CreditNote` | 2 | Gutschrift | Credit note for refunds | | `CollectiveShippingNote` | 4 | Sammellieferschein | Batch/collective shipping document | | `CollectiveCreditNote` | 8 | Sammelgutschrift | Batch/collective credit note | | `BonusCardCollectiveShippingNote` | 16 | Bonuskarte Sammellieferschein | Bonus card batch shipping | | `BonusCardCollectiveCreditNote` | 32 | Bonuskarte Sammelgutschrift | Bonus card batch credit | | `PaymentReceipt` | 64 | Zahlungsbeleg | Payment confirmation document | | `Invoice` | 128 | Rechnung | Customer invoice | | `CollectiveInvoice` | 256 | Sammelrechnung | Batch/collective invoice | | `ProformaInvoice` | 512 | Proforma-Rechnung | Pro forma invoice (quotation) | | `CashReceipt` | 1024 | Kassenbeleg | Cash register receipt | | `ReturnReceipt` | 2048 | Retourenbeleg | Return/remission receipt | ### Receipt Type Categories #### Shipping Documents - **ShippingNote** (1) - Individual shipping - **CollectiveShippingNote** (4) - Batch shipping - **BonusCardCollectiveShippingNote** (16) - Bonus card shipping #### Credit Documents - **CreditNote** (2) - Individual credit - **CollectiveCreditNote** (8) - Batch credit - **BonusCardCollectiveCreditNote** (32) - Bonus card credit #### Invoice Documents - **Invoice** (128) - Standard invoice - **CollectiveInvoice** (256) - Batch invoice - **ProformaInvoice** (512) - Quotation invoice #### Payment Documents - **PaymentReceipt** (64) - Payment confirmation - **CashReceipt** (1024) - Cash transaction #### Return Documents - **ReturnReceipt** (2048) - Returns/remissions ### Bit Flag Architecture The enum uses bit flag values (powers of 2), allowing for potential future combination operations: ```typescript // Current usage: Single discrete values const type = ReceiptType.Invoice; // 128 // Potential future usage: Bitwise combinations (not currently implemented) // const combined = ReceiptType.Invoice | ReceiptType.ShippingNote; // 129 ``` **Important**: The current implementation does not support bitwise operations. Each receipt has exactly one type. ## Custom Translations ### Providing Custom Translations You can override the default translations at any level of your application using the `provideReceiptTypeTranslation` function: #### Application-Level Override ```typescript import { ApplicationConfig } from '@angular/core'; import { provideReceiptTypeTranslation } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; const customTranslations = { [ReceiptType.NotSet]: 'Kein Typ', [ReceiptType.ShippingNote]: 'Versandschein', [ReceiptType.CreditNote]: 'Kreditnote', [ReceiptType.CollectiveShippingNote]: 'Sammel-Versandschein', [ReceiptType.CollectiveCreditNote]: 'Sammel-Kreditnote', [ReceiptType.BonusCardCollectiveShippingNote]: 'Bonuskarte Sammel-Versandschein', [ReceiptType.BonusCardCollectiveCreditNote]: 'Bonuskarte Sammel-Kreditnote', [ReceiptType.PaymentReceipt]: 'Zahlungsnachweis', [ReceiptType.Invoice]: 'Faktura', [ReceiptType.CollectiveInvoice]: 'Sammel-Faktura', [ReceiptType.ProformaInvoice]: 'Proforma-Faktura', [ReceiptType.CashReceipt]: 'Kassenbon', [ReceiptType.ReturnReceipt]: 'Rückgabe-Beleg', }; export const appConfig: ApplicationConfig = { providers: [ provideReceiptTypeTranslation(customTranslations) ] }; ``` #### Component-Level Override ```typescript import { Component } from '@angular/core'; import { provideReceiptTypeTranslation } from '@isa/oms/utils/translation'; import { ReceiptTypeTranslationPipe } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; const shortTranslations = { [ReceiptType.NotSet]: 'N/A', [ReceiptType.ShippingNote]: 'LS', [ReceiptType.CreditNote]: 'GS', [ReceiptType.CollectiveShippingNote]: 'SLS', [ReceiptType.CollectiveCreditNote]: 'SGS', [ReceiptType.BonusCardCollectiveShippingNote]: 'BK-SLS', [ReceiptType.BonusCardCollectiveCreditNote]: 'BK-SGS', [ReceiptType.PaymentReceipt]: 'ZB', [ReceiptType.Invoice]: 'RE', [ReceiptType.CollectiveInvoice]: 'SRE', [ReceiptType.ProformaInvoice]: 'PRO', [ReceiptType.CashReceipt]: 'KB', [ReceiptType.ReturnReceipt]: 'RB', }; @Component({ selector: 'app-compact-receipt-view', standalone: true, imports: [ReceiptTypeTranslationPipe], providers: [ provideReceiptTypeTranslation(shortTranslations) ], template: ` {{ receiptType | omsReceiptTypeTranslation }} ` }) export class CompactReceiptViewComponent { receiptType = ReceiptType.Invoice; // Displays: "RE" } ``` #### Testing with Custom Translations ```typescript import { TestBed } from '@angular/core/testing'; import { ReceiptTypeTranslationService, provideReceiptTypeTranslation } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; describe('Custom Translation', () => { it('should use custom translations', () => { const customTranslations = { [ReceiptType.NotSet]: 'Custom NotSet', [ReceiptType.ShippingNote]: 'Custom Shipping', // ... other translations }; TestBed.configureTestingModule({ providers: [provideReceiptTypeTranslation(customTranslations)] }); const service = TestBed.inject(ReceiptTypeTranslationService); expect(service.translate(ReceiptType.ShippingNote)).toBe('Custom Shipping'); }); }); ``` ### Direct Token Override For advanced scenarios, you can provide the translation directly using the injection token: ```typescript import { Component } from '@angular/core'; import { RECEIPT_TYPE_TRANSLATION } from '@isa/oms/utils/translation'; import { ReceiptType } from '@isa/oms/data-access'; @Component({ selector: 'app-advanced', providers: [ { provide: RECEIPT_TYPE_TRANSLATION, useValue: { [ReceiptType.NotSet]: 'Custom Translation', // ... other translations } } ], template: '...' }) export class AdvancedComponent {} ``` ## Testing The library uses **Jest** with **jest-preset-angular** for testing. ### Running Tests ```bash # Run tests for this library npx nx test oms-utils-translation --skip-nx-cache # Run tests with coverage npx nx test oms-utils-translation --code-coverage --skip-nx-cache # Run tests in watch mode npx nx test oms-utils-translation --watch ``` ### Test Examples #### Testing the Service ```typescript import { TestBed } from '@angular/core/testing'; import { ReceiptTypeTranslationService } from './receipt-type-translation.service'; import { ReceiptType } from '@isa/oms/data-access'; describe('ReceiptTypeTranslationService', () => { let service: ReceiptTypeTranslationService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ReceiptTypeTranslationService] }); service = TestBed.inject(ReceiptTypeTranslationService); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should translate ShippingNote correctly', () => { expect(service.translate(ReceiptType.ShippingNote)).toBe('Lieferschein'); }); it('should translate Invoice correctly', () => { expect(service.translate(ReceiptType.Invoice)).toBe('Rechnung'); }); it('should translate NotSet correctly', () => { expect(service.translate(ReceiptType.NotSet)).toBe('Nicht gesetzt'); }); it('should fallback to enum name for unknown types', () => { const unknownType = 9999 as ReceiptType; expect(service.translate(unknownType)).toBe('9999'); }); it('should translate all receipt types', () => { const types = [ ReceiptType.NotSet, ReceiptType.ShippingNote, ReceiptType.CreditNote, ReceiptType.CollectiveShippingNote, ReceiptType.CollectiveCreditNote, ReceiptType.BonusCardCollectiveShippingNote, ReceiptType.BonusCardCollectiveCreditNote, ReceiptType.PaymentReceipt, ReceiptType.Invoice, ReceiptType.CollectiveInvoice, ReceiptType.ProformaInvoice, ReceiptType.CashReceipt, ReceiptType.ReturnReceipt, ]; types.forEach(type => { const result = service.translate(type); expect(result).toBeTruthy(); expect(typeof result).toBe('string'); expect(result.length).toBeGreaterThan(0); }); }); }); ``` #### Testing the Pipe ```typescript import { TestBed } from '@angular/core/testing'; import { ReceiptTypeTranslationPipe } from './receipt-type-translation.pipe'; import { ReceiptTypeTranslationService } from './receipt-type-translation.service'; import { ReceiptType } from '@isa/oms/data-access'; describe('ReceiptTypeTranslationPipe', () => { let pipe: ReceiptTypeTranslationPipe; beforeEach(() => { TestBed.configureTestingModule({ providers: [ ReceiptTypeTranslationPipe, ReceiptTypeTranslationService ] }); pipe = TestBed.inject(ReceiptTypeTranslationPipe); }); it('should create', () => { expect(pipe).toBeTruthy(); }); it('should transform ShippingNote', () => { expect(pipe.transform(ReceiptType.ShippingNote)).toBe('Lieferschein'); }); it('should transform Invoice', () => { expect(pipe.transform(ReceiptType.Invoice)).toBe('Rechnung'); }); it('should use NotSet as default', () => { expect(pipe.transform()).toBe('Nicht gesetzt'); }); it('should handle undefined by using default', () => { expect(pipe.transform(undefined as any)).toBe('Nicht gesetzt'); }); }); ``` #### Testing Custom Translations ```typescript import { TestBed } from '@angular/core/testing'; import { ReceiptTypeTranslationService, provideReceiptTypeTranslation, ReceiptTypeTranslation } from './receipt-type-translation.service'; import { ReceiptType } from '@isa/oms/data-access'; describe('Custom Receipt Type Translation', () => { it('should use custom translation dictionary', () => { const customTranslations: ReceiptTypeTranslation = { [ReceiptType.NotSet]: 'Test NotSet', [ReceiptType.ShippingNote]: 'Test Shipping', [ReceiptType.CreditNote]: 'Test Credit', [ReceiptType.CollectiveShippingNote]: 'Test Collective Shipping', [ReceiptType.CollectiveCreditNote]: 'Test Collective Credit', [ReceiptType.BonusCardCollectiveShippingNote]: 'Test Bonus Shipping', [ReceiptType.BonusCardCollectiveCreditNote]: 'Test Bonus Credit', [ReceiptType.PaymentReceipt]: 'Test Payment', [ReceiptType.Invoice]: 'Test Invoice', [ReceiptType.CollectiveInvoice]: 'Test Collective Invoice', [ReceiptType.ProformaInvoice]: 'Test Proforma', [ReceiptType.CashReceipt]: 'Test Cash', [ReceiptType.ReturnReceipt]: 'Test Return', }; TestBed.configureTestingModule({ providers: [provideReceiptTypeTranslation(customTranslations)] }); const service = TestBed.inject(ReceiptTypeTranslationService); expect(service.translate(ReceiptType.ShippingNote)).toBe('Test Shipping'); expect(service.translate(ReceiptType.Invoice)).toBe('Test Invoice'); }); }); ``` ### Test Coverage Goals The library should maintain high test coverage: - **Service tests**: All translation methods and fallback behavior - **Pipe tests**: Transform method with various inputs - **Custom provider tests**: Override functionality - **Integration tests**: Pipe using service correctly ## Architecture Notes ### Current Architecture The library follows a simple, focused architecture: ``` Components/Features ↓ ReceiptTypeTranslationPipe (template usage) ↓ ReceiptTypeTranslationService (business logic) ↓ RECEIPT_TYPE_TRANSLATION (injection token) ↓ receiptTypeTranslations (default dictionary) ``` ### Design Principles 1. **Single Responsibility** - Library focuses solely on receipt type translation 2. **Dependency Injection** - Full Angular DI integration for flexibility 3. **Type Safety** - TypeScript ensures translation completeness 4. **Fallback Safety** - Never crashes on unknown types 5. **Extensibility** - Easy to override translations at any level 6. **Minimal Dependencies** - Only depends on Angular core and OMS data-access ### Architectural Considerations #### 1. Standalone vs Module-Based (Completed) **Current State**: The pipe is a traditional module-based component (not standalone). **Recommendation**: Migrate to standalone component in line with Angular 20 best practices: ```typescript // Current @Pipe({ name: 'omsReceiptTypeTranslation' }) export class ReceiptTypeTranslationPipe {} // Recommended @Pipe({ name: 'omsReceiptTypeTranslation', standalone: true }) export class ReceiptTypeTranslationPipe {} ``` **Impact**: Low risk, improves consistency with modern Angular patterns #### 2. Internationalization (i18n) Integration (Future Enhancement) **Current State**: Hardcoded German translations. **Future Option**: Integrate with Angular i18n or external translation library: ```typescript // Potential future implementation @Injectable({ providedIn: 'root' }) export class ReceiptTypeTranslationService { #translateService = inject(TranslateService); translate(type: ReceiptType): string { return this.#translateService.instant(`receipt.type.${ReceiptType[type]}`); } } ``` **Impact**: Medium priority if multi-language support is needed #### 3. Translation Completeness Validation (Low Priority) **Current State**: TypeScript ensures all enum values have translations. **Potential Enhancement**: Runtime validation to ensure completeness: ```typescript function validateTranslationCompleteness( translations: ReceiptTypeTranslation ): void { const allTypes = Object.values(ReceiptType).filter(v => typeof v === 'number'); const missingTranslations = allTypes.filter( type => !translations[type as ReceiptType] ); if (missingTranslations.length > 0) { console.warn('Missing translations for:', missingTranslations); } } ``` **Impact**: Low priority, type safety already provides compile-time checks ### Performance Considerations 1. **Service Singleton** - `providedIn: 'root'` ensures single instance 2. **Pure Pipe** - Pipe should be marked as pure for Angular optimization: ```typescript @Pipe({ name: 'omsReceiptTypeTranslation', pure: true // Recommended addition }) ``` 3. **Dictionary Lookup** - O(1) constant time translation lookup 4. **No External Calls** - All translations in memory, no HTTP requests ### Future Enhancements Potential improvements identified: 1. **Standalone Migration** - Convert pipe to standalone component 2. **Pure Pipe Flag** - Add explicit pure flag for clarity 3. **Multi-Language Support** - Integration with i18n system 4. **Translation Caching** - Optional caching for repeated translations (likely unnecessary) 5. **Abbreviation Support** - Optional short form translations (e.g., "LS" for Lieferschein) 6. **Category Grouping** - Helper methods to group by category (shipping, invoice, etc.) ## Dependencies ### Required Libraries - `@angular/core` - Angular framework (dependency injection, pipes) - `@isa/oms/data-access` - OMS data models (ReceiptType enum) ### Path Alias Import from: `@isa/oms/utils/translation` ### Export Structure ```typescript // Main exports export { ReceiptTypeTranslationService } from './lib/receipt-type/receipt-type-translation.service'; export { ReceiptTypeTranslationPipe } from './lib/receipt-type/receipt-type-translation.pipe'; export { RECEIPT_TYPE_TRANSLATION, provideReceiptTypeTranslation, type ReceiptTypeTranslation } from './lib/receipt-type/receipt-type-translation.service'; ``` ## Best Practices ### 1. Prefer the Pipe in Templates ```typescript // ✅ Good - declarative and clear template: `{{ type | omsReceiptTypeTranslation }}` // ❌ Avoid - unnecessary service injection for simple display template: `{{ getTranslation(type) }}` ``` ### 2. Use the Service for Business Logic ```typescript // ✅ Good - service for programmatic translation generateReport(): string { return this.#translationService.translate(this.receiptType); } // ❌ Avoid - pipe is for templates only ``` ### 3. Type Safety First ```typescript // ✅ Good - type-safe translation translate(type: ReceiptType): string { return this.#service.translate(type); } // ❌ Avoid - loosing type safety translate(type: any): string { return this.#service.translate(type); } ``` ### 4. Custom Translations at Appropriate Level ```typescript // ✅ Good - app-level for global overrides export const appConfig: ApplicationConfig = { providers: [provideReceiptTypeTranslation(customTranslations)] }; // ❌ Avoid - component-level unless truly component-specific ``` ### 5. Test Translation Coverage ```typescript // ✅ Good - test all enum values it('should translate all receipt types', () => { Object.values(ReceiptType) .filter(v => typeof v === 'number') .forEach(type => { const result = service.translate(type as ReceiptType); expect(result).toBeTruthy(); }); }); ``` ### 6. Handle Unknown Types Gracefully ```typescript // ✅ Good - fallback handling const translation = this.#service.translate(type); // Always returns a string, never null/undefined // ✅ Good - explicit handling const translation = this.#service.translate(type); if (translation === ReceiptType[type]) { console.warn('Unknown receipt type:', type); } ``` ### 7. Use Standalone Flag for Pipe ```typescript // ✅ Recommended - modern Angular pattern @Pipe({ name: 'omsReceiptTypeTranslation', standalone: true, pure: true }) export class ReceiptTypeTranslationPipe {} ``` ## License Internal ISA Frontend library - not for external distribution.