[Angular] Dynamic App Config and Translations in a Submodule
When I'm setting up a new angular application, I always start with translation support and an external app config json loader. You should never start a new app without an internationalization (i18n) system - even if you only support one language! We are going to use the package @ngx-translate/core
.
The app config isn't essential but very practical if you want to change some values without rebuilding and deploying your app. Very inspired by this MS DevBlog!
After multiple app creations and copying the same files over and over again I put the main logic inside a submodule utils folder. Now I can import the submodule in every new project I start and use it like an npm package. If you want to learn more about submodules:
I will reference to this source code:
To have a better overview which files we will create and how it should look like in the end:

App Config
Setting File Setup
First we have to define where the config file lives - normally i put it under assets/customization
.

Create a new json file and add some content - mine looks like the following:
{
"api": {
"url": "/api"
},
"language": ["en-US"],
"title": "ngmug",
"cookiePrefix": "ngmug",
"logoUrl": "./assets/images/logo/ngmug.png"
}
Now we have to define a constant (or use the environment file) to set the settings file's path. I created this object in the app.module.ts
:
export const environment = {
settingsFile: `assets/customization/settings.json?version=${Date.now()}`,
};
Middleware Service
Create a new service for your app ng g s app-config
. This should be the middleware service between the app and the generic submodule:
@Injectable({
providedIn: 'root',
})
export class AppConfigService extends GhConfigService<AppConfig> {
static settings: AppConfig;
defaultValue: AppConfig = {
api: {
url: '',
},
language: ['de-DE', 'en-US'],
title: 'Title',
logoUrl: 'TBD',
cookiePrefix: '',
};
constructor(
platformLocation: PlatformLocation,
titleService: Title,
httpback: HttpBackend
) {
super(new HttpClient(httpback), platformLocation, titleService);
}
override async load(configPath: string): Promise<AppConfig> {
AppConfigService.settings = await super.load(configPath, this.defaultValue);
return AppConfigService.settings;
}
}
Beside the service, create a new file with the app specific config interface:
export interface AppConfig extends GhAppConfig {
cookiePrefix: string;
logoUrl: string;
}
Submodule Service
Let's create the generic service inside your submodule library:
export class GhConfigService<TAppConfig extends GhAppConfig> {
constructor(
protected _http: HttpClient,
protected _platformLocation: PlatformLocation,
protected _titleService: Title
) {}
load(configPath: string, defaultValues: any): Promise<TAppConfig> {
return new Promise<TAppConfig>((resolve, reject) => {
lastValueFrom(this._http.get(configPath))
.then((response: any) => {
var settings = response as TAppConfig;
this.setDefaultValues(settings, defaultValues);
//inject default values if not set
settings.api.url = this.replaceFQDN(settings.api.url);
this._titleService.setTitle(settings.title);
resolve(settings as TAppConfig);
})
.catch((response: any) => {
console.log('cfg error');
reject(
`Could not load configuration file '${configPath}': ${JSON.stringify(
response
)}`
);
});
});
}
private setDefaultValues(target: any, source: any) {
for (const key of Reflect.ownKeys(source)) {
const sourceChild = source[key];
const sourceChildType = typeof sourceChild;
const isArray = Array.isArray(sourceChild);
if (!this.isDefined(target[key], sourceChildType, isArray)) {
//property missing: set
target[key] = sourceChild;
continue;
}
if (sourceChildType === 'object') {
//traverse child object
if (!isArray) {
this.setDefaultValues(target[key], sourceChild);
}
} else if (sourceChildType === 'string') {
//overwrite if only empty string is set
if ((target[key] as string).length <= 0) {
target[key] = sourceChild;
}
}
}
}
private isDefined(
value: any,
expectedType: string,
isArray: boolean
): boolean {
if (value === null || value === undefined) {
//missing -> not defined
return false;
}
if (expectedType !== typeof value) {
//wrong type -> not defined
return false;
}
if (isArray !== Array.isArray(value)) {
//array missmatch -> not defined
return false;
}
return true;
}
replaceFQDN(url: string): string {
return url.replace('$host', this._platformLocation.hostname);
}
}
And add a file to define the default config interface:
export interface GhAppConfig {
title: string;
language: string[];
api: {
url: string;
};
}
The service just makes an http call to the defined assets file and loads the content. If everything works as expected it will get set the result to a static property to be available from the entire application!
Put everything together
The last thing we have to do is actually calling it - switch back to your app.module.ts
and add the following provider and the app init factory!
(if you don't need the translation part, remove it!)
providers: [
{
provide: APP_INITIALIZER,
useFactory: AppInit,
deps: [AppConfigService, TranslationManagementService],
multi: true,
},
],
export function AppInit(
configService: AppConfigService,
transService: TranslationManagementService
) {
return async () => {
await configService.load(environment.settingsFile);
transService.init();
};
}
You can use your app config properties like this:
AppConfigService.settings.api.url;
Translation
Language Files Setup
Add to properties your environment file or object which we created in the app config section:
export const environment = {
settingsFile: `assets/customization/settings.json?version=${Date.now()}`,
languagesFolder: 'assets/languages/',
languagesSuffix: `.json?version=${Date.now()}`,
};
Don't forget to change the path to your translation files! Mine look like the following:

Initialize Packages
I'm using ngx-translate for my i18n - it's a great!
npm i @ngx-translate/core @ngx-translate/http-loader
Add this to your imports to load your translation files per http:
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
}),
export function HttpLoaderFactory(httpClient: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(
httpClient,
environment.languagesFolder,
environment.languagesSuffix
);
}
Middleware Service
Same story as above - add a service to your shared folder which is specific for this app and add this to it:
(the middleware isn't necessary - but maybe a needed abstraction)
@Injectable({
providedIn: 'root',
})
export class TranslationManagementService extends GhTranslationService {
constructor(ngxTranslateService: TranslateService) {
super(ngxTranslateService);
}
override init(): void {
super.init(
AppConfigService.settings.language,
AppConfigService.settings.cookiePrefix,
getCookie,
setCookie
);
}
}
The getCookie
and setCookie
are just util functions - you will find them here if you need them:
Submodule Service
My translation service adds some automatic detecting logic - you can delete it if you don't need it. The important part is the init function!
registerLocaleData(localEn, 'en');
@Injectable({
providedIn: 'root',
})
export class GhTranslationService {
defaultLanguage: string = '';
supportedLanguages: string[] = [];
onLangChange = this.ngxTranslateService.onLangChange;
cookiePrefix: string = '';
getCookie!: Function;
setCookie!: Function;
constructor(private ngxTranslateService: TranslateService) {}
get languageCookie(): string {
return `${this.cookiePrefix}appLanguage`;
}
set language(desiredLanguage: string) {
desiredLanguage =
desiredLanguage ||
(this.ngxTranslateService.getBrowserCultureLang() ?? '');
let usedLanguage = GhTranslationService.findBestMatchingLanguage(
desiredLanguage,
this.supportedLanguages
);
if (!usedLanguage) {
usedLanguage = this.defaultLanguage;
}
this.setCookie(this.languageCookie, usedLanguage, 360);
this.ngxTranslateService.use(usedLanguage);
}
get language(): string {
return this.ngxTranslateService.currentLang;
}
static findBestMatchingLanguage(
language: string,
supportedLanguages: string[]
) {
if (!language) {
language = '';
}
language = language.toLowerCase();
const shortLanguage = language.split('-')[0];
//try to find exact match
let result = supportedLanguages.find((sl) => sl.toLowerCase() === language);
//try to remove "-" of target language (en-US -> en)
if (!result) {
result = supportedLanguages.find(
(sl) => sl.toLowerCase() === shortLanguage
);
}
//try to remove "-" of supported languages
if (!result) {
result = supportedLanguages.find(
(sl) => sl.toLowerCase().split('-')[0] === shortLanguage
);
}
return result;
}
init(
appLanguages: string[],
cookiePrefix: string,
getCookie: Function,
setCookie: Function
) {
if (appLanguages.length < 1)
throw new Error('Init Languages Error: No Language found in settings');
this.supportedLanguages = appLanguages;
this.cookiePrefix = cookiePrefix;
this.getCookie = getCookie;
this.setCookie = setCookie;
const defaultLanguage = appLanguages[0];
this.ngxTranslateService.setDefaultLang(defaultLanguage);
this.defaultLanguage = defaultLanguage;
const persistedLang = this.getCookie(this.languageCookie);
this.language = persistedLang;
}
instant(key: string): string {
return this.ngxTranslateService.instant(key);
}
}
Put everything together
Now the translation should run - for example:
<input [placeholder]="'general.test' | translate" />
Remember to import TranslateModule
from @ngx-translate/core
when you need translations in another module!
If you have further questions about this topic or need help with a problem in general, please write a comment or simply contact me at yesreply@georghoeller.dev :)
Georg Hoeller Newsletter
Join the newsletter to receive the latest updates in your inbox.