locale
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 27s

This commit is contained in:
Gal Podlipnik 2025-06-11 13:51:26 +02:00
parent 7260de8384
commit 58f6e6bde7
28 changed files with 444 additions and 15 deletions

View File

@ -0,0 +1,7 @@
# locale
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test locale` to execute the unit tests.

View File

@ -0,0 +1,34 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../eslint.base.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lib',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'lib',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@ -0,0 +1,21 @@
export default {
displayName: 'locale',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/shared/locale',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

View File

@ -0,0 +1,20 @@
{
"name": "locale",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/shared/locale/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/shared/locale/jest.config.ts"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@ -0,0 +1,4 @@
export * from './lib/common.namespace';
export * from './lib/common.t';
export * from './lib/locale-shell.component';
export * from './lib/locale.routes';

View File

@ -0,0 +1,17 @@
import { createNamespace as createCoreNamespace } from '@mmstack/translate';
const ns = createCoreNamespace('common', {
yes: 'Yes',
no: 'No',
actions: {
start: 'Start',
stop: 'Stop',
},
});
export const createNamespace = ns.createMergedNamespace;
export const createTranslation = ns.createTranslation;
export default ns.translation;
export type CommonLocale = typeof ns.translation;

View File

@ -0,0 +1,10 @@
import { createTranslation } from './common.namespace';
export default createTranslation('sl', {
yes: 'Da',
no: 'Ne',
actions: {
start: 'Začni',
stop: 'Ustavi',
},
});

View File

@ -0,0 +1,12 @@
import { registerNamespace } from '@mmstack/translate';
const r = registerNamespace(
() => import('./common.namespace').then((m) => m.default),
{
sl: () => import('./common.sl').then((m) => m.default),
}
);
export const injectCommonT = r.injectNamespaceT;
export const resolveCommonTranslations = r.resolveNamespaceTranslation;

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'frontend-locale-shell',
imports: [RouterOutlet],
template: `<router-outlet />`,
})
export class LocaleShellComponent {}

View File

@ -0,0 +1,27 @@
import { Route } from '@angular/router';
import { resolveContainersTranslations } from '@frontend/web/containers';
export const LOCALE_ROUTE: Route[] = [
{
path: 'containers',
loadComponent: () =>
import('@frontend/web/containers').then(
(m) => m.ContainersComponentShell
),
resolve: {
resolveContainersTranslations,
},
},
{
path: 'container',
loadComponent: () =>
import('@frontend/web/container').then(
(m) => m.ContainerDetailsComponent
),
},
{
path: '**',
redirectTo: 'containers',
},
];

View File

@ -0,0 +1,6 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});

View File

@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es2022",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts"
],
"include": ["src/**/*.ts"]
}

View File

@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"]
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View File

@ -10,7 +10,7 @@ import { ThemeToggleService } from '@frontend/shared/theme-toggle';
imports: [MatToolbarModule, MatIcon, MatButtonModule], imports: [MatToolbarModule, MatIcon, MatButtonModule],
template: ` template: `
<mat-toolbar class="navbar"> <mat-toolbar class="navbar">
<div (click)="goToHome()"> <div (click)="goToHome()" class="title">
<mat-icon>view_carousel</mat-icon> <mat-icon>view_carousel</mat-icon>
<span>Docker Containers</span> <span>Docker Containers</span>
</div> </div>
@ -26,6 +26,12 @@ import { ThemeToggleService } from '@frontend/shared/theme-toggle';
flex: 1 1 auto; flex: 1 1 auto;
} }
.title {
display: flex;
align-items: center;
cursor: pointer;
}
.navbar { .navbar {
background-color: var(--mat-accent-500); background-color: var(--mat-accent-500);
color: var(--mat-accent-contrast-500); color: var(--mat-accent-contrast-500);

View File

@ -1 +1,2 @@
export * from './lib/containers-shell.component'; export * from './lib/containers-shell.component';
export * from './lib/translations/containers.t';

View File

@ -2,12 +2,18 @@ import { Component, inject, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { WsService } from '@frontend/shared/ws'; import { WsService } from '@frontend/shared/ws';
import { ContainerCardComponent } from './container-card.component'; import { ContainerCardComponent } from './container-card.component';
import { ContainersTranslatePipe } from './translations/translation.pipe';
@Component({ @Component({
selector: 'frontend-containers', selector: 'frontend-containers',
standalone: true, standalone: true,
imports: [ContainerCardComponent], imports: [ContainerCardComponent, ContainersTranslatePipe],
template: ` template: `
<p>
{{
'containers.containersFound' | translate: { count: containers().length }
}}
</p>
<div class="content-container"> <div class="content-container">
@for (container of containers(); track container.id) { @for (container of containers(); track container.id) {
<frontend-container-card <frontend-container-card

View File

@ -0,0 +1,7 @@
import { createQuoteTranslation } from './containers.namespace';
export default createQuoteTranslation('sl', {
containersFound:
'{count, plural, =1 { {count} kontejner} =2 { {count} kontejnerja} other { {count} kontejnerjev}}',
});

View File

@ -0,0 +1,13 @@
import { createNamespace } from '@frontend/shared/locale';
const ns = createNamespace('containers', {
containersFound:
'{count, plural, =1 {# {count} container } other {# containers }}',
});
export default ns.translation;
export type ContainersLocale = (typeof ns)['translation'];
export const createQuoteTranslation = ns.createTranslation;

View File

@ -0,0 +1,12 @@
import { registerNamespace } from '@mmstack/translate';
const r = registerNamespace(
() => import('./containers.namespace').then((m) => m.default),
{
sl: () => import('./containers-sl.translation').then((m) => m.default),
}
);
export const injectContainersT = r.injectNamespaceT;
export const resolveContainersTranslations = r.resolveNamespaceTranslation;

View File

@ -0,0 +1,11 @@
import { Directive } from '@angular/core';
import { BaseTranslateDirective } from '@mmstack/translate';
import { type ContainersLocale } from './containers.namespace';
@Directive({
selector: '[translate]',
})
export class ContainersTranslateDirective<
TInput extends string,
> extends BaseTranslateDirective<TInput, ContainersLocale> {}

View File

@ -0,0 +1,9 @@
import { Pipe } from '@angular/core';
import { BaseTranslatePipe } from '@mmstack/translate';
import { type ContainersLocale } from './containers.namespace';
@Pipe({
name: 'translate',
})
export class ContainersTranslatePipe extends BaseTranslatePipe<ContainersLocale> {}

View File

@ -18,10 +18,12 @@
"@angular/platform-browser": "20.0.2", "@angular/platform-browser": "20.0.2",
"@angular/platform-browser-dynamic": "20.0.2", "@angular/platform-browser-dynamic": "20.0.2",
"@angular/router": "20.0.2", "@angular/router": "20.0.2",
"@formatjs/intl": "^3.1.6",
"@mmstack/form-material": "19.2.2", "@mmstack/form-material": "19.2.2",
"@mmstack/primitives": "19.2.3", "@mmstack/primitives": "19.2.3",
"@mmstack/resource": "19.2.0", "@mmstack/resource": "19.2.0",
"@mmstack/router-core": "^19.3.0", "@mmstack/router-core": "^19.3.0",
"@mmstack/translate": "^19.2.8",
"rxjs": "7.8.2", "rxjs": "7.8.2",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },
@ -5612,6 +5614,78 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.1",
"decimal.js": "^10.4.3",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/icu-skeleton-parser": "1.8.14",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.14",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.1.6.tgz",
"integrity": "sha512-tDkXnA4qpIFcDWac8CyVJq6oW8DR7W44QDUBsfXWIIJD/FYYen0QoH46W7XsVMFfPOVKkvbufjboZrrWbEfmww==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"intl-messageformat": "10.7.16",
"tslib": "^2.8.0"
},
"peerDependencies": {
"typescript": "^5.6.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -7043,6 +7117,19 @@
"rxjs": "~7.8.2" "rxjs": "~7.8.2"
} }
}, },
"node_modules/@mmstack/translate": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/@mmstack/translate/-/translate-19.2.8.tgz",
"integrity": "sha512-YsKjX6kLqSXyjKywdZKZDfTtcpvYWYTJzyfUJM66tprAcGoEI3Nsc97bh1k7lXKkbCRqgwuiEVa2gBjWjcF7/Q==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/core": "~19.2.3",
"@formatjs/intl": "~3.1.6"
}
},
"node_modules/@modern-js/node-bundle-require": { "node_modules/@modern-js/node-bundle-require": {
"version": "2.67.6", "version": "2.67.6",
"resolved": "https://registry.npmjs.org/@modern-js/node-bundle-require/-/node-bundle-require-2.67.6.tgz", "resolved": "https://registry.npmjs.org/@modern-js/node-bundle-require/-/node-bundle-require-2.67.6.tgz",
@ -14215,7 +14302,6 @@
"version": "10.5.0", "version": "10.5.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/dedent": { "node_modules/dedent": {
@ -17117,6 +17203,18 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/intl-messageformat": {
"version": "10.7.16",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.4",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.2",
"tslib": "^2.8.0"
}
},
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",

View File

@ -18,10 +18,12 @@
"@angular/platform-browser": "20.0.2", "@angular/platform-browser": "20.0.2",
"@angular/platform-browser-dynamic": "20.0.2", "@angular/platform-browser-dynamic": "20.0.2",
"@angular/router": "20.0.2", "@angular/router": "20.0.2",
"@formatjs/intl": "^3.1.6",
"@mmstack/form-material": "19.2.2", "@mmstack/form-material": "19.2.2",
"@mmstack/primitives": "19.2.3", "@mmstack/primitives": "19.2.3",
"@mmstack/resource": "19.2.0", "@mmstack/resource": "19.2.0",
"@mmstack/router-core": "^19.3.0", "@mmstack/router-core": "^19.3.0",
"@mmstack/translate": "^19.2.8",
"rxjs": "7.8.2", "rxjs": "7.8.2",
"zone.js": "0.15.1" "zone.js": "0.15.1"
}, },

0
frontend/public/env.js Normal file
View File

View File

@ -3,6 +3,7 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withPreloading } from '@angular/router'; import { provideRouter, withPreloading } from '@angular/router';
import { ENVIRONMENT } from '@frontend/shared/environment'; import { ENVIRONMENT } from '@frontend/shared/environment';
import { PreloadStrategy } from '@mmstack/router-core'; import { PreloadStrategy } from '@mmstack/router-core';
import { provideIntlConfig } from '@mmstack/translate';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { appRoutes } from './app.routes'; import { appRoutes } from './app.routes';
@ -11,6 +12,9 @@ export const appConfig: ApplicationConfig = {
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(), provideHttpClient(),
provideRouter(appRoutes, withPreloading(PreloadStrategy)), provideRouter(appRoutes, withPreloading(PreloadStrategy)),
provideIntlConfig({
defaultLocale: 'en',
}),
{ provide: ENVIRONMENT, useValue: environment }, { provide: ENVIRONMENT, useValue: environment },
], ],
}; };

View File

@ -1,22 +1,52 @@
import { Route } from '@angular/router'; import { ActivatedRouteSnapshot, Route } from '@angular/router';
import { inject, Injectable, LOCALE_ID } from '@angular/core';
import {
LocaleShellComponent,
resolveCommonTranslations,
} from '@frontend/shared/locale';
@Injectable({
providedIn: 'root',
})
export class LocaleStore {
locale = 'en';
}
export const appRoutes: Route[] = [ export const appRoutes: Route[] = [
{ {
path: 'containers', path: ':locale',
loadComponent: () => component: LocaleShellComponent,
import('@frontend/web/containers').then( resolve: {
(m) => m.ContainersComponentShell localeId: (route: ActivatedRouteSnapshot) => {
), const store = inject(LocaleStore);
const locale = route.params['locale'] || 'en';
store.locale = locale;
return locale;
},
common: resolveCommonTranslations,
},
providers: [
{
provide: LOCALE_ID,
useFactory: (store: LocaleStore) => {
return store.locale;
},
deps: [LocaleStore],
},
],
loadChildren: () =>
import('@frontend/shared/locale').then((m) => m.LOCALE_ROUTE),
}, },
{ {
path: 'container', path: '',
loadComponent: () => redirectTo: 'en',
import('@frontend/web/container').then( pathMatch: 'full',
(m) => m.ContainerDetailsComponent
),
}, },
{ {
path: '**', path: '**',
redirectTo: 'containers', redirectTo: 'en',
}, },
]; ];

View File

@ -16,6 +16,7 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@frontend/shared/environment": ["libs/shared/environment/src/index.ts"], "@frontend/shared/environment": ["libs/shared/environment/src/index.ts"],
"@frontend/shared/locale": ["libs/shared/locale/src/index.ts"],
"@frontend/shared/navbar": ["libs/shared/navbar/src/index.ts"], "@frontend/shared/navbar": ["libs/shared/navbar/src/index.ts"],
"@frontend/shared/theme-toggle": [ "@frontend/shared/theme-toggle": [
"libs/shared/theme-toggle/src/index.ts" "libs/shared/theme-toggle/src/index.ts"