Skip to content

Commit 830d48b

Browse files
committed
feat(browserUtils): 添加获取语言国家信息和公共IP的功能
新增 getLocaleInfo 和 getCommonLocaleTable 方法用于获取语言国家信息 改进 getPublicIP 方法增加超时和重试机制 更新相关文档和测试用例
1 parent 0fffa29 commit 830d48b

File tree

5 files changed

+172
-3
lines changed

5 files changed

+172
-3
lines changed

docs/guide/api-browser.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,42 @@ browserUtils.scrollToTop()
117117
```
118118
:::
119119

120+
## 语言与国家对照(Locale Table)
121+
- `browserUtils.getLocaleInfo(locale?)` → 返回语言/国家的规范信息
122+
- `browserUtils.getCommonLocaleTable()` → 常用 Locale 列表(语言/国家英文名)
123+
124+
::: code-group
125+
```ts [Example]
126+
import { browserUtils } from 'nex-lib'
127+
browserUtils.getLocaleInfo('zh-CN')
128+
browserUtils.getLocaleInfo('en-GB')
129+
browserUtils.getCommonLocaleTable()
130+
```
131+
```json [Output]
132+
{"code":"zh-CN","language":{"code":"zh","name":"Chinese"},"country":{"code":"CN","name":"China"}}
133+
{"code":"en-GB","language":{"code":"en","name":"English"},"country":{"code":"GB","name":"United Kingdom"}}
134+
[
135+
{"code":"zh-CN","language":"Chinese","country":"China"},
136+
{"code":"en-US","language":"English","country":"United States"},
137+
{"code":"ja-JP","language":"Japanese","country":"Japan"},
138+
{"code":"ko-KR","language":"Korean","country":"South Korea"}
139+
]
140+
```
141+
:::
142+
143+
## IP 获取(Public IP)
144+
- `browserUtils.getPublicIP()` → 返回公网 IP 字符串(内置超时与重试)
145+
146+
::: code-group
147+
```ts [Example]
148+
import { browserUtils } from 'nex-lib'
149+
const ip = await browserUtils.getPublicIP()
150+
```
151+
```text [Output]
152+
"203.0.113.42"
153+
```
154+
:::
155+
120156
## 主题(Dark/Light)与监听
121157
- `browserUtils.isDarkMode()``boolean`
122158
- `browserUtils.getTheme()``'dark' | 'light'`

docs/scenarios/react.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,55 @@ export default function Theme() {
162162
return null
163163
}
164164
```
165+
:::
166+
167+
## 国际化(IP/语言/时区)
168+
169+
::: info I18n Highlights
170+
- 自动 Locale 与语言/国家英文名
171+
- 本地时区与 UTC 偏移
172+
- 一行获取公共 IP(带超时与重试)
173+
- 示例展示统一 JSON 输出
174+
:::
175+
176+
::: code-group
177+
```jsx [JS]
178+
import { useEffect, useState } from 'react'
179+
import { browserUtils, TimeUtils } from 'nex-lib'
180+
export default function I18nInfo() {
181+
const [info, setInfo] = useState({ ip: '-', locale: '', language: '', country: '', timezone: '', offsetMin: 0, languages: '' })
182+
useEffect(() => {
183+
(async () => {
184+
// 浏览器首选语言(规范化),以及语言/国家英文名
185+
const locale = browserUtils.getPreferredLocale()
186+
const li = browserUtils.getLocaleInfo(locale)
187+
// 本地时区与与 UTC 的分钟偏移
188+
const timezone = TimeUtils.getTimezone()
189+
const offsetMin = TimeUtils.getTimezoneOffsetMinutes()
190+
const ip = await browserUtils.getPublicIP()
191+
setInfo({ ip, locale: li.code, language: li.language.name, country: li.country.name || '', timezone, offsetMin, languages: browserUtils.getLanguage() })
192+
})()
193+
}, [])
194+
return <pre>{JSON.stringify(info, null, 2)}</pre>
195+
}
196+
```
197+
```tsx
198+
import { useEffect, useState } from 'react'
199+
import { browserUtils, TimeUtils } from 'nex-lib'
200+
type Info = { ip: string; locale: string; language: string; country: string; timezone: string; offsetMin: number; languages: string }
201+
export default function I18nInfo() {
202+
const [info, setInfo] = useState<Info>({ ip: '-', locale: '', language: '', country: '', timezone: '', offsetMin: 0, languages: '' })
203+
useEffect(() => {
204+
(async () => {
205+
const locale = browserUtils.getPreferredLocale()
206+
const li = browserUtils.getLocaleInfo(locale)
207+
const timezone = TimeUtils.getTimezone()
208+
const offsetMin = TimeUtils.getTimezoneOffsetMinutes()
209+
const ip = await browserUtils.getPublicIP()
210+
setInfo({ ip, locale: li.code, language: li.language.name, country: li.country.name || '', timezone, offsetMin, languages: browserUtils.getLanguage() })
211+
})()
212+
}, [])
213+
return <pre>{JSON.stringify(info, null, 2)}</pre>
214+
}
215+
```
165216
:::

docs/scenarios/vue.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,41 @@ document.documentElement.style.setProperty('--btn-bg', btnBg)
113113
document.documentElement.style.setProperty('--block-bg', blockBg)
114114
document.documentElement.style.setProperty('--text', textColor)
115115
```
116+
:::
117+
118+
## 国际化(IP/语言/时区)
119+
120+
::: info I18n Highlights
121+
- 自动 Locale 与语言/国家英文名
122+
- 本地时区与 UTC 偏移
123+
- 一行获取公共 IP(带超时与重试)
124+
- 示例展示统一 JSON 输出
125+
:::
126+
127+
::: code-group
128+
```js [JS]
129+
import { browserUtils, TimeUtils } from 'nex-lib'
130+
const locale = browserUtils.getPreferredLocale()
131+
const li = browserUtils.getLocaleInfo(locale)
132+
const timezone = TimeUtils.getTimezone()
133+
const offsetMin = TimeUtils.getTimezoneOffsetMinutes()
134+
const ip = await browserUtils.getPublicIP()
135+
const info = { ip, locale: li.code, language: li.language.name, country: li.country.name || '', timezone, offsetMin, languages: browserUtils.getLanguage() }
136+
// 渲染到页面:<pre id="i18n">...</pre>
137+
document.querySelector('#i18n').textContent = JSON.stringify(info, null, 2)
138+
```
139+
```ts
140+
import { ref, onMounted } from 'vue'
141+
import { browserUtils, TimeUtils } from 'nex-lib'
142+
type Info = { ip: string; locale: string; language: string; country: string; timezone: string; offsetMin: number; languages: string }
143+
const info = ref<Info>({ ip: '-', locale: '', language: '', country: '', timezone: '', offsetMin: 0, languages: '' })
144+
onMounted(async () => {
145+
const locale = browserUtils.getPreferredLocale()
146+
const li = browserUtils.getLocaleInfo(locale)
147+
const timezone = TimeUtils.getTimezone()
148+
const offsetMin = TimeUtils.getTimezoneOffsetMinutes()
149+
const ip = await browserUtils.getPublicIP()
150+
info.value = { ip, locale: li.code, language: li.language.name, country: li.country.name || '', timezone, offsetMin, languages: browserUtils.getLanguage() }
151+
})
152+
```
116153
:::

src/browserUtils.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
const LANGUAGE_NAMES: Record<string, string> = {
2+
en: 'English', zh: 'Chinese', ja: 'Japanese', ko: 'Korean', de: 'German', fr: 'French', es: 'Spanish', it: 'Italian', pt: 'Portuguese', ru: 'Russian', ar: 'Arabic', hi: 'Hindi', th: 'Thai', vi: 'Vietnamese', id: 'Indonesian', nl: 'Dutch', sv: 'Swedish', pl: 'Polish', tr: 'Turkish', he: 'Hebrew', cs: 'Czech', hu: 'Hungarian', ro: 'Romanian', sk: 'Slovak', uk: 'Ukrainian', el: 'Greek', nb: 'Norwegian', da: 'Danish', fi: 'Finnish'
3+
};
4+
const COUNTRY_NAMES: Record<string, string> = {
5+
US: 'United States', GB: 'United Kingdom', CN: 'China', TW: 'Taiwan', HK: 'Hong Kong', JP: 'Japan', KR: 'South Korea', DE: 'Germany', FR: 'France', ES: 'Spain', IT: 'Italy', BR: 'Brazil', PT: 'Portugal', RU: 'Russia', SA: 'Saudi Arabia', IN: 'India', TH: 'Thailand', VN: 'Vietnam', ID: 'Indonesia', NL: 'Netherlands', SE: 'Sweden', PL: 'Poland', TR: 'Turkey', IL: 'Israel', CZ: 'Czech Republic', HU: 'Hungary', RO: 'Romania', SK: 'Slovakia', UA: 'Ukraine', GR: 'Greece', NO: 'Norway', DK: 'Denmark', FI: 'Finland', MX: 'Mexico', CA: 'Canada', AU: 'Australia', NZ: 'New Zealand', IE: 'Ireland', SG: 'Singapore'
6+
};
17
const browserUtils = {
28

39
isBrowser: () => typeof window !== 'undefined',
@@ -44,8 +50,16 @@ const browserUtils = {
4450
return window.getSelection().toString();
4551
},
4652

47-
async getPublicIP(): Promise<any> {
48-
return fetch('https://api.ipify.org?format=json')
53+
async getPublicIP(): Promise<string> {
54+
try {
55+
const r: any = await Http.get('https://api.ipify.org?format=json', { timeoutMs: 3000, retry: 2 });
56+
if (r && typeof r.ip === 'string') return r.ip;
57+
} catch {}
58+
try {
59+
const r: any = await Http.get('https://api.myip.com', { timeoutMs: 3000, retry: 2 });
60+
if (r && typeof r.ip === 'string') return r.ip;
61+
} catch {}
62+
return '';
4963
},
5064

5165
async copyToClipboard(text: string): Promise<boolean> {
@@ -181,8 +195,27 @@ const browserUtils = {
181195
window.addEventListener('resize', fn);
182196
fn();
183197
return () => window.removeEventListener('resize', fn);
198+
},
199+
200+
getLocaleInfo(locale?: string): { code: string; language: { code: string; name: string }; country: { code?: string; name?: string } } {
201+
const raw = (locale || (this as any).getPreferredLocale()).replace('_', '-');
202+
const parts = raw.split('-');
203+
const lang = (parts[0] || '').toLowerCase();
204+
const region = parts[1] ? parts[1].toUpperCase() : undefined;
205+
const languageName = LANGUAGE_NAMES[lang] || lang;
206+
const countryName = region ? (COUNTRY_NAMES[region!] || region) : undefined;
207+
return { code: region ? `${lang}-${region}` : lang, language: { code: lang, name: languageName }, country: { code: region, name: countryName } };
208+
},
209+
210+
getCommonLocaleTable(): { code: string; language: string; country: string }[] {
211+
const codes = ['zh-CN','zh-TW','zh-HK','en-US','en-GB','ja-JP','ko-KR','de-DE','fr-FR','es-ES','it-IT','pt-BR','pt-PT','ru-RU','ar-SA','hi-IN','th-TH','vi-VN','id-ID','nl-NL','sv-SE','pl-PL','tr-TR','he-IL','cs-CZ','hu-HU','ro-RO','sk-SK','uk-UA','el-GR','nb-NO','da-DK','fi-FI','es-MX','en-CA','en-AU','en-NZ','en-IE','zh-SG'];
212+
return codes.map((c) => {
213+
const info = (this as any).getLocaleInfo(c);
214+
return { code: info.code, language: info.language.name, country: info.country.name || '' };
215+
});
184216
}
185217

186218
}
187219

188-
export default browserUtils
220+
export default browserUtils
221+
import Http from './Http'

test/browserUtils.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ describe('browserUtils', () => {
1010
expect(typeof browserUtils.getLanguage()).toBe('string');
1111
});
1212

13+
it('locale info and table', () => {
14+
const zh = browserUtils.getLocaleInfo('zh-CN');
15+
const en = browserUtils.getLocaleInfo('en-GB');
16+
expect(zh.language.name).toBe('Chinese');
17+
expect(zh.country?.name).toBe('China');
18+
expect(en.language.name).toBe('English');
19+
expect(en.country?.name).toBe('United Kingdom');
20+
const table = browserUtils.getCommonLocaleTable();
21+
expect(Array.isArray(table)).toBe(true);
22+
expect(table.length).toBeGreaterThan(10);
23+
});
24+
1325
it('viewport/screen/DPR/mobile flag', () => {
1426
const vp = browserUtils.getViewportSize();
1527
const sc = browserUtils.getScreenSize();

0 commit comments

Comments
 (0)