Skip to content

Commit

Permalink
chore: localization tests + tiny format refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
YuukanOO committed Nov 27, 2023
1 parent bb3f619 commit 878e2a6
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 50 deletions.
8 changes: 4 additions & 4 deletions cmd/serve/front/src/components/deployment-card.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@

<div class="grid">
<Display label="deployment.started_at">
{data.state.started_at ? l.format('datetime', data.state.started_at) : '-'}
{data.state.started_at ? l.datetime(data.state.started_at) : '-'}
</Display>
<Display label="deployment.finished_at">
{data.state.finished_at ? l.format('datetime', data.state.finished_at) : '-'}
{data.state.finished_at ? l.datetime(data.state.finished_at) : '-'}
</Display>
<Display label="deployment.queued_at">
{l.format('datetime', data.requested_at)}
{l.datetime(data.requested_at)}
</Display>
<Display label="deployment.duration">
{data.state.started_at
? l.format('duration', data.state.started_at, data.state.finished_at ?? new Date())
? l.duration(data.state.started_at, data.state.finished_at ?? new Date())
: '-'}
</Display>

Expand Down
13 changes: 5 additions & 8 deletions cmd/serve/front/src/components/deployments-list.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,22 @@
function metadataFromStatus(depl: DeploymentData): string {
switch (depl.state.status) {
case DeploymentStatus.Succeeded:
return `${l.format('datetime', depl.state.finished_at!)} (${l.format(
'duration',
return `${l.datetime(depl.state.finished_at!)} (${l.duration(
depl.state.started_at!,
depl.state.finished_at!
)})`;
case DeploymentStatus.Failed:
return `${depl.state.error_code} (${l.format(
'duration',
return `${depl.state.error_code} (${l.duration(
depl.state.started_at!,
depl.state.finished_at!
)})`;
case DeploymentStatus.Running:
return `${l.format('datetime', depl.state.started_at!)} (${l.format(
'duration',
return `${l.datetime(depl.state.started_at!)} (${l.duration(
depl.state.started_at!,
new Date()
)})`;
default:
return `${l.format('datetime', depl.requested_at)}`;
return l.datetime(depl.requested_at);
}
}
</script>
Expand All @@ -50,7 +47,7 @@
/>
<div>
<div class="title">
{variant === 'env' ? depl.environment : l.format('datetime', depl.requested_at)}
{variant === 'env' ? depl.environment : l.datetime(depl.requested_at)}
</div>
<div class="metadata">
{metadataFromStatus(depl)}
Expand Down
3 changes: 1 addition & 2 deletions cmd/serve/front/src/lib/localization/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ This action is IRREVERSIBLE and will DELETE ALL DATA associated with this applic
`No environment variables set for environment <strong>${name}</strong>.`,
'app.cleanup_requested': 'Marked for deletion',
'app.cleanup_requested.description': function (date: DateValue) {
return `The application removal has been requested at ${this.format(
'date',
return `The application removal has been requested at ${this.date(
date
)} and will be processed shortly.`;
},
Expand Down
3 changes: 1 addition & 2 deletions cmd/serve/front/src/lib/localization/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ Cette action est IRRÉVERSIBLE et supprimera TOUTES LES DONNÉES associées : co
`Aucune variable pour l'environnement <strong>${name}</strong>.`,
'app.cleanup_requested': 'Suppression demandée',
'app.cleanup_requested.description': function (date: DateValue) {
return `La suppression de l'application a été demandé le ${this.format(
'date',
return `La suppression de l'application a été demandé le ${this.date(
date
)} et sera traitée sous peu.`;
},
Expand Down
97 changes: 97 additions & 0 deletions cmd/serve/front/src/lib/localization/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, expect, it } from 'vitest';
import {
LocalLocalizationService,
type Locale,
type LocalizationService,
type Translations
} from '.';

const translations = {
greet: (name: string) => `Hello, ${name}!`,
bye: 'Goodbye!'
} satisfies Translations;

const en = {
code: 'en',
displayName: 'English',
translations
} as const satisfies Locale<typeof translations>;

const fr = {
code: 'fr',
displayName: 'Français',
translations: {
greet: (name: string) => `Bonjour, ${name}!`,
bye: 'Au revoir!'
}
} as const satisfies Locale<typeof translations>;

describe('the LocalLocalizationService', () => {
const locales = [en, fr] satisfies Locale<typeof translations>[];

it('should be initialized with the fallback locale if no locale is set', () => {
const service: LocalizationService<typeof translations, typeof locales> =
new LocalLocalizationService({
fallback: 'en',
locales
});

expect(service.locale()).toBe('en');
});

it('should be initialized with the fallback locale if the default locale does not exist', () => {
const service: LocalizationService<typeof translations, typeof locales> =
new LocalLocalizationService({
fallback: 'en',
default: 'it',
locales
});

expect(service.locale()).toBe('en');
});

it('should be initialized with the given locale if set', () => {
const service: LocalizationService<typeof translations, typeof locales> =
new LocalLocalizationService({
fallback: 'en',
default: 'fr',
locales
});

expect(service.locale()).toBe('fr');
});

it('should be able to change the locale', () => {
const service: LocalizationService<typeof translations, typeof locales> =
new LocalLocalizationService({
fallback: 'en',
locales
});

service.locale('fr');

expect(service.locale()).toBe('fr');
});

const service: LocalizationService<typeof translations, typeof locales> =
new LocalLocalizationService({
fallback: 'en',
locales
});

it('should returns all available locales', () => {
expect(service.locales()).toEqual(locales);
});

it('should returns the translation key if the translation does not exist', () => {
expect(service.translate('not-found' as any)).toBe('not-found');
});

it('should correctly translate a string', () => {
expect(service.translate('bye')).toBe('Goodbye!');
});

it('should correctly translate a string with arguments', () => {
expect(service.translate('greet', ['John'])).toBe('Hello, John!');
});
});
59 changes: 25 additions & 34 deletions cmd/serve/front/src/lib/localization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import fr from '$lib/localization/fr';
* Provides formatting methods for a specific locale.
*/
export interface FormatProvider {
format(format: 'date' | 'datetime', value: DateValue): string;
format(format: 'duration', start: DateValue, end: DateValue): string;
date(value: DateValue): string;
datetime(value: DateValue): string;
duration(start: DateValue, end: DateValue): string;
}

export type TranslationsArgs<T> = T extends (...args: infer P) => string ? P : never;
Expand Down Expand Up @@ -77,39 +78,29 @@ export class LocalLocalizationService<T extends Translations, TLocales extends L
this.locale(_options.default ?? _options.fallback);
}

format(format: 'date' | 'datetime', value: DateValue): string;
format(format: 'duration', start: DateValue, end: DateValue): string;
format(
format: 'date' | 'datetime' | 'duration',
valueOrStart: DateValue,
end?: DateValue
): string {
switch (format) {
case 'date':
return new Date(valueOrStart).toLocaleString(this._currentLocaleCode, this._dateOptions);
case 'datetime':
return new Date(valueOrStart).toLocaleString(
this._currentLocaleCode,
this._dateTimeOptions
);
case 'duration':
const diffInSeconds = Math.max(
Math.floor((new Date(end!).getTime() - new Date(valueOrStart).getTime()) / 1000),
0
);

const numberOfMinutes = Math.floor(diffInSeconds / 60);
const numberOfSeconds = diffInSeconds - numberOfMinutes * 60;

// FIXME: handle it better but since for now I only support french and english, this is not needed.
if (numberOfMinutes === 0) {
return `${numberOfSeconds}s`;
}

return `${numberOfMinutes}m ${numberOfSeconds}s`;
default:
throw new Error(`Unsupported format: ${format}`);
date(value: DateValue): string {
return new Date(value).toLocaleDateString(this._currentLocaleCode, this._dateOptions);
}

datetime(value: DateValue): string {
return new Date(value).toLocaleString(this._currentLocaleCode, this._dateTimeOptions);
}

duration(start: DateValue, end: DateValue): string {
const diffInSeconds = Math.max(
Math.floor((new Date(end!).getTime() - new Date(start).getTime()) / 1000),
0
);

const numberOfMinutes = Math.floor(diffInSeconds / 60);
const numberOfSeconds = diffInSeconds - numberOfMinutes * 60;

// FIXME: handle it better but since for now I only support french and english, this is not needed.
if (numberOfMinutes === 0) {
return `${numberOfSeconds}s`;
}

return `${numberOfMinutes}m ${numberOfSeconds}s`;
}

translate<TKey extends keyof T>(key: TKey, args?: TranslationsArgs<T[TKey]> | undefined): string {
Expand Down

0 comments on commit 878e2a6

Please sign in to comment.