Files
ISA-Frontend/libs/oms/utils/translation/README.md
Lorenz Hilpert 2b5da00249 feat(checkout): add reward order confirmation feature with schema migrations
- 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
2025-10-21 14:28:52 +02:00

36 KiB

@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

  • 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

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:

  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:

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 to ReceiptType.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

  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:

// 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

  1. Service Singleton - providedIn: 'root' ensures single instance
  2. Pure Pipe - Pipe should be marked as pure for Angular optimization:
    @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

// 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.