- Add new reward-order-confirmation feature library with components and store - Implement checkout completion orchestrator service for order finalization - Migrate checkout/oms/crm models to Zod schemas for better type safety - Add order creation facade and display order schemas - Update shopping cart facade with order completion flow - Add comprehensive tests for shopping cart facade - Update routing to include order confirmation page
@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
- Quick Start
- Core Concepts
- API Reference
- Usage Examples
- Receipt Types
- Custom Translations
- Testing
- Architecture Notes
Features
- 13 receipt type translations - Complete German translation coverage for all OMS receipt types
- Service-based translation -
ReceiptTypeTranslationServicefor programmatic use - Angular pipe -
omsReceiptTypeTranslationpipe 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
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
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: `
<div>
<span>{{ receiptType | omsReceiptTypeTranslation }}</span>
</div>
`
})
export class ReceiptListComponent {
receiptType = ReceiptType.ShippingNote; // Displays: "Lieferschein"
}
3. Simple Example with Multiple Types
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: `
<ul>
@for (type of receiptTypes; track type) {
<li>{{ type | omsReceiptTypeTranslation }}</li>
}
</ul>
`
})
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:
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:
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:
export const RECEIPT_TYPE_TRANSLATION = new InjectionToken<ReceiptTypeTranslation>(
'RECEIPT_TYPE_TRANSLATION',
{
factory() {
return receiptTypeTranslations; // Default translations
},
}
);
This pattern enables:
- Default behavior - Automatic injection of default translations
- Custom overrides - Provide custom translations at any level (root, module, component)
- Testability - Easy mocking in unit tests
- Type safety - TypeScript ensures translation completeness
Fallback Mechanism
The service includes intelligent fallback handling:
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:
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:
// 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 toReceiptType.NotSet)
Returns: string - German translation
Example:
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: `
<!-- Simple usage -->
<span>{{ receiptType | omsReceiptTypeTranslation }}</span>
<!-- With default fallback (NotSet) -->
<span>{{ undefined | omsReceiptTypeTranslation }}</span>
<!-- In conditional -->
@if (receipt) {
<span>{{ receipt.type | omsReceiptTypeTranslation }}</span>
}
`
})
export class ExampleComponent {
receiptType = ReceiptType.Invoice;
}
Types and Tokens
ReceiptTypeTranslation
Type representing the translation dictionary structure.
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.
export const RECEIPT_TYPE_TRANSLATION: InjectionToken<ReceiptTypeTranslation>;
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:
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
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: `
<div class="receipt-header">
<h2>{{ receiptType | omsReceiptTypeTranslation }}</h2>
<p>Receipt #{{ receiptNumber }}</p>
</div>
`
})
export class ReceiptHeaderComponent {
receiptType = ReceiptType.Invoice;
receiptNumber = '12345';
}
Service-Based Translation in Component Logic
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: `
<div>
<h3>Receipt Summary</h3>
<p>{{ summaryText }}</p>
</div>
`
})
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
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: `
<span [class]="badgeClass()">
{{ type() | omsReceiptTypeTranslation }}
</span>
`,
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<ReceiptType>();
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
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: `
<div>
<h3>Receipts</h3>
<select (change)="onFilterChange($event)">
<option value="">All Types</option>
@for (type of receiptTypes; track type.value) {
<option [value]="type.value">{{ type.value | omsReceiptTypeTranslation }}</option>
}
</select>
<table>
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Customer</th>
<th>Date</th>
</tr>
</thead>
<tbody>
@for (receipt of filteredReceipts(); track receipt.id) {
<tr>
<td>{{ receipt.id }}</td>
<td>{{ receipt.type | omsReceiptTypeTranslation }}</td>
<td>{{ receipt.customer }}</td>
<td>{{ receipt.date | date }}</td>
</tr>
}
</tbody>
</table>
</div>
`
})
export class ReceiptListComponent {
receiptTypes = [
{ value: ReceiptType.Invoice },
{ value: ReceiptType.CreditNote },
{ value: ReceiptType.ShippingNote },
{ value: ReceiptType.ReturnReceipt }
];
allReceipts = signal<ReceiptListItem[]>([
{ 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<ReceiptType | null>(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:
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: `
<div class="return-details">
<div class="receipt-info">
<label>Receipt Type:</label>
<span>{{ data().receiptType | omsReceiptTypeTranslation }}</span>
</div>
<div class="receipt-number">
<label>Receipt Number:</label>
<span>{{ data().receiptNumber }}</span>
</div>
</div>
`
})
export class ReturnDetailsDataComponent {
data = input.required<ReturnData>();
}
Programmatic Translation with Type Guards
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:
// 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
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
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: `
<span class="receipt-code">
{{ receiptType | omsReceiptTypeTranslation }}
</span>
`
})
export class CompactReceiptViewComponent {
receiptType = ReceiptType.Invoice; // Displays: "RE"
}
Testing with Custom Translations
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:
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
# 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
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
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
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
- Single Responsibility - Library focuses solely on receipt type translation
- Dependency Injection - Full Angular DI integration for flexibility
- Type Safety - TypeScript ensures translation completeness
- Fallback Safety - Never crashes on unknown types
- Extensibility - Easy to override translations at any level
- 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:
// 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:
// 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:
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
- Service Singleton -
providedIn: 'root'ensures single instance - Pure Pipe - Pipe should be marked as pure for Angular optimization:
@Pipe({ name: 'omsReceiptTypeTranslation', pure: true // Recommended addition }) - Dictionary Lookup - O(1) constant time translation lookup
- No External Calls - All translations in memory, no HTTP requests
Future Enhancements
Potential improvements identified:
- Standalone Migration - Convert pipe to standalone component
- Pure Pipe Flag - Add explicit pure flag for clarity
- Multi-Language Support - Integration with i18n system
- Translation Caching - Optional caching for repeated translations (likely unnecessary)
- Abbreviation Support - Optional short form translations (e.g., "LS" for Lieferschein)
- 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
// 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
// ✅ Good - declarative and clear
template: `<span>{{ type | omsReceiptTypeTranslation }}</span>`
// ❌ Avoid - unnecessary service injection for simple display
template: `<span>{{ getTranslation(type) }}</span>`
2. Use the Service for Business Logic
// ✅ Good - service for programmatic translation
generateReport(): string {
return this.#translationService.translate(this.receiptType);
}
// ❌ Avoid - pipe is for templates only
3. Type Safety First
// ✅ 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
// ✅ Good - app-level for global overrides
export const appConfig: ApplicationConfig = {
providers: [provideReceiptTypeTranslation(customTranslations)]
};
// ❌ Avoid - component-level unless truly component-specific
5. Test Translation Coverage
// ✅ 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
// ✅ 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
// ✅ Recommended - modern Angular pattern
@Pipe({
name: 'omsReceiptTypeTranslation',
standalone: true,
pure: true
})
export class ReceiptTypeTranslationPipe {}
License
Internal ISA Frontend library - not for external distribution.