Awesome
L10N
This demo project shows how to localize your web application with just a few clicks using reactive state management in combination with ResXResourceManager.
This sample code is based on Angular and ngrx, but it should work fine with any other UI framework and state management.
Watch the demo video
The Concept
Neutral (usually en-US) resources are provided not as .json files but as one or more typescript classes, mapping the keys to their default text:
export class Resources {
MAINPAGE_WELCOME = "Welcome";
MAINPAGE_APPNAME = "Localized App";
}
resources = new Resources();
This file is available at compile time, so you can benefit from static type checking and code completion.
const text = resources.MAINPAGE_WELCOME;
Trying to access non-existing resources will result in compile errors.
All other languages are provides as .json files, so at runtime the members of the Resources
class can be easily overwritten dynamically with their localized content:
{
"Resources": {
"MAINPAGE_APPNAME": "Lokalisierte Anwendung",
"MAINPAGE_WELCOME": "Willkommen"
}
}
const url = './assets/resources.' + culture + '.json';
const resources$ = this.httpClient.get(url).pipe(map(data => Object.assign(new Resources(), data.Resources)));
While maintaining the translations in ResXResourceManager, the typescript and json files will be automatically generated or updated.
String Placeholders
Replacing placeholders with values at runtime is also supported with static type support.
Consider a resource "MAINPAGE_WELCOME"
with the value "Welcome $(User)!"
:
The TypeScript generated by ResXResourceManager will look like this:
export class Resources {
private MAINPAGE_WELCOME_TEMPLATE = "Welcome ${User}!";
MAINPAGE_WELCOME = (args: { User: string }) => {
return formatString(this.MAINPAGE_WELCOME_TEMPLATE, args);
}
}
so the usage in your code will look like:
const text = resources.MAINPAGE_WELCOME({ User: 'tom'})
Again code completion will support you to write correct code that will not compile if you provide insufficient or wrong formatting parameters.
Installing ResXResourceManager
You will need at latest version 1.40 of ResXResourceManager. If you don't have installed it yet, get the latest version as described here.
The Visual Studio extension is recommended, but if you don't have VisualStudio, you can use the standalone version as well. Just note that the standalone version does not support the "Move To Resource" feature.
Setting up your application
If you start from the scratch, generate a new application and setup the ngrx store.
1. Integration of ResXResourceManager
To enable integration with ResXResourceManager you will need to add two files to your project:
- the
resx-manager.webexport.config
file in yourscr/
root folder (next toindex.html
). This file configures the locations of the files generated by ResXResourceManager. - an empty resource file as a starting point. A good place is e.g. in the
l10n/
sub-folder.
You can just copy these two files from here
Now start Visual Studio or standalone ResXResourceManager and open your src/
folder.
This will trigger the initial creation of the resources.ts
file in the app/
folder next to your app.module.ts
2. Setting up state, actions, epics and reducers:
State
Create a new state for localization and add it to your global state.
The Resources
object is the one created by ResXResourceManager in step #1,
containing the neutral resources with the default values.
import {Resources} from '../resources';
export interface L10nState {
resources: Resources;
}
const initialL10NState: L10nState = {
resources: new Resources()
};
export interface State {
l10n: L10nState;
}
Actions
You will need three actions to load culture specific resources, the typical load/loadSucceeded pair plus one optional to reset the resources to their invariant defaults.
export const loadL10N = createAction(
'[L10N] Load L10Ns',
props<{ culture: string }>()
);
export const loadL10NSuccess = createAction(
'[L10N] Load L10Ns Success',
props<{ data: any }>()
);
export const resetL10N = createAction(
'[L10N] Reset L10Ns',
);
Effects
You will need two effects, one to load culture specific resources upon request, and one optional to reset the resources to the neutral defaults.
loadL10N$ = createEffect(() =>
this.actions$.pipe(
ofType(loadL10N),
map(({culture}) => culture),
filter(culture => culture && culture !== 'en'),
map(culture => './assets/resources.' + culture + '.json'),
switchMap(url => this.httpClient.get(url).pipe(
map(data => loadL10NSuccess({data})),
catchError((err, caught) => {
console.log('resource not found:', err);
return EMPTY;
})
)),
));
resetL10N$ = createEffect(() =>
this.actions$.pipe(
ofType(loadL10N),
map(({culture}) => culture),
filter(culture => !culture || culture === 'en'),
map(data => resetL10N())
));
Reducers
Finally we need the reducers to update the state with the loaded resources:
export const l10nReducer = createReducer(
initialL10NState,
on(loadL10NSuccess, (state, {data}) => {
const resources = Object.assign(new Resources(), data.Resources);
return {...state, resources };
}),
on(resetL10N, (state, {}) => {
return {...state, resources: new Resources()};
})
);
export const reducers: ActionReducerMap<State> = {
l10n: l10nReducer
};
export const selectL10n = (state: State) => state.l10n;
Start localizing
Now that all is set up, you can start localizing your app. If you use the Visual Studio extension of ResXManager, you can use the 'Move to Resource' feature as shown in the demo video. When you use the standalone version, you have to manually create your resource entries in ResXManager, but you still have intellisense, so can't type wrong when using the resources in your .html or .ts code.