termcard 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/browser.js +1 -1
- package/src/build/web.js +4 -3
- package/src/i18n/index.js +18 -1
- package/src/i18n/locales/ar.js +39 -0
- package/src/i18n/locales/bn.js +39 -0
- package/src/i18n/locales/hi.js +39 -0
- package/src/i18n/locales/id.js +39 -0
- package/src/i18n/locales/ja.js +39 -0
- package/src/i18n/locales/pt.js +39 -0
- package/src/i18n/locales/ru.js +39 -0
- package/src/i18n/locales/ur.js +39 -0
- package/src/i18n/locales/zh.js +39 -0
- package/src/index.js +1 -1
- package/src/render/card.js +3 -1
- package/src/text.js +14 -1
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ npm create termcard@latest
|
|
|
40
40
|
- **Save contact** — adds your vCard to their address book
|
|
41
41
|
- **Visit your website and profiles** — LinkedIn, GitHub, X, Behance, Dribbble and more
|
|
42
42
|
- **Copy your email or phone number** to the clipboard
|
|
43
|
-
- **Switch language** —
|
|
43
|
+
- **Switch language** — 14 languages, from English, Chinese and Hindi to Arabic and Urdu (right to left), detected automatically
|
|
44
44
|
|
|
45
45
|
Flags such as `--lang tr`, `--card` (no menu) and `--vcard` work on every card.
|
|
46
46
|
|
package/package.json
CHANGED
package/src/browser.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export { normalizeConfig, ConfigError, isEmail, urlProblem, TEXT_LIMIT, DESCRIPTION_LIMIT, ICON_SETS } from './config/schema.js';
|
|
9
9
|
export { templates, DEFAULT_TEMPLATE, CARD_OPTIONS } from './config/templates.js';
|
|
10
10
|
export { themes, THEME_DESCRIPTIONS, DEFAULT_THEME } from './config/themes.js';
|
|
11
|
-
export { BUILT_IN_LANGUAGES, createTranslator, detectLanguage, languageFromHeader, languageName } from './i18n/index.js';
|
|
11
|
+
export { BUILT_IN_LANGUAGES, createTranslator, detectLanguage, languageFromHeader, languageName, textDirection } from './i18n/index.js';
|
|
12
12
|
export { layoutCard, layoutHeader } from './render/card.js';
|
|
13
13
|
export { toPlain, toHtml, escapeHtml } from './render/format.js';
|
|
14
14
|
export { webIconSet } from './render/icons.js';
|
package/src/build/web.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { layoutCard } from '../render/card.js';
|
|
4
4
|
import { webIconSet } from '../render/icons.js';
|
|
5
5
|
import { toHtml, escapeHtml } from '../render/format.js';
|
|
6
|
-
import { createTranslator, languageName } from '../i18n/index.js';
|
|
6
|
+
import { createTranslator, languageName, textDirection } from '../i18n/index.js';
|
|
7
7
|
import { localize } from '../text.js';
|
|
8
8
|
|
|
9
9
|
export function renderWebPage(config, { command }) {
|
|
@@ -13,7 +13,7 @@ export function renderWebPage(config, { command }) {
|
|
|
13
13
|
const data = Object.fromEntries(
|
|
14
14
|
langs.map((lang) => {
|
|
15
15
|
const t = createTranslator(lang, config.strings);
|
|
16
|
-
return [lang, { card: toHtml(layoutCard(config, lang, { iconSet: webIconSet(config.icons) })), save: t('action.save'), vcf: `contact.${lang}.vcf
|
|
16
|
+
return [lang, { card: toHtml(layoutCard(config, lang, { iconSet: webIconSet(config.icons) })), save: t('action.save'), vcf: `contact.${lang}.vcf`, dir: textDirection(lang, config.strings) }];
|
|
17
17
|
}),
|
|
18
18
|
);
|
|
19
19
|
const description = [config.name, localize(config.title, fallback), localize(config.tagline, fallback)].filter(Boolean).join(' — ');
|
|
@@ -44,7 +44,7 @@ body{min-height:100vh;display:flex;align-items:center;justify-content:center;pad
|
|
|
44
44
|
.lang button{all:unset;cursor:pointer;padding:2px 8px;border-radius:4px;color:var(--faint)}
|
|
45
45
|
.lang button[aria-pressed="true"]{color:var(--accent)}
|
|
46
46
|
.lang button:focus-visible,.btn:focus-visible,a:focus-visible{outline:1px solid var(--accent);outline-offset:2px}
|
|
47
|
-
pre{margin:20px;padding:1.6em 2.4em;border:1px solid var(--line);border-radius:6px;font:inherit;line-height:1.45;white-space:pre;overflow-x:auto}
|
|
47
|
+
pre{unicode-bidi:plaintext;margin:20px;padding:1.6em 2.4em;border:1px solid var(--line);border-radius:6px;font:inherit;line-height:1.45;white-space:pre;overflow-x:auto}
|
|
48
48
|
a{color:inherit;text-decoration:none}
|
|
49
49
|
a:hover span{text-decoration:underline;text-underline-offset:3px}
|
|
50
50
|
.name{color:var(--text);font-weight:700}.text{color:var(--text)}.muted{color:var(--muted)}.faint{color:var(--faint)}.accent{color:var(--accent)}
|
|
@@ -78,6 +78,7 @@ a:hover span{text-decoration:underline;text-underline-offset:3px}
|
|
|
78
78
|
const set = (lang) => {
|
|
79
79
|
if (!has(lang)) lang = langs.includes('${fallback}') ? '${fallback}' : langs[0];
|
|
80
80
|
document.documentElement.lang = lang;
|
|
81
|
+
document.documentElement.dir = data[lang].dir;
|
|
81
82
|
$('card').innerHTML = data[lang].card;
|
|
82
83
|
$('vcf').textContent = data[lang].save;
|
|
83
84
|
$('vcf').href = data[lang].vcf;
|
package/src/i18n/index.js
CHANGED
|
@@ -13,8 +13,18 @@ import tr from './locales/tr.js';
|
|
|
13
13
|
import de from './locales/de.js';
|
|
14
14
|
import es from './locales/es.js';
|
|
15
15
|
import fr from './locales/fr.js';
|
|
16
|
+
import zh from './locales/zh.js';
|
|
17
|
+
import hi from './locales/hi.js';
|
|
18
|
+
import ar from './locales/ar.js';
|
|
19
|
+
import bn from './locales/bn.js';
|
|
20
|
+
import pt from './locales/pt.js';
|
|
21
|
+
import ru from './locales/ru.js';
|
|
22
|
+
import id from './locales/id.js';
|
|
23
|
+
import ur from './locales/ur.js';
|
|
24
|
+
import ja from './locales/ja.js';
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
// The most widely spoken languages, plus Turkish, German and French.
|
|
27
|
+
const LOCALES = { en, zh, hi, es, ar, fr, bn, pt, ru, id, ur, de, ja, tr };
|
|
18
28
|
export const BUILT_IN_LANGUAGES = Object.keys(LOCALES);
|
|
19
29
|
|
|
20
30
|
/** Returns a translator bound to one language, honoring the card's own string overrides. */
|
|
@@ -26,6 +36,13 @@ export function createTranslator(lang, overrides = {}) {
|
|
|
26
36
|
};
|
|
27
37
|
}
|
|
28
38
|
|
|
39
|
+
// Languages written right to left. A card can mark its own languages with "meta.dir": "rtl".
|
|
40
|
+
const RTL = new Set(['ar', 'ur', 'fa', 'he', 'ps', 'sd', 'ug', 'yi', 'dv', 'ckb']);
|
|
41
|
+
|
|
42
|
+
/** "rtl" or "ltr" for a language. */
|
|
43
|
+
export const textDirection = (lang, overrides = {}) =>
|
|
44
|
+
overrides[lang]?.['meta.dir'] ?? (RTL.has(lang) ? 'rtl' : 'ltr');
|
|
45
|
+
|
|
29
46
|
/** Native display name of a language, e.g. "Türkçe". */
|
|
30
47
|
export const languageName = (lang, overrides = {}) =>
|
|
31
48
|
overrides[lang]?.['meta.name'] ?? LOCALES[lang]?.['meta.name'] ?? lang.toUpperCase();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': 'العربية',
|
|
3
|
+
'label.website': 'الموقع',
|
|
4
|
+
'label.email': 'البريد',
|
|
5
|
+
'label.phone': 'الهاتف',
|
|
6
|
+
'action.contact': 'تواصل معي',
|
|
7
|
+
'hint.contact': 'يفتح رسالة جديدة في تطبيق البريد لديك.',
|
|
8
|
+
'action.save': 'حفظ جهة الاتصال',
|
|
9
|
+
'hint.save': 'يضيف بطاقة الاتصال (vCard) إلى دفتر العناوين.',
|
|
10
|
+
'action.website': 'زيارة الموقع',
|
|
11
|
+
'hint.website': 'يفتح {host} في المتصفح.',
|
|
12
|
+
'action.link': 'عرض ملف {label}',
|
|
13
|
+
'hint.link': 'يفتح {host} في المتصفح.',
|
|
14
|
+
'action.copyEmail': 'نسخ البريد الإلكتروني',
|
|
15
|
+
'hint.copyEmail': 'ينسخ العنوان إلى الحافظة.',
|
|
16
|
+
'action.copyPhone': 'نسخ رقم الهاتف',
|
|
17
|
+
'hint.copyPhone': 'ينسخ الرقم إلى الحافظة.',
|
|
18
|
+
'action.language': 'اللغة',
|
|
19
|
+
'hint.language': 'يعرض البطاقة بلغة أخرى.',
|
|
20
|
+
'action.back': 'رجوع',
|
|
21
|
+
'hint.back': 'يعود إلى القائمة الرئيسية.',
|
|
22
|
+
'action.exit': 'خروج',
|
|
23
|
+
'hint.exit': 'يغلق البطاقة.',
|
|
24
|
+
'status.contact': 'تم فتح مسودة البريد.',
|
|
25
|
+
'status.contactFailed': 'تعذّر فتح تطبيق البريد — {value}',
|
|
26
|
+
'status.save': 'تم فتح بطاقة الاتصال.',
|
|
27
|
+
'status.saveFailed': 'تم حفظ بطاقة الاتصال في {value}',
|
|
28
|
+
'status.saveError': 'تعذّر إنشاء بطاقة الاتصال.',
|
|
29
|
+
'status.browser': 'تم الفتح في المتصفح.',
|
|
30
|
+
'status.browserFailed': 'تعذّر فتح المتصفح — {value}',
|
|
31
|
+
'status.copied': 'تم النسخ إلى الحافظة.',
|
|
32
|
+
'status.copyFailed': 'الحافظة غير متاحة — {value}',
|
|
33
|
+
'status.language': 'تم تغيير اللغة.',
|
|
34
|
+
'keys.navigate': 'تنقّل',
|
|
35
|
+
'keys.select': 'اختيار',
|
|
36
|
+
'keys.quit': 'خروج',
|
|
37
|
+
'keys.back': 'رجوع',
|
|
38
|
+
'mail.subject': 'مرحبًا',
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': 'বাংলা',
|
|
3
|
+
'label.website': 'ওয়েবসাইট',
|
|
4
|
+
'label.email': 'ইমেল',
|
|
5
|
+
'label.phone': 'ফোন',
|
|
6
|
+
'action.contact': 'যোগাযোগ করুন',
|
|
7
|
+
'hint.contact': 'আপনার ইমেল অ্যাপে একটি নতুন বার্তা খোলে।',
|
|
8
|
+
'action.save': 'পরিচিতি সংরক্ষণ করুন',
|
|
9
|
+
'hint.save': 'পরিচিতি কার্ড (vCard) আপনার ঠিকানা বইয়ে যোগ করে।',
|
|
10
|
+
'action.website': 'ওয়েবসাইট দেখুন',
|
|
11
|
+
'hint.website': '{host} আপনার ব্রাউজারে খোলে।',
|
|
12
|
+
'action.link': '{label} প্রোফাইল দেখুন',
|
|
13
|
+
'hint.link': '{host} আপনার ব্রাউজারে খোলে।',
|
|
14
|
+
'action.copyEmail': 'ইমেল ঠিকানা কপি করুন',
|
|
15
|
+
'hint.copyEmail': 'ঠিকানাটি ক্লিপবোর্ডে কপি করে।',
|
|
16
|
+
'action.copyPhone': 'ফোন নম্বর কপি করুন',
|
|
17
|
+
'hint.copyPhone': 'নম্বরটি ক্লিপবোর্ডে কপি করে।',
|
|
18
|
+
'action.language': 'ভাষা',
|
|
19
|
+
'hint.language': 'কার্ডটি অন্য ভাষায় দেখায়।',
|
|
20
|
+
'action.back': 'ফিরে যান',
|
|
21
|
+
'hint.back': 'প্রধান মেনুতে ফিরে যায়।',
|
|
22
|
+
'action.exit': 'প্রস্থান',
|
|
23
|
+
'hint.exit': 'কার্ডটি বন্ধ করে।',
|
|
24
|
+
'status.contact': 'ইমেলের খসড়া খোলা হয়েছে।',
|
|
25
|
+
'status.contactFailed': 'ইমেল অ্যাপ খোলা যায়নি — {value}',
|
|
26
|
+
'status.save': 'পরিচিতি কার্ড খোলা হয়েছে।',
|
|
27
|
+
'status.saveFailed': 'পরিচিতি কার্ড সংরক্ষিত হয়েছে: {value}',
|
|
28
|
+
'status.saveError': 'পরিচিতি কার্ড তৈরি করা যায়নি।',
|
|
29
|
+
'status.browser': 'ব্রাউজারে খোলা হয়েছে।',
|
|
30
|
+
'status.browserFailed': 'ব্রাউজার খোলা যায়নি — {value}',
|
|
31
|
+
'status.copied': 'ক্লিপবোর্ডে কপি হয়েছে।',
|
|
32
|
+
'status.copyFailed': 'ক্লিপবোর্ড উপলব্ধ নয় — {value}',
|
|
33
|
+
'status.language': 'ভাষা পরিবর্তন করা হয়েছে।',
|
|
34
|
+
'keys.navigate': 'সরান',
|
|
35
|
+
'keys.select': 'বাছুন',
|
|
36
|
+
'keys.quit': 'প্রস্থান',
|
|
37
|
+
'keys.back': 'ফিরুন',
|
|
38
|
+
'mail.subject': 'হ্যালো',
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': 'हिन्दी',
|
|
3
|
+
'label.website': 'वेबसाइट',
|
|
4
|
+
'label.email': 'ईमेल',
|
|
5
|
+
'label.phone': 'फ़ोन',
|
|
6
|
+
'action.contact': 'संपर्क करें',
|
|
7
|
+
'hint.contact': 'आपके ईमेल ऐप में एक नया संदेश खोलता है।',
|
|
8
|
+
'action.save': 'संपर्क सहेजें',
|
|
9
|
+
'hint.save': 'संपर्क कार्ड (vCard) को आपकी एड्रेस बुक में जोड़ता है।',
|
|
10
|
+
'action.website': 'वेबसाइट देखें',
|
|
11
|
+
'hint.website': '{host} को आपके ब्राउज़र में खोलता है।',
|
|
12
|
+
'action.link': '{label} प्रोफ़ाइल देखें',
|
|
13
|
+
'hint.link': '{host} को आपके ब्राउज़र में खोलता है।',
|
|
14
|
+
'action.copyEmail': 'ईमेल पता कॉपी करें',
|
|
15
|
+
'hint.copyEmail': 'पते को क्लिपबोर्ड पर कॉपी करता है।',
|
|
16
|
+
'action.copyPhone': 'फ़ोन नंबर कॉपी करें',
|
|
17
|
+
'hint.copyPhone': 'नंबर को क्लिपबोर्ड पर कॉपी करता है।',
|
|
18
|
+
'action.language': 'भाषा',
|
|
19
|
+
'hint.language': 'इस कार्ड को किसी दूसरी भाषा में दिखाता है।',
|
|
20
|
+
'action.back': 'वापस',
|
|
21
|
+
'hint.back': 'मुख्य मेनू पर लौटता है।',
|
|
22
|
+
'action.exit': 'बाहर निकलें',
|
|
23
|
+
'hint.exit': 'कार्ड बंद करता है।',
|
|
24
|
+
'status.contact': 'ईमेल ड्राफ़्ट खुल गया।',
|
|
25
|
+
'status.contactFailed': 'ईमेल ऐप नहीं खुल सका — {value}',
|
|
26
|
+
'status.save': 'संपर्क कार्ड खुल गया।',
|
|
27
|
+
'status.saveFailed': 'संपर्क कार्ड यहाँ सहेजा गया: {value}',
|
|
28
|
+
'status.saveError': 'संपर्क कार्ड नहीं बनाया जा सका।',
|
|
29
|
+
'status.browser': 'आपके ब्राउज़र में खुल गया।',
|
|
30
|
+
'status.browserFailed': 'ब्राउज़र नहीं खुल सका — {value}',
|
|
31
|
+
'status.copied': 'क्लिपबोर्ड पर कॉपी हो गया।',
|
|
32
|
+
'status.copyFailed': 'क्लिपबोर्ड उपलब्ध नहीं है — {value}',
|
|
33
|
+
'status.language': 'भाषा बदल दी गई।',
|
|
34
|
+
'keys.navigate': 'चलें',
|
|
35
|
+
'keys.select': 'चुनें',
|
|
36
|
+
'keys.quit': 'बाहर',
|
|
37
|
+
'keys.back': 'वापस',
|
|
38
|
+
'mail.subject': 'नमस्ते',
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': 'Bahasa Indonesia',
|
|
3
|
+
'label.website': 'Situs',
|
|
4
|
+
'label.email': 'Email',
|
|
5
|
+
'label.phone': 'Telepon',
|
|
6
|
+
'action.contact': 'Hubungi saya',
|
|
7
|
+
'hint.contact': 'Membuka pesan baru di aplikasi email Anda.',
|
|
8
|
+
'action.save': 'Simpan kontak',
|
|
9
|
+
'hint.save': 'Menambahkan kartu kontak (vCard) ke buku alamat Anda.',
|
|
10
|
+
'action.website': 'Kunjungi situs',
|
|
11
|
+
'hint.website': 'Membuka {host} di browser Anda.',
|
|
12
|
+
'action.link': 'Lihat profil {label}',
|
|
13
|
+
'hint.link': 'Membuka {host} di browser Anda.',
|
|
14
|
+
'action.copyEmail': 'Salin alamat email',
|
|
15
|
+
'hint.copyEmail': 'Menyalin alamat ke papan klip.',
|
|
16
|
+
'action.copyPhone': 'Salin nomor telepon',
|
|
17
|
+
'hint.copyPhone': 'Menyalin nomor ke papan klip.',
|
|
18
|
+
'action.language': 'Bahasa',
|
|
19
|
+
'hint.language': 'Menampilkan kartu dalam bahasa lain.',
|
|
20
|
+
'action.back': 'Kembali',
|
|
21
|
+
'hint.back': 'Kembali ke menu utama.',
|
|
22
|
+
'action.exit': 'Keluar',
|
|
23
|
+
'hint.exit': 'Menutup kartu.',
|
|
24
|
+
'status.contact': 'Draf email dibuka.',
|
|
25
|
+
'status.contactFailed': 'Tidak dapat membuka aplikasi email — {value}',
|
|
26
|
+
'status.save': 'Kartu kontak dibuka.',
|
|
27
|
+
'status.saveFailed': 'Kartu kontak disimpan di {value}',
|
|
28
|
+
'status.saveError': 'Tidak dapat membuat kartu kontak.',
|
|
29
|
+
'status.browser': 'Dibuka di browser Anda.',
|
|
30
|
+
'status.browserFailed': 'Tidak dapat membuka browser — {value}',
|
|
31
|
+
'status.copied': 'Disalin ke papan klip.',
|
|
32
|
+
'status.copyFailed': 'Papan klip tidak tersedia — {value}',
|
|
33
|
+
'status.language': 'Bahasa diubah.',
|
|
34
|
+
'keys.navigate': 'navigasi',
|
|
35
|
+
'keys.select': 'pilih',
|
|
36
|
+
'keys.quit': 'keluar',
|
|
37
|
+
'keys.back': 'kembali',
|
|
38
|
+
'mail.subject': 'Halo',
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': '日本語',
|
|
3
|
+
'label.website': 'ウェブ',
|
|
4
|
+
'label.email': 'メール',
|
|
5
|
+
'label.phone': '電話',
|
|
6
|
+
'action.contact': '連絡する',
|
|
7
|
+
'hint.contact': 'メールアプリで新しいメッセージを作成します。',
|
|
8
|
+
'action.save': '連絡先を保存',
|
|
9
|
+
'hint.save': '名刺(vCard)をアドレス帳に追加します。',
|
|
10
|
+
'action.website': 'ウェブサイトを見る',
|
|
11
|
+
'hint.website': '{host} をブラウザで開きます。',
|
|
12
|
+
'action.link': '{label} のプロフィールを見る',
|
|
13
|
+
'hint.link': '{host} をブラウザで開きます。',
|
|
14
|
+
'action.copyEmail': 'メールアドレスをコピー',
|
|
15
|
+
'hint.copyEmail': 'アドレスをクリップボードにコピーします。',
|
|
16
|
+
'action.copyPhone': '電話番号をコピー',
|
|
17
|
+
'hint.copyPhone': '番号をクリップボードにコピーします。',
|
|
18
|
+
'action.language': '言語',
|
|
19
|
+
'hint.language': '名刺を別の言語で表示します。',
|
|
20
|
+
'action.back': '戻る',
|
|
21
|
+
'hint.back': 'メインメニューに戻ります。',
|
|
22
|
+
'action.exit': '終了',
|
|
23
|
+
'hint.exit': '名刺を閉じます。',
|
|
24
|
+
'status.contact': 'メールの下書きを開きました。',
|
|
25
|
+
'status.contactFailed': 'メールアプリを開けませんでした — {value}',
|
|
26
|
+
'status.save': '連絡先カードを開きました。',
|
|
27
|
+
'status.saveFailed': '連絡先カードを保存しました: {value}',
|
|
28
|
+
'status.saveError': '連絡先カードを作成できませんでした。',
|
|
29
|
+
'status.browser': 'ブラウザで開きました。',
|
|
30
|
+
'status.browserFailed': 'ブラウザを開けませんでした — {value}',
|
|
31
|
+
'status.copied': 'クリップボードにコピーしました。',
|
|
32
|
+
'status.copyFailed': 'クリップボードを利用できません — {value}',
|
|
33
|
+
'status.language': '言語を変更しました。',
|
|
34
|
+
'keys.navigate': '移動',
|
|
35
|
+
'keys.select': '選択',
|
|
36
|
+
'keys.quit': '終了',
|
|
37
|
+
'keys.back': '戻る',
|
|
38
|
+
'mail.subject': 'こんにちは',
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': 'Português',
|
|
3
|
+
'label.website': 'Site',
|
|
4
|
+
'label.email': 'E-mail',
|
|
5
|
+
'label.phone': 'Telefone',
|
|
6
|
+
'action.contact': 'Entrar em contato',
|
|
7
|
+
'hint.contact': 'Abre uma nova mensagem no seu app de e-mail.',
|
|
8
|
+
'action.save': 'Salvar contato',
|
|
9
|
+
'hint.save': 'Adiciona o cartão de contato (vCard) à sua agenda.',
|
|
10
|
+
'action.website': 'Visitar site',
|
|
11
|
+
'hint.website': 'Abre {host} no seu navegador.',
|
|
12
|
+
'action.link': 'Ver perfil no {label}',
|
|
13
|
+
'hint.link': 'Abre {host} no seu navegador.',
|
|
14
|
+
'action.copyEmail': 'Copiar e-mail',
|
|
15
|
+
'hint.copyEmail': 'Copia o endereço para a área de transferência.',
|
|
16
|
+
'action.copyPhone': 'Copiar telefone',
|
|
17
|
+
'hint.copyPhone': 'Copia o número para a área de transferência.',
|
|
18
|
+
'action.language': 'Idioma',
|
|
19
|
+
'hint.language': 'Mostra o cartão em outro idioma.',
|
|
20
|
+
'action.back': 'Voltar',
|
|
21
|
+
'hint.back': 'Volta ao menu principal.',
|
|
22
|
+
'action.exit': 'Sair',
|
|
23
|
+
'hint.exit': 'Fecha o cartão.',
|
|
24
|
+
'status.contact': 'Rascunho de e-mail aberto.',
|
|
25
|
+
'status.contactFailed': 'Não foi possível abrir um app de e-mail — {value}',
|
|
26
|
+
'status.save': 'Cartão de contato aberto.',
|
|
27
|
+
'status.saveFailed': 'Cartão de contato salvo em {value}',
|
|
28
|
+
'status.saveError': 'Não foi possível criar o cartão de contato.',
|
|
29
|
+
'status.browser': 'Aberto no seu navegador.',
|
|
30
|
+
'status.browserFailed': 'Não foi possível abrir um navegador — {value}',
|
|
31
|
+
'status.copied': 'Copiado para a área de transferência.',
|
|
32
|
+
'status.copyFailed': 'Área de transferência indisponível — {value}',
|
|
33
|
+
'status.language': 'Idioma alterado.',
|
|
34
|
+
'keys.navigate': 'navegar',
|
|
35
|
+
'keys.select': 'selecionar',
|
|
36
|
+
'keys.quit': 'sair',
|
|
37
|
+
'keys.back': 'voltar',
|
|
38
|
+
'mail.subject': 'Olá',
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': 'Русский',
|
|
3
|
+
'label.website': 'Сайт',
|
|
4
|
+
'label.email': 'Почта',
|
|
5
|
+
'label.phone': 'Телефон',
|
|
6
|
+
'action.contact': 'Написать мне',
|
|
7
|
+
'hint.contact': 'Открывает новое письмо в почтовом приложении.',
|
|
8
|
+
'action.save': 'Сохранить контакт',
|
|
9
|
+
'hint.save': 'Добавляет визитку (vCard) в адресную книгу.',
|
|
10
|
+
'action.website': 'Открыть сайт',
|
|
11
|
+
'hint.website': 'Открывает {host} в браузере.',
|
|
12
|
+
'action.link': 'Профиль в {label}',
|
|
13
|
+
'hint.link': 'Открывает {host} в браузере.',
|
|
14
|
+
'action.copyEmail': 'Скопировать e-mail',
|
|
15
|
+
'hint.copyEmail': 'Копирует адрес в буфер обмена.',
|
|
16
|
+
'action.copyPhone': 'Скопировать телефон',
|
|
17
|
+
'hint.copyPhone': 'Копирует номер в буфер обмена.',
|
|
18
|
+
'action.language': 'Язык',
|
|
19
|
+
'hint.language': 'Показывает визитку на другом языке.',
|
|
20
|
+
'action.back': 'Назад',
|
|
21
|
+
'hint.back': 'Возвращает в главное меню.',
|
|
22
|
+
'action.exit': 'Выход',
|
|
23
|
+
'hint.exit': 'Закрывает визитку.',
|
|
24
|
+
'status.contact': 'Черновик письма открыт.',
|
|
25
|
+
'status.contactFailed': 'Не удалось открыть почтовое приложение — {value}',
|
|
26
|
+
'status.save': 'Визитка открыта.',
|
|
27
|
+
'status.saveFailed': 'Визитка сохранена: {value}',
|
|
28
|
+
'status.saveError': 'Не удалось создать визитку.',
|
|
29
|
+
'status.browser': 'Открыто в браузере.',
|
|
30
|
+
'status.browserFailed': 'Не удалось открыть браузер — {value}',
|
|
31
|
+
'status.copied': 'Скопировано в буфер обмена.',
|
|
32
|
+
'status.copyFailed': 'Буфер обмена недоступен — {value}',
|
|
33
|
+
'status.language': 'Язык изменён.',
|
|
34
|
+
'keys.navigate': 'навигация',
|
|
35
|
+
'keys.select': 'выбрать',
|
|
36
|
+
'keys.quit': 'выход',
|
|
37
|
+
'keys.back': 'назад',
|
|
38
|
+
'mail.subject': 'Здравствуйте',
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': 'اردو',
|
|
3
|
+
'label.website': 'ویب سائٹ',
|
|
4
|
+
'label.email': 'ای میل',
|
|
5
|
+
'label.phone': 'فون',
|
|
6
|
+
'action.contact': 'رابطہ کریں',
|
|
7
|
+
'hint.contact': 'آپ کی ای میل ایپ میں نیا پیغام کھولتا ہے۔',
|
|
8
|
+
'action.save': 'رابطہ محفوظ کریں',
|
|
9
|
+
'hint.save': 'رابطہ کارڈ (vCard) آپ کی ایڈریس بک میں شامل کرتا ہے۔',
|
|
10
|
+
'action.website': 'ویب سائٹ دیکھیں',
|
|
11
|
+
'hint.website': '{host} کو آپ کے براؤزر میں کھولتا ہے۔',
|
|
12
|
+
'action.link': '{label} پروفائل دیکھیں',
|
|
13
|
+
'hint.link': '{host} کو آپ کے براؤزر میں کھولتا ہے۔',
|
|
14
|
+
'action.copyEmail': 'ای میل پتہ کاپی کریں',
|
|
15
|
+
'hint.copyEmail': 'پتہ کلپ بورڈ پر کاپی کرتا ہے۔',
|
|
16
|
+
'action.copyPhone': 'فون نمبر کاپی کریں',
|
|
17
|
+
'hint.copyPhone': 'نمبر کلپ بورڈ پر کاپی کرتا ہے۔',
|
|
18
|
+
'action.language': 'زبان',
|
|
19
|
+
'hint.language': 'کارڈ کو کسی اور زبان میں دکھاتا ہے۔',
|
|
20
|
+
'action.back': 'واپس',
|
|
21
|
+
'hint.back': 'مرکزی مینو پر واپس جاتا ہے۔',
|
|
22
|
+
'action.exit': 'باہر نکلیں',
|
|
23
|
+
'hint.exit': 'کارڈ بند کرتا ہے۔',
|
|
24
|
+
'status.contact': 'ای میل کا مسودہ کھل گیا۔',
|
|
25
|
+
'status.contactFailed': 'ای میل ایپ نہیں کھل سکی — {value}',
|
|
26
|
+
'status.save': 'رابطہ کارڈ کھل گیا۔',
|
|
27
|
+
'status.saveFailed': 'رابطہ کارڈ یہاں محفوظ ہوا: {value}',
|
|
28
|
+
'status.saveError': 'رابطہ کارڈ نہیں بن سکا۔',
|
|
29
|
+
'status.browser': 'براؤزر میں کھل گیا۔',
|
|
30
|
+
'status.browserFailed': 'براؤزر نہیں کھل سکا — {value}',
|
|
31
|
+
'status.copied': 'کلپ بورڈ پر کاپی ہو گیا۔',
|
|
32
|
+
'status.copyFailed': 'کلپ بورڈ دستیاب نہیں — {value}',
|
|
33
|
+
'status.language': 'زبان تبدیل کر دی گئی۔',
|
|
34
|
+
'keys.navigate': 'چلیں',
|
|
35
|
+
'keys.select': 'منتخب',
|
|
36
|
+
'keys.quit': 'باہر',
|
|
37
|
+
'keys.back': 'واپس',
|
|
38
|
+
'mail.subject': 'سلام',
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
'meta.name': '中文',
|
|
3
|
+
'label.website': '网站',
|
|
4
|
+
'label.email': '邮箱',
|
|
5
|
+
'label.phone': '电话',
|
|
6
|
+
'action.contact': '联系我',
|
|
7
|
+
'hint.contact': '在你的邮件应用中新建一封邮件。',
|
|
8
|
+
'action.save': '保存联系人',
|
|
9
|
+
'hint.save': '将名片(vCard)添加到你的通讯录。',
|
|
10
|
+
'action.website': '访问网站',
|
|
11
|
+
'hint.website': '在浏览器中打开 {host}。',
|
|
12
|
+
'action.link': '查看 {label} 主页',
|
|
13
|
+
'hint.link': '在浏览器中打开 {host}。',
|
|
14
|
+
'action.copyEmail': '复制邮箱地址',
|
|
15
|
+
'hint.copyEmail': '将地址复制到剪贴板。',
|
|
16
|
+
'action.copyPhone': '复制电话号码',
|
|
17
|
+
'hint.copyPhone': '将号码复制到剪贴板。',
|
|
18
|
+
'action.language': '语言',
|
|
19
|
+
'hint.language': '以其他语言显示这张名片。',
|
|
20
|
+
'action.back': '返回',
|
|
21
|
+
'hint.back': '返回主菜单。',
|
|
22
|
+
'action.exit': '退出',
|
|
23
|
+
'hint.exit': '关闭名片。',
|
|
24
|
+
'status.contact': '已打开邮件草稿。',
|
|
25
|
+
'status.contactFailed': '无法打开邮件应用 — {value}',
|
|
26
|
+
'status.save': '已打开联系人名片。',
|
|
27
|
+
'status.saveFailed': '联系人名片已保存到 {value}',
|
|
28
|
+
'status.saveError': '无法创建联系人名片。',
|
|
29
|
+
'status.browser': '已在浏览器中打开。',
|
|
30
|
+
'status.browserFailed': '无法打开浏览器 — {value}',
|
|
31
|
+
'status.copied': '已复制到剪贴板。',
|
|
32
|
+
'status.copyFailed': '剪贴板不可用 — {value}',
|
|
33
|
+
'status.language': '语言已更改。',
|
|
34
|
+
'keys.navigate': '移动',
|
|
35
|
+
'keys.select': '选择',
|
|
36
|
+
'keys.quit': '退出',
|
|
37
|
+
'keys.back': '返回',
|
|
38
|
+
'mail.subject': '你好',
|
|
39
|
+
};
|
package/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { loadConfig } from './config/load.js';
|
|
|
10
10
|
export { normalizeConfig, ConfigError, isEmail, urlProblem, TEXT_LIMIT, DESCRIPTION_LIMIT, ICON_SETS } from './config/schema.js';
|
|
11
11
|
export { templates, DEFAULT_TEMPLATE } from './config/templates.js';
|
|
12
12
|
export { themes, THEME_DESCRIPTIONS, DEFAULT_THEME } from './config/themes.js';
|
|
13
|
-
export { BUILT_IN_LANGUAGES, detectLanguage, languageName } from './i18n/index.js';
|
|
13
|
+
export { BUILT_IN_LANGUAGES, detectLanguage, languageName, textDirection } from './i18n/index.js';
|
|
14
14
|
export { layoutCard, layoutHeader } from './render/card.js';
|
|
15
15
|
export { toAnsi, toPlain, toHtml } from './render/format.js';
|
|
16
16
|
export { colorDepth } from './render/color.js';
|
package/src/render/card.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* renderer replaces with CSS. Formatters live in ./format.js.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { displayWidth, localize, stripProtocol, upper, wrap } from '../text.js';
|
|
8
|
+
import { displayWidth, hasCase, localize, stripProtocol, upper, wrap } from '../text.js';
|
|
9
9
|
import { createTranslator } from '../i18n/index.js';
|
|
10
10
|
import { borders } from './borders.js';
|
|
11
11
|
import { iconFor } from './icons.js';
|
|
@@ -20,6 +20,8 @@ function nameLines(name, { nameStyle, splitName }, lang) {
|
|
|
20
20
|
const words = name.split(/\s+/);
|
|
21
21
|
const parts = splitName && words.length > 1 ? [words.slice(0, -1).join(' '), words.at(-1)] : [name];
|
|
22
22
|
return parts.map((part) => {
|
|
23
|
+
// Scripts without case (Arabic, Devanagari, CJK, …) are shown as written: spacing would break them.
|
|
24
|
+
if (!hasCase(part)) return part;
|
|
23
25
|
if (nameStyle === 'spaced') return [...upper(part, lang)].join(' ');
|
|
24
26
|
if (nameStyle === 'uppercase') return upper(part, lang);
|
|
25
27
|
return part;
|
package/src/text.js
CHANGED
|
@@ -9,8 +9,18 @@ const UNSAFE = /[\u0000-\u001f\u007f-\u009f--]/g;
|
|
|
9
9
|
|
|
10
10
|
export const sanitize = (value) => String(value).replace(UNSAFE, '').trim();
|
|
11
11
|
|
|
12
|
+
// Non-spacing combining marks: they attach to the previous letter and take no column.
|
|
13
|
+
const COMBINING = [
|
|
14
|
+
[0x0300, 0x036f], // Latin, Greek, Cyrillic diacritics
|
|
15
|
+
[0x0591, 0x05bd], [0x05bf, 0x05bf], [0x05c1, 0x05c2], [0x05c4, 0x05c5], [0x05c7, 0x05c7], // Hebrew points
|
|
16
|
+
[0x0610, 0x061a], [0x064b, 0x065f], [0x0670, 0x0670], [0x06d6, 0x06dc], [0x06df, 0x06e4], [0x06e7, 0x06e8], [0x06ea, 0x06ed], // Arabic
|
|
17
|
+
[0x0900, 0x0902], [0x093a, 0x093a], [0x093c, 0x093c], [0x0941, 0x0948], [0x094d, 0x094d], [0x0951, 0x0957], [0x0962, 0x0963], // Devanagari
|
|
18
|
+
[0x0981, 0x0981], [0x09bc, 0x09bc], [0x09c1, 0x09c4], [0x09cd, 0x09cd], [0x09e2, 0x09e3], // Bengali
|
|
19
|
+
[0x3099, 0x309a], // Japanese voicing marks
|
|
20
|
+
];
|
|
21
|
+
|
|
12
22
|
const isZeroWidth = (cp) =>
|
|
13
|
-
(cp >=
|
|
23
|
+
COMBINING.some(([from, to]) => cp >= from && cp <= to) ||
|
|
14
24
|
(cp >= 0x200b && cp <= 0x200f) || // zero-width space/joiners
|
|
15
25
|
(cp >= 0xfe00 && cp <= 0xfe0f); // variation selectors
|
|
16
26
|
|
|
@@ -62,6 +72,9 @@ export function localize(value, lang, fallback = 'en') {
|
|
|
62
72
|
return value[lang] ?? value[fallback] ?? Object.values(value)[0] ?? '';
|
|
63
73
|
}
|
|
64
74
|
|
|
75
|
+
/** Whether text contains letters with upper/lower case (Latin, Greek, Cyrillic, …). */
|
|
76
|
+
export const hasCase = (text) => /\p{Lu}|\p{Ll}/u.test(text);
|
|
77
|
+
|
|
65
78
|
/** Locale-aware uppercase that keeps Turkish dotted/dotless i correct. */
|
|
66
79
|
export const upper = (text, lang) => text.toLocaleUpperCase(lang === 'tr' || lang === 'az' ? 'tr-TR' : undefined);
|
|
67
80
|
|