170 lines
5.3 KiB
JavaScript
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, /<script>/);
|
|
assert.doesNotMatch(escaped, /<script>/);
|
|
|
|
console.log('Localization behavior checks passed.');
|
|
} finally {
|
|
await server.close();
|
|
}
|