Files
KanbanGit/tests/Fixtures/localization.mjs
T
Twyco 5194ed0a17
CI/CD / test_and_check_app (push) Successful in 4m9s
CI/CD / Publish immutable image (push) Skipped
CI/CD / Publish immutable image (pull_request) Canceled after 0s
CI/CD / test_and_check_app (pull_request) Canceled after 3m45s
feat(KAN-16): add English localization foundation
2026-09-09 14:02:20 +02:00

170 lines
5.3 KiB
JavaScript

import assert from 'node:assert/strict';
import { fileURLToPath } from 'node:url';
import vue from '@vitejs/plugin-vue';
import { createServer } from 'vite';
import { computed, createSSRApp, h } from 'vue';
import { renderToString } from 'vue/server-renderer';
const server = await createServer({
configFile: false,
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('../../resources/js', import.meta.url)),
},
},
server: { middlewareMode: true, hmr: false, watch: null },
appType: 'custom',
});
try {
const { createLocalization, validateCatalog } = await server.ssrLoadModule(
'/resources/js/lib/localization.ts',
);
const { useLocalization } = await server.ssrLoadModule(
'/resources/js/composables/useLocalization.ts',
);
const { en } = await server.ssrLoadModule('/resources/js/locales/en.ts');
const { default: AlertError } = await server.ssrLoadModule(
'/resources/js/components/AlertError.vue',
);
const english = createLocalization();
assert.equal(english.t('common.save'), 'Save');
assert.equal(
english.t('pagination.page', { page: 0, total: 2000 }),
'Page 0 of 2,000',
);
for (const [count, expected] of [
[0, '0 tickets'],
[1, '1 ticket'],
[2, '2 tickets'],
[1.5, '1.5 tickets'],
]) {
assert.equal(english.t('tickets.count', { count }), expected);
}
assert.equal(
english.number(1234.5, { minimumFractionDigits: 2 }),
'1,234.50',
);
assert.equal(
english.number(12, { style: 'currency', currency: 'USD' }),
'$12.00',
);
assert.equal(english.date(new Date('2026-09-09T23:30:00Z')), '9/9/2026');
assert.equal(
createLocalization({ timeZone: 'Europe/Berlin' }).date(
new Date('2026-09-09T23:30:00Z'),
),
'9/10/2026',
);
assert.throws(() => english.t('missing.key'), /Missing translation/);
assert.throws(() => english.t('toString'), /Missing translation/);
assert.throws(
() => english.t('pagination.page', { page: 1 }),
/Missing translation parameter/,
);
for (const count of [undefined, '1', Infinity, NaN]) {
assert.throws(
() => english.t('tickets.count', { count }),
/finite count/,
);
}
const incomplete = { ...en };
delete incomplete['common.save'];
assert.throws(
() => validateCatalog(incomplete),
/Missing or invalid translation/,
);
assert.throws(
() =>
validateCatalog({
...en,
'tickets.count': { one: '{count} ticket' },
}),
/plural fallback/,
);
assert.throws(
() => validateCatalog({ ...en, 'pagination.page': 'Page {unknown}' }),
/placeholders/,
);
assert.throws(
() => validateCatalog({ ...en, 'common.save': '' }),
/placeholders/,
);
assert.throws(
() => createLocalization({}, { de: incomplete }),
/Missing or invalid translation/,
);
for (const locale of ['de-DE', 'en_US', 'not a locale', '']) {
assert.equal(createLocalization({ locale }).locale.value, 'en');
}
const de = {
...en,
'common.error': 'Etwas ist schiefgelaufen.',
'common.save': 'Speichern',
};
const translated = createLocalization({ locale: 'de_DE' }, { de });
assert.equal(translated.locale.value, 'de');
assert.equal(translated.number(1234.5), '1.234,5');
const label = computed(() => translated.t('common.save'));
assert.equal(label.value, 'Speichern');
translated.setLocale('en');
assert.equal(label.value, 'Save');
translated.setLocale('de');
assert.equal(label.value, 'Speichern');
assert.equal(english.locale.value, 'en');
const renderAlert = (localization, props = {}) =>
renderToString(
createSSRApp({
render: () =>
h(AlertError, {
errors: ['Invalid input', 'Invalid input'],
...props,
}),
}).use(localization),
);
const [englishHtml, germanHtml] = await Promise.all([
renderAlert(english),
renderAlert(translated),
]);
assert.match(englishHtml, /Something went wrong\./);
assert.match(germanHtml, /Etwas ist schiefgelaufen\./);
assert.equal((englishHtml.match(/Invalid input/g) ?? []).length, 1);
assert.match(
await renderAlert(translated, { title: 'Custom title' }),
/Custom title/,
);
assert.doesNotMatch(
await renderAlert(translated, { title: '' }),
/Etwas ist schiefgelaufen/,
);
const InterpolatedText = {
setup() {
const { t } = useLocalization();
return () =>
h(
'p',
t('pagination.page', {
page: '<script>alert(1)</script>',
total: 1,
}),
);
},
};
const escaped = await renderToString(
createSSRApp(InterpolatedText).use(createLocalization()),
);
assert.match(escaped, /&lt;script&gt;/);
assert.doesNotMatch(escaped, /<script>/);
console.log('Localization behavior checks passed.');
} finally {
await server.close();
}