APP-PUSH和纯色小组件
This commit is contained in:
@@ -33,9 +33,13 @@ function mapMomStage(raw: string | undefined): QuestionnaireAnswersV1_2['mom_sta
|
||||
|
||||
function mapEmotion(raw: string | undefined): QuestionnaireAnswersV1_2['emotion'] {
|
||||
if (!raw) return null;
|
||||
// UI 当前选项:happy/calm/stressed/low
|
||||
// UI 选项:
|
||||
// - happy/calm/stressed/low:历史选项(仍保留兼容)
|
||||
// - okay/tired:新增选项
|
||||
if (raw === 'happy') return 'joyful';
|
||||
if (raw === 'calm') return 'calm';
|
||||
if (raw === 'okay') return 'neutral';
|
||||
if (raw === 'tired') return 'tired';
|
||||
if (raw === 'stressed') return 'overwhelmed';
|
||||
if (raw === 'low') return 'low';
|
||||
return null;
|
||||
|
||||
@@ -3,6 +3,8 @@ import * as Localization from 'expo-localization';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import { isTraditionalChineseLocaleTag } from './locale';
|
||||
|
||||
// 用 require 避免 TS 的 json module 配置差异导致无法编译
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const all = require('./locales/all.json') as { en: Record<string, unknown>; 'zh-TW': Record<string, unknown> };
|
||||
@@ -29,13 +31,8 @@ function isSupportedLanguage(lang: string): lang is AppLanguage {
|
||||
function normalizeDeviceLanguageTagToAppLanguage(languageTag: string): AppLanguage {
|
||||
const tag = languageTag.toLowerCase();
|
||||
|
||||
// 中文:当前仅支持繁体中文(zh-TW)
|
||||
if (tag.startsWith('zh')) {
|
||||
return 'zh-TW';
|
||||
}
|
||||
|
||||
// 其他语言:按前缀匹配(当前仅支持英文)
|
||||
if (tag.startsWith('en')) return 'en';
|
||||
if (isTraditionalChineseLocaleTag(tag)) return 'zh-TW';
|
||||
|
||||
return DEFAULT_FALLBACK_LANGUAGE;
|
||||
}
|
||||
|
||||
35
client/src/i18n/locale.ts
Normal file
35
client/src/i18n/locale.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export type BackendLocale = 'en' | 'tc';
|
||||
|
||||
/**
|
||||
* 判断一个 BCP-47 language tag 是否应视为「繁体中文」(TC)。
|
||||
*
|
||||
* 规则(面向当前产品约束:只支持 EN/TC,默认 EN):
|
||||
* - 仅在语言为中文(zh)且脚本为 Hant 或地区为 TW/HK/MO 时,判定为 TC
|
||||
* - 兼容后端/历史写法:明确包含 tc 也视为 TC
|
||||
* - 其他情况一律视为 EN
|
||||
*/
|
||||
export function isTraditionalChineseLocaleTag(languageTag: string): boolean {
|
||||
const tag = (languageTag || '').trim().toLowerCase();
|
||||
if (!tag) return false;
|
||||
|
||||
// 兼容:有些链路可能直接传 tc
|
||||
const parts = tag.split(/[-_]/g).filter(Boolean);
|
||||
if (parts.includes('tc')) return true;
|
||||
|
||||
const lang = parts[0];
|
||||
if (lang !== 'zh') return false;
|
||||
|
||||
// 脚本:zh-Hant / zh-Hant-TW / zh-Hant-HK ...
|
||||
if (tag.includes('hant') || parts.includes('hant')) return true;
|
||||
|
||||
// 地区:zh-TW / zh-HK / zh-MO
|
||||
if (parts.includes('tw') || parts.includes('hk') || parts.includes('mo')) return true;
|
||||
|
||||
// 其他中文(如 zh / zh-CN / zh-Hans)不属于 TC → 回退 EN
|
||||
return false;
|
||||
}
|
||||
|
||||
export function toBackendLocaleFromLanguageTag(languageTag: string | null | undefined): BackendLocale {
|
||||
return isTraditionalChineseLocaleTag(languageTag ?? '') ? 'tc' : 'en';
|
||||
}
|
||||
|
||||
@@ -25,39 +25,41 @@
|
||||
},
|
||||
"onboardingSurvey": {
|
||||
"steps": {
|
||||
"name": { "title": "What should I call you?" },
|
||||
"name": { "title": "What do you want to be called?", "placeholder": "Mama" },
|
||||
"status": {
|
||||
"title": "Your current stage?",
|
||||
"title": "Which stage of motherhood are you in?",
|
||||
"options": {
|
||||
"pregnant": "Pregnant / preparing for motherhood",
|
||||
"has_kids": "Already have kids",
|
||||
"pregnant": "Pregnant / Preparing",
|
||||
"has_kids": "Parenting",
|
||||
"no_fill": "Prefer not to say"
|
||||
}
|
||||
},
|
||||
"emotion": {
|
||||
"title": "How are you feeling right now?",
|
||||
"options": {
|
||||
"happy": "Happy / satisfied",
|
||||
"calm": "Calm / grounded",
|
||||
"stressed": "Stressed / overwhelmed",
|
||||
"low": "Down / low mood"
|
||||
"happy": "Joyful",
|
||||
"calm": "Calm",
|
||||
"okay": "Okay",
|
||||
"tired": "Tired",
|
||||
"stressed": "Overwhelmed",
|
||||
"low": "Low"
|
||||
}
|
||||
},
|
||||
"influence": {
|
||||
"title": "What has been affecting you lately?",
|
||||
"title": "What’s been influencing how you feel?",
|
||||
"options": {
|
||||
"family": "Family & kids",
|
||||
"family": "Family",
|
||||
"work": "Work or study",
|
||||
"relationship": "Intimate relationship",
|
||||
"friends": "Friends & social life",
|
||||
"health": "Mental & physical health"
|
||||
"relationship": "Relationship",
|
||||
"friends": "Friends",
|
||||
"health": "Health"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
"title": "What support do you need most?",
|
||||
"title": "What kind of support do you need most right now?",
|
||||
"options": {
|
||||
"emotional": "Emotional support",
|
||||
"parenting": "Parenting stress",
|
||||
"parenting": "Parenting pressure",
|
||||
"self_worth": "Self-worth",
|
||||
"anxiety": "Anxiety relief",
|
||||
"balance": "Rest & balance"
|
||||
@@ -119,6 +121,9 @@
|
||||
"widget": {
|
||||
"lockScreen": "Lock Screen Widget",
|
||||
"homeScreen": "Home Screen Widget",
|
||||
"howToTitle": "How to add the widget",
|
||||
"howToDesc1": "Long-press on the Home Screen until the apps jiggle, then tap “+” in the top-left corner.",
|
||||
"howToDesc2": "Search “Mindfulness”, choose a widget size you like, then tap “Add Widget”.",
|
||||
"previewDate": "Thu, Jan 29",
|
||||
"previewQuote": "I’m proud of who I am, even while becoming who I want to be."
|
||||
},
|
||||
@@ -136,10 +141,11 @@
|
||||
},
|
||||
"consent": {
|
||||
"title": "You Are Perfect.",
|
||||
"subtitle": "Everything Will Be Better.",
|
||||
"subtitle": "Everything\nWill Be Better.",
|
||||
"agree": "Agree & Continue",
|
||||
"privacy": "Privacy Policy",
|
||||
"terms": "Terms of Use"
|
||||
"terms": "Terms of Use",
|
||||
"notice": "By continuing, you agree to the Privacy Policy and Terms of Use."
|
||||
},
|
||||
"permissions": {
|
||||
"notificationsDenied": "Notifications are denied. Please enable them in Settings."
|
||||
@@ -180,7 +186,7 @@
|
||||
},
|
||||
"onboardingSurvey": {
|
||||
"steps": {
|
||||
"name": { "title": "我可以怎麼稱呼你?" },
|
||||
"name": { "title": "我可以怎麼稱呼你?", "placeholder": "媽媽" },
|
||||
"status": {
|
||||
"title": "媽媽的狀態?",
|
||||
"options": {
|
||||
@@ -194,6 +200,8 @@
|
||||
"options": {
|
||||
"happy": "愉悅、滿足",
|
||||
"calm": "平靜、安穩",
|
||||
"okay": "還可以、普通",
|
||||
"tired": "疲累、沒什麼力氣",
|
||||
"stressed": "被壓得有點喘不過氣",
|
||||
"low": "情緒低落"
|
||||
}
|
||||
@@ -274,6 +282,9 @@
|
||||
"widget": {
|
||||
"lockScreen": "鎖屏小工具",
|
||||
"homeScreen": "桌面小工具",
|
||||
"howToTitle": "如何添加小工具",
|
||||
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||
"howToDesc2": "搜尋「正念」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||
},
|
||||
@@ -290,9 +301,12 @@
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"title": "你很完美。",
|
||||
"subtitle": "一切\n都會更好。",
|
||||
"agree": "同意並繼續",
|
||||
"privacy": "隱私協議",
|
||||
"terms": "用戶使用協議"
|
||||
"terms": "用戶使用協議",
|
||||
"notice": "繼續使用即代表你同意《隱私協議》與《用戶使用協議》。"
|
||||
},
|
||||
"permissions": {
|
||||
"notificationsDenied": "系統權限已被拒絕,請前往手機設定開啟通知。"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { API_BASE_URL } from '@/src/constants/env';
|
||||
import type { UserProfileV1_2, UserProfileV1_2_Extended } from '@/src/features/userProfileScoring/types';
|
||||
import { toBackendLocaleFromLanguageTag } from '@/src/i18n/locale';
|
||||
import { fetchRecoWidget } from '@/src/services/recoApi';
|
||||
import i18n from 'i18next';
|
||||
import { getUserProfileScoring } from '@/src/storage/appStorage';
|
||||
@@ -144,7 +145,7 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
||||
const top = items?.[0];
|
||||
if (!top?.text) return;
|
||||
|
||||
const lang = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
|
||||
const lang = toBackendLocaleFromLanguageTag(i18n.language);
|
||||
await setWidgetDailyRecoCache({
|
||||
schema_version: 1,
|
||||
saved_at: new Date().toISOString(),
|
||||
|
||||
@@ -20,10 +20,15 @@ describe('legalApi.buildAcceptLanguage', () => {
|
||||
expect(buildAcceptLanguage()).toBe('en');
|
||||
});
|
||||
|
||||
it('任意 zh* 归一为 tc', () => {
|
||||
it('简中/其他中文不支持时回退为 en', () => {
|
||||
setLang('zh-CN');
|
||||
expect(buildAcceptLanguage()).toBe('tc');
|
||||
expect(buildAcceptLanguage()).toBe('en');
|
||||
|
||||
setLang('zh');
|
||||
expect(buildAcceptLanguage()).toBe('en');
|
||||
});
|
||||
|
||||
it('繁体中文归一为 tc', () => {
|
||||
setLang('zh-TW');
|
||||
expect(buildAcceptLanguage()).toBe('tc');
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import i18n from 'i18next';
|
||||
|
||||
import { httpJson } from '../utils/http';
|
||||
import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
|
||||
|
||||
export type LegalLinks = {
|
||||
privacyPolicyUrl: string;
|
||||
@@ -9,17 +10,8 @@ export type LegalLinks = {
|
||||
};
|
||||
|
||||
export function buildAcceptLanguage(): 'en' | 'tc' {
|
||||
const lang = (i18n.language || '').trim();
|
||||
const lower = lang.toLowerCase();
|
||||
|
||||
// 当前多语言仅支持 EN / TC(与 reco 链路一致);其他语言统一回退到 en
|
||||
if (lower.startsWith('zh')) {
|
||||
return 'tc';
|
||||
}
|
||||
if (lower.includes('tc') || lower.includes('hant') || lower.includes('hk') || lower.includes('mo') || lower.includes('tw')) {
|
||||
return 'tc';
|
||||
}
|
||||
return 'en';
|
||||
// 当前多语言仅支持 EN / TC;其他语言统一回退到 en
|
||||
return toBackendLocaleFromLanguageTag(i18n.language);
|
||||
}
|
||||
|
||||
export async function fetchLegalLinks(): Promise<LegalLinks> {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { httpJson } from '../utils/http';
|
||||
import { APP_ENV } from '../constants/env';
|
||||
import { getDailyReminderSettings, getOrCreateClientUserId, getUserProfileScoring } from '../storage/appStorage';
|
||||
import type { UserProfileScoring } from '../storage/appStorage';
|
||||
import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
|
||||
|
||||
export type PushEnv = 'dev' | 'prod';
|
||||
|
||||
@@ -44,11 +45,7 @@ export type PushPreferencesResponse = PushPreferencesRequest & {
|
||||
};
|
||||
|
||||
export function buildAcceptLanguage(): 'en' | 'tc' {
|
||||
const lang = (i18n.language || '').trim();
|
||||
const lower = lang.toLowerCase();
|
||||
if (lower.startsWith('zh')) return 'tc';
|
||||
if (lower.includes('tc') || lower.includes('hant') || lower.includes('hk') || lower.includes('mo') || lower.includes('tw')) return 'tc';
|
||||
return 'en';
|
||||
return toBackendLocaleFromLanguageTag(i18n.language);
|
||||
}
|
||||
|
||||
function toPushEnv(appEnv: typeof APP_ENV): PushEnv {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import i18n from 'i18next';
|
||||
|
||||
import type { UserProfileV1_2 } from '../features/userProfileScoring';
|
||||
import { toBackendLocaleFromLanguageTag } from '../i18n/locale';
|
||||
import { httpJson } from '../utils/http';
|
||||
|
||||
export type RecommendedItem = {
|
||||
@@ -27,7 +28,7 @@ export type RecoRequest = {
|
||||
};
|
||||
|
||||
export async function fetchRecoFeed(req: RecoRequest): Promise<RecoEngineResult> {
|
||||
const acceptLanguage = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
|
||||
const acceptLanguage = toBackendLocaleFromLanguageTag(i18n.language);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
// 让后端做 locale 选择(目前后端只区分 en/tc)
|
||||
@@ -52,7 +53,7 @@ export async function fetchRecoFeed(req: RecoRequest): Promise<RecoEngineResult>
|
||||
}
|
||||
|
||||
export async function fetchRecoWidget(req: RecoRequest): Promise<RecoEngineResult> {
|
||||
const acceptLanguage = i18n.language?.toLowerCase().startsWith('zh') ? 'tc' : 'en';
|
||||
const acceptLanguage = toBackendLocaleFromLanguageTag(i18n.language);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
// 让后端做 locale 选择(目前后端只区分 en/tc)
|
||||
|
||||
Reference in New Issue
Block a user