Internationalization

Every user-facing string goes through @ngx-translate. English is currently the only language, but the mechanism is fully in place.

Loader

The app uses a local loader rather than fetching JSON at runtime:

// src/app/translate-loader.ts
const languages = {
    en: {
        load: () => fromPromise(import('../assets/i18n/en')).pipe(map(res => res.default)),
    },
};

export class LocalTranslateLoader implements TranslateLoader {
    getTranslation(lang: string): Observable<any> {
        return languages[lang].load();
    }
}

Translations are a TypeScript module, dynamically imported and bundled by webpack — not a static JSON asset.

HttpLoaderFactory (a TranslateHttpLoader against ./assets/i18n/*.json) is also exported from the same file but is not the loader AppModule uses. Adding a language means adding an entry to the languages map, not dropping a JSON file into assets/.

The translation file

src/assets/i18n/en.ts — around 3,800 lines, exporting a nested object as the default export.

Because it is TypeScript, it can import:

import { CrossSellProductEnum } from '@payability/pyb-ng2-api';
import { MIN_INSTANT_TRANSFER_AMOUNT } from '.../transfer-funds.const';
import { APP_CONFIG } from '../../app/core/_config/app-config/app-config';
import { descriptionLinkClassName } from '.../transfer-error-modal.const';

const i18n = {
    AUTH: {
        CHANGE: {
            CURRENT: 'Current password',
            FORGOT: 'Forgot password?',
            // …
        },
    },
};

Enum members become computed keys, and constants can be interpolated directly, so a value such as a minimum transfer amount is defined once and reused in copy.

The translation file imports from src/app. A circular import — an app file importing en.ts — will break the build. Keep the dependency direction one-way.

Initialization

Loading is an APP_INITIALIZER that blocks bootstrap:

const translateFactory = (translate: TranslateService) => {
    return async () => {
        translate.setDefaultLang(DEFAULT_LANGUAGE);
        translate.use(DEFAULT_LANGUAGE);
        return new Promise(resolve => translate.onLangChange.subscribe(change => resolve(change)));
    };
};

Nothing renders until translations are ready, which is why raw keys are never briefly visible.

DEFAULT_LANGUAGE is 'en', defined in core/_base/layout/services/translation.service.ts. TranslationService registers the available languages and sets the fallback.

Using translations

In templates:

<span>{{ 'AUTH.CHANGE.HEADER' | translate }}</span>
<span>{{ 'BANNERS.CANCELLATION.PENDING.LAST_DAY' | translate: { marketplaceName, optOutDate } }}</span>

In code:

this.translateService.instant('MODALS.INSTANT_ADVANCE.SUCCESS.TITLE');
this.translateService.instant('FEATURES.ADVANCE_LINE.MODALS.SUCCESS.HEADER', { amount });
this.translateService.get('CTA.AIRWALLEX_PCARD_TERMS.SUCCESS').subscribe(({ HEADER, DESCRIPTION }) => { /* … */ });

get() on a branch key returns the whole sub-object. The notification success modal relies on this, destructuring { HEADER, DESCRIPTION } from one lookup.

Key naming

AUTH.*

Authentication screens.

SETTINGS.MANAGE_ACCOUNT.*

Account settings, mirroring the tab structure.

MODALS.*

Dialog copy — MODALS.AIRWALLEX_PCARD_TERMS, MODALS.AP_AMAZON.SUCCESS, MODALS.LEGAL_INFO_CONFIRM.AGREEMENT_SIGNED.

BANNERS.*

Banner copy — BANNERS.CANCELLATION.PENDING.LAST_DAY.

CTA.*

Notification CTA copy — CTA.PRE_ADVANCE_LINE.SUCCESS.

FEATURES.*

Feature-specific copy — FEATURES.ADVANCE_LINE.MODALS.SUCCESS.*.

SUSPENSION_REASON.*

Suspension copy, keyed by the stripped reason code.

PRODUCT_OFFERINGS.PRODUCTS.*

Product card copy, keyed by CrossSellProductEnum.

SELLER_CARD., CARDS_PANEL.

Card status and provider labels.

TOP_BAR_STATUS.STEPS.*

Onboarding step titles.

MENU.*

Navigation labels.

COMMON.*

Shared words — COMMON.CLOSE, COMMON.NEXT, COMMON.TRY_AGAIN, COMMON.ERRORS.SOMETHING_WENT_WRONG.

INSTANT_TRANSFER.*

Instant Transfer copy, including the failure modals.

PAYSHIELD_SUBSCRIPTION.*

PayShield plan names and descriptions.

Computed keys

Several places build keys at runtime. Grep will not find them, so they need care.

Suspension

${copyNamespace}.${reasonCode} and ${copyNamespace}.${reasonCode}.${marketplace}, with .first / .next suffixes for failed payments.

Product offerings

freeAccessProduct(type) / commonProduct(type) prefixes, four keys each.

Onboarding steps

TOP_BAR_STATUS.STEPS.${stepKey}.

Card status

SELLER_CARD.ACTIVE / .BLOCKED / .PENDING_ACTIVATION / .TERMINATED via unifiedStatusLabelMap.

CTA success

phraseKeyByType[type] with a fallback to CTA.PRE_INSTANT_ADVANCE.SUCCESS.

A missing computed key renders as the raw key string, and only for the cohort that hits that branch. Adding a suspension reason, a product, or an accept-able notification means adding its key and exercising that branch.

Extraction

$ npm run extract-translations

runs:

ngx-translate-extract --input ./src --output ./src/assets/i18n/*.json --sort --format namespaced-json

The extractor writes .json, but the loader reads en.ts. Extraction is therefore an *audit tool for finding keys used in templates, not a generator for the file the app actually loads. Add keys to en.ts by hand.

make i18n is the Makefile equivalent.

HTML in copy

Keys ending _HTML (for example INSTANT_TRANSFER.FAILURE_ADDRESS_MODAL.DESCRIPTION_HTML) contain markup and are rendered through the html-render component or the safe pipe. HyperlinkService can also inject links into plain text at runtime, using the class from HYPER_LINK_CONFIG_TOKEN.

Known exceptions

Some strings are hardcoded English rather than translated:

  • SendMoneyPageService — the success toast and confirmation modal text.

  • ExportService — the default 'Export' and 'Your report is preparing' parameter values.

  • HttpErrorHandlerInterceptor — the account-locked message.

These should move into en.ts when touched.

Adding a language

  1. Add src/assets/i18n/<lang>.ts with the same key structure.

  2. Add an entry to the languages map in src/app/translate-loader.ts.

  3. Register the language with TranslationService.

  4. Add a language selector and persist the choice.

  5. Review date and currency formatting — DateFormatsEnum is US-centric (MM/dd/yyyy) and CurrencyPipe is used without an explicit locale.