Skip to content

Internationalization (i18n)

Microtec ERP supports Arabic (RTL) and English (LTR) using @ngx-translate. All translation files live in shared-assets and are distributed to every app at build time. This page covers the translation file structure, how RTL/LTR switching works, CSS conventions for bidirectional layouts, and how to add new keys.


Overview

ConcernSolution
Translation library@ngx-translate/core + @ngx-translate/http-loader
Translation filesJSON, stored in libs/shared-assets/src/assets/langs/
Supported localesar (Arabic, RTL) and en (English, LTR)
Direction switchingdocument.documentElement.dir and lang attributes, set at runtime
CSS direction strategyLogical CSS properties (margin-inline-start instead of margin-left)
Active locale storagelocalStorage key: microtec_lang

Translation File Structure

libs/shared-assets/src/assets/langs/
├── shared/
│   ├── ar.json        # Shared Arabic keys (used by all apps)
│   └── en.json        # Shared English keys (used by all apps)
├── accounting/
│   ├── ar.json        # Accounting module Arabic keys
│   └── en.json        # Accounting module English keys
├── hr/
│   ├── ar.json
│   └── en.json
├── finance/
│   ├── ar.json
│   └── en.json
├── sales/
│   ├── ar.json
│   └── en.json
├── purchase/
│   ├── ar.json
│   └── en.json
├── inventory/
│   ├── ar.json
│   └── en.json
├── distribution/
│   ├── ar.json
│   └── en.json
├── fixed-assets/
│   ├── ar.json
│   └── en.json
└── bo/
    ├── ar.json
    └── en.json

Each app loads its own module translations plus the shared translations at startup. The TranslationService in shared-lib merges both sources.


Key Naming Convention

Translation keys use module_scope_keyname format — all lowercase with underscores.

json
// libs/shared-assets/src/assets/langs/shared/en.json
{
  "shared_save": "Save",
  "shared_cancel": "Cancel",
  "shared_delete": "Delete",
  "shared_confirm_delete": "Are you sure you want to delete this item?",
  "shared_loading": "Loading...",
  "shared_no_data": "No data available",
  "shared_success": "Operation completed successfully",
  "shared_error": "An error occurred. Please try again."
}
json
// libs/shared-assets/src/assets/langs/shared/ar.json
{
  "shared_save": "حفظ",
  "shared_cancel": "إلغاء",
  "shared_delete": "حذف",
  "shared_confirm_delete": "هل أنت متأكد من حذف هذا العنصر؟",
  "shared_loading": "جاري التحميل...",
  "shared_no_data": "لا توجد بيانات",
  "shared_success": "تمت العملية بنجاح",
  "shared_error": "حدث خطأ. يرجى المحاولة مرة أخرى."
}
json
// libs/shared-assets/src/assets/langs/accounting/en.json
{
  "accounting_invoice_list_title": "Invoices",
  "accounting_invoice_add": "Add Invoice",
  "accounting_invoice_post": "Post Invoice",
  "accounting_invoice_status_draft": "Draft",
  "accounting_invoice_status_posted": "Posted",
  "accounting_journal_entry_title": "Journal Entries"
}

Prefix rules:

PrefixUsed For
shared_Keys used by more than one module
accounting_Accounting module only
hr_HR module only
finance_Finance module only
sales_Sales module only
purchase_Purchase module only
inventory_Inventory module only
distribution_Distribution module only
bo_Business Owner portal only

@ngx-translate Setup

Module registration

The shell (erp-home) loads translations as the root loader. Remote apps use forChild with isolate: false to share the translation instance:

typescript
// apps/erp-home/src/app/app.module.ts (host — forRoot)
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';

export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
  return new TranslateHttpLoader(http, './assets/langs/', '.json');
}

@NgModule({
  imports: [
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient],
      },
      defaultLanguage: 'ar',
    }),
  ],
})
export class AppModule {}
typescript
// apps/apps-accounting/src/app/accounting.module.ts (remote — forChild)
import { TranslateModule } from '@ngx-translate/core';

@NgModule({
  imports: [
    TranslateModule.forChild({
      isolate: false, // shares translations with the host
    }),
  ],
})
export class AccountingModule {}

Using translations in templates

html
<!-- Pipe syntax (recommended) -->
<h1>{{ 'accounting_invoice_list_title' | translate }}</h1>

<!-- Directive syntax (for attributes) -->
<button [attr.aria-label]="'shared_delete' | translate">
  {{ 'shared_delete' | translate }}
</button>

<!-- With parameters -->
<p>{{ 'shared_items_count' | translate: { count: invoices.length } }}</p>

Using translations in TypeScript

typescript
import { TranslateService } from '@ngx-translate/core';

@Injectable({ providedIn: 'root' })
export class InvoiceService {
  constructor(private translate: TranslateService) {}

  getSuccessMessage(): string {
    return this.translate.instant('shared_success');
  }
}

Language Switching

Language switching is centralised in the TranslationService from shared-lib:

typescript
// libs/shared-lib/src/lib/services/translation.service.ts
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';

@Injectable({ providedIn: 'root' })
export class TranslationService {
  private readonly STORAGE_KEY = 'microtec_lang';

  constructor(private translate: TranslateService) {}

  initLanguage(): void {
    const saved = localStorage.getItem(this.STORAGE_KEY) ?? 'ar';
    this.setLanguage(saved);
  }

  setLanguage(lang: 'ar' | 'en'): void {
    this.translate.use(lang);
    localStorage.setItem(this.STORAGE_KEY, lang);
    document.documentElement.lang = lang;
    document.documentElement.dir = lang === 'ar' ? 'rtl' : 'ltr';
  }

  get currentLang(): string {
    return this.translate.currentLang;
  }

  get isRtl(): boolean {
    return this.translate.currentLang === 'ar';
  }
}

The shell calls translationService.initLanguage() in AppComponent.ngOnInit(). All subsequent setLanguage() calls propagate across remotes because the TranslateService instance is shared (isolate: false).


RTL/LTR Switching

When the language changes, two attributes on the <html> element control direction:

  • dir="rtl" or dir="ltr"
  • lang="ar" or lang="en"

PrimeNG respects the dir attribute automatically for all its components (dropdowns, dialogs, menus align correctly in RTL).


CSS Logical Properties

All SCSS in shared-lib and in remote apps uses CSS logical properties instead of physical directional properties. This ensures RTL layouts work without writing separate RTL style overrides.

Physical propertyLogical equivalentMeaning
margin-leftmargin-inline-startStart of the inline axis
margin-rightmargin-inline-endEnd of the inline axis
padding-leftpadding-inline-start
padding-rightpadding-inline-end
leftinset-inline-startPosition from inline start
rightinset-inline-endPosition from inline end
text-align: lefttext-align: start
text-align: righttext-align: end
border-leftborder-inline-start
border-rightborder-inline-end
scss
// WRONG — breaks RTL
.sidebar {
  margin-left: 240px;
  padding-right: 16px;
}

// CORRECT — works in both LTR and RTL
.sidebar {
  margin-inline-start: 240px;
  padding-inline-end: 16px;
}

Never use physical directional properties in shared-lib

Any SCSS added to shared-lib must use logical properties. Physical properties (left, right, margin-left) are permitted only for elements that are explicitly direction-independent (e.g., absolute positioned decorative elements).


How to Add New Translation Keys

Adding a shared key (used by multiple modules)

  1. Add the English key to libs/shared-assets/src/assets/langs/shared/en.json.
  2. Add the Arabic equivalent to libs/shared-assets/src/assets/langs/shared/ar.json.
  3. Use the shared_ prefix.
  4. Run npm run publishAssets to copy files to all dist folders (or rely on the build pipeline).

Adding a module-specific key

  1. Identify the module's lang folder (e.g., libs/shared-assets/src/assets/langs/accounting/).
  2. Add the key to both en.json and ar.json with the module prefix (accounting_).
  3. The key is available immediately — no code registration required.

Adding a translation key with parameters

json
// en.json
{
  "shared_items_found": "Found {{count}} items",
  "shared_welcome_user": "Welcome, {{name}}"
}
html
<!-- Template usage -->
<p>{{ 'shared_items_found' | translate: { count: total } }}</p>
<p>{{ 'shared_welcome_user' | translate: { name: username } }}</p>

Translation File Loading per App

Each app has its own TranslateHttpLoader that loads from its own assets/langs/ directory. The path ./assets/langs/ is relative to the deployed app's base URL. The publishAssets script ensures that both shared and module-specific files land in the correct assets/langs/ directory for each app.

Missing translation key behavior

If a translation key is missing, @ngx-translate displays the raw key string as a fallback. Always add both en.json and ar.json entries at the same time to avoid showing raw keys in production.

Internal Documentation — Microtec Platform Team