Appearance
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
| Concern | Solution |
|---|---|
| Translation library | @ngx-translate/core + @ngx-translate/http-loader |
| Translation files | JSON, stored in libs/shared-assets/src/assets/langs/ |
| Supported locales | ar (Arabic, RTL) and en (English, LTR) |
| Direction switching | document.documentElement.dir and lang attributes, set at runtime |
| CSS direction strategy | Logical CSS properties (margin-inline-start instead of margin-left) |
| Active locale storage | localStorage 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.jsonEach 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:
| Prefix | Used 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"ordir="ltr"lang="ar"orlang="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 property | Logical equivalent | Meaning |
|---|---|---|
margin-left | margin-inline-start | Start of the inline axis |
margin-right | margin-inline-end | End of the inline axis |
padding-left | padding-inline-start | — |
padding-right | padding-inline-end | — |
left | inset-inline-start | Position from inline start |
right | inset-inline-end | Position from inline end |
text-align: left | text-align: start | — |
text-align: right | text-align: end | — |
border-left | border-inline-start | — |
border-right | border-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)
- Add the English key to
libs/shared-assets/src/assets/langs/shared/en.json. - Add the Arabic equivalent to
libs/shared-assets/src/assets/langs/shared/ar.json. - Use the
shared_prefix. - Run
npm run publishAssetsto copy files to all dist folders (or rely on the build pipeline).
Adding a module-specific key
- Identify the module's lang folder (e.g.,
libs/shared-assets/src/assets/langs/accounting/). - Add the key to both
en.jsonandar.jsonwith the module prefix (accounting_). - 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.