Files
hop.noname.company/apps/server/test/localization.test.js

149 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
initialLanguage,
normalizeLanguage,
preferredLanguage,
translate,
translateSystemMessage,
} from '../../../packages/shared/src/localization.js';
import { messages } from '../../../packages/shared/src/messages.js';
import { fixture } from './helpers.js';
test('language priority uses a known country, supported browser languages, quality and safe defaults', () => {
assert.equal(initialLanguage('KR', 'ja-JP,en;q=0.9'), 'ko');
assert.equal(initialLanguage('JP', 'ko-KR'), 'ja');
assert.equal(initialLanguage('CN', 'ko'), 'en');
assert.equal(initialLanguage('US', 'ja'), 'en');
assert.equal(initialLanguage(null, 'fr-FR,ja-JP;q=0.8,en;q=0.5'), 'ja');
assert.equal(initialLanguage('XX', 'ko;q=0,en;q=0.2'), 'en');
assert.equal(initialLanguage(null, undefined), 'en');
assert.equal(preferredLanguage('en;q=oops,ja;q=0.8,ko;q=0.9'), 'ko');
assert.equal(preferredLanguage('ja;q=0,en;q=0', 'ko'), 'ko');
assert.equal(normalizeLanguage('EN-us'), 'en');
assert.equal(normalizeLanguage('fr'), null);
});
test('English/Japanese catalog is complete, contains matching placeholders, and interpolates once without treating content as HTML', () => {
for (const [source, translations] of Object.entries(messages)) {
const placeholders = (text) => [...new Set(text.match(/\{p\d+\}/g) || [])].sort();
for (const language of ['en', 'ja']) {
assert.ok(translations[language]?.trim(), `${language}: ${source}`);
assert.equal(/[가-힣]/.test(translations[language]), false, `${language}: ${source}`);
assert.deepEqual(
placeholders(translations[language]),
placeholders(source),
`${language}: ${source}`,
);
}
}
const name = '<script>日本語 한글 {p1}</script>';
assert.equal(translate('{p0}에 보내기', 'en', { p0: name }), 'Send to ' + name);
assert.equal(
translateSystemMessage('비밀번호는 8~1024자로 입력해 주세요.', 'ja'),
'パスワードは8~1,024文字で入力してください。',
);
assert.equal(
translateSystemMessage('https://example.test/한글', 'en'),
'https://example.test/한글',
);
assert.equal(translateSystemMessage('Email verified.', 'ja'), 'メールアドレスを確認しました。');
assert.equal(
translateSystemMessage('Mac declined the transfer.', 'ja'),
'Macが受信を辞退しました。',
);
});
test('guest default language trusts verified country headers only, and user API system messages use the chosen language', async () => {
const f = await fixture(false, undefined, { clientIpSource: 'cloudflare' });
try {
for (const [country, browser, expected] of [
['KR', 'en', 'ko'],
['JP', 'en', 'ja'],
['CN', 'ja', 'en'],
['XX', 'ja-JP', 'ja'],
]) {
const response = await f.call(null, '/guest/session', 'POST', {}, undefined, {
'cf-connecting-ip': '8.8.8.8',
'cf-ipcountry': country,
'accept-language': browser,
});
assert.equal(response.statusCode, 200, response.body);
assert.equal(response.json().defaultLanguage, expected);
assert.equal(response.headers['cache-control'], 'no-store');
}
f.cfg.trustedProxies = [];
const spoofed = await f.call(null, '/guest/session', 'POST', {}, undefined, {
'cf-connecting-ip': '8.8.8.8',
'cf-ipcountry': 'JP',
'accept-language': 'ko-KR',
});
assert.equal(spoofed.json().defaultLanguage, 'ko');
const client = await f.create();
for (const language of ['en', 'ja']) {
const response = await f.call(
client,
'/auth/login',
'POST',
{ email: 'nobody@hop.test', password: '12345678' },
undefined,
{ 'accept-language': language },
);
assert.equal(response.statusCode, 401);
assert.equal(response.json().code, 'LOGIN_FAILED');
assert.equal(
response.json().message,
translate('이메일 또는 비밀번호를 확인해 주세요.', language),
);
const invalid = await f.call(client, '/device/me', 'PATCH', { name: '' }, undefined, {
'accept-language': language,
});
assert.equal(invalid.json().message, translate('입력 내용을 확인해 주세요.', language));
}
const korean = await f.call(client, '/device/me', 'PATCH', { name: '' });
assert.equal(korean.json().message, '입력 내용을 확인해 주세요.');
} finally {
await f.close();
}
});
test('verification and reset emails use the chosen language without changing one-use URLs or user data', async () => {
const sent = [];
const f = await fixture(false, async (mail) => sent.push(mail), { registrationMode: 'open' });
try {
const client = await f.create();
for (const language of ['ja', 'en']) {
const email = language + '@hop.test';
const response = await f.call(
client,
'/auth/register',
'POST',
{
email,
password: '12345678',
passwordConfirmation: '12345678',
displayName: '日本語 한글 😀',
},
undefined,
{ 'accept-language': language },
);
assert.equal(response.statusCode, 200, response.body);
assert.equal(
response.json().message,
translate('가입을 진행할 수 있는 주소라면 인증 메일이 발송됩니다.', language),
);
assert.equal(sent.at(-1).subject, translate('hop. 이메일 인증', language));
assert.equal(/[가-힣]/.test(sent.at(-1).text), false);
assert.match(sent.at(-1).text, /http:\/\/hop.test\/#verify=[\w-]+/);
const reset = await f.call(client, '/auth/forgot-password', 'POST', { email }, undefined, {
'accept-language': language,
});
assert.equal(reset.statusCode, 200, reset.body);
assert.equal(sent.at(-1).subject, translate('hop. 비밀번호 재설정', language));
assert.match(sent.at(-1).text, /#reset-password=[\w-]+/);
}
} finally {
await f.close();
}
});