149 lines
6.0 KiB
JavaScript
149 lines
6.0 KiB
JavaScript
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();
|
||
}
|
||
});
|