小组件换行文案更新
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { MeasureWidthImpl } from './measure/types';
|
||||
import type { ScoreTerm } from './scoring/types';
|
||||
import type { Weights } from './scoring/types';
|
||||
|
||||
export type OverflowMode = 'ELLIPSIS' | 'CLIP' | 'SYSTEM_DEFAULT';
|
||||
export type LineMode = 'AUTO' | 'FIXED';
|
||||
@@ -26,6 +27,21 @@ export type WrapTextInput = {
|
||||
fontSpec?: Partial<FontSpecInput> | null;
|
||||
/** 可选注入测量实现(APP 场景强烈建议提供;WIDGET 默认不启用) */
|
||||
measureWidthImpl?: MeasureWidthImpl;
|
||||
/**
|
||||
* 可选评分偏好(用于 UI 场景“更好看”的排版风格)。
|
||||
* - 不传:使用算法默认 v1 权重与口径(更贴近文档 10.3)
|
||||
* - 传入:仅在本次 wrapText 调用内生效,不影响全局
|
||||
*/
|
||||
scoringOverrides?: {
|
||||
/** 覆盖/微调默认权重(整数)。建议只改少数项,例如 TC 的标点断行奖励。 */
|
||||
weights?: Partial<Weights>;
|
||||
/** 覆盖理想行宽比例(0~1)。更小会更倾向“提前换行”。 */
|
||||
idealWidthRatio?: Partial<Record<TextWrapContext, number>>;
|
||||
/** 最短偏好比例(相对 idealWidth)。更小会更宽容“短行”。 */
|
||||
minPreferredRatio?: number;
|
||||
/** 末行过短惩罚阈值比例(相对 idealWidth)。更小会更宽容“短末行”。 */
|
||||
shortLastLineRatio?: number;
|
||||
};
|
||||
/**
|
||||
* 测量缓存隔离 key(可选)。
|
||||
* 口径建议:`APP|ios|<scale?>` / `WIDGET|small`。
|
||||
|
||||
@@ -3,6 +3,7 @@ import { normalizeWhitespace, tokenizeEN } from './core/index';
|
||||
import { segmentGraphemes } from './grapheme/index';
|
||||
import { generateBreakpoints } from './breakpoints/index';
|
||||
import { DEFAULT_LEXICONS, DEFAULT_WEIGHTS } from './scoring/index';
|
||||
import { mergeWeights } from './scoring/weights';
|
||||
import { searchBestLayoutApp } from './searchApp/index';
|
||||
import { searchBestLayoutWidget } from './searchWidget/index';
|
||||
import { applyOverflowFallback } from './overflow/index';
|
||||
@@ -45,12 +46,23 @@ export async function wrapText(input: WrapTextInput): Promise<WrapTextOutput> {
|
||||
// scoring:protectedPhrases 从 constraints 注入
|
||||
const lexicons = { ...DEFAULT_LEXICONS, protectedPhrases: input.constraints?.protectedPhrases ?? DEFAULT_LEXICONS.protectedPhrases };
|
||||
const tcPunctuations: string[] = [',', '。', '!', '?', ';', ':', '、'];
|
||||
|
||||
// 评分偏好(可选):用于 UI 侧“更好看”的排版风格微调
|
||||
const scoringOverrides = input.scoringOverrides ?? null;
|
||||
const weights = scoringOverrides?.weights ? mergeWeights(scoringOverrides.weights) : DEFAULT_WEIGHTS;
|
||||
const idealWidthRatio = {
|
||||
APP: scoringOverrides?.idealWidthRatio?.APP ?? 0.9,
|
||||
WIDGET: scoringOverrides?.idealWidthRatio?.WIDGET ?? 0.95,
|
||||
};
|
||||
const scoringConfig = {
|
||||
weights: DEFAULT_WEIGHTS,
|
||||
idealWidthRatio: { APP: 0.9, WIDGET: 0.95 },
|
||||
weights,
|
||||
idealWidthRatio,
|
||||
ellipsisToken: '…',
|
||||
tcParticleWhitelist: [],
|
||||
tcPunctuations,
|
||||
// 下面两项默认由 score.ts 内部给出;此处仅在有 overrides 时注入
|
||||
minPreferredRatio: scoringOverrides?.minPreferredRatio,
|
||||
shortLastLineRatio: scoringOverrides?.shortLastLineRatio,
|
||||
};
|
||||
|
||||
// 搜索
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
## 应用文案总表(请在此文件对应的 JSON 中修改)
|
||||
## 應用文案總表(請在對應 JSON 中修改)
|
||||
|
||||
**单一文案源文件**:`client/src/i18n/locales/all.json`
|
||||
### 語言與檔案對應(單一來源,避免多檔覆蓋)
|
||||
|
||||
- **English**:`all.json` 的 `en`
|
||||
- **繁体中文**:`all.json` 的 `zh-TW`
|
||||
| 語言 | 實際使用的檔案 | 說明 |
|
||||
|------------|-----------------------------|------|
|
||||
| **英文** | `locales/all.json` 的 `en` | 只改 all.json 的 `en` 區塊 |
|
||||
| **繁體中文** | `locales/zh-TW.json` | **唯一來源**,Onboarding / 開屏 consent 等繁中文案只改此檔 |
|
||||
|
||||
> 说明:项目运行时只读取 `all.json`;请不要再改 `locales/en.json`、`locales/zh-TW.json`(它们已不再作为运行时数据源)。
|
||||
- 運行時 **繁中(zh-TW)只讀取 `zh-TW.json`**,不會讀取 `all.json` 的 zh-TW 區塊。
|
||||
- 修改 JSON 後需**重新載入 App**(模擬器 `Cmd+R`)或重啟 Metro(必要時加 `--clear`)才會看到新文案。
|
||||
- **若修改 zh-TW.json 後開屏 consent 仍顯示舊文案**:請執行 `npm run clean:cache`,再以 `npm run start:clean` 啟動 Metro(或先刪除模擬器上的 App 再執行 `npm run ios:clean`),確保吃到最新 bundle。
|
||||
|
||||
### 快速索引(高频文案)
|
||||
### 快速索引(高頻文案)
|
||||
|
||||
- **開屏 / 協議**:`consent.*`(**繁中只改 zh-TW.json**:`consent.title`、`consent.subtitle`、`consent.subtitleSecondary`、agree, privacy, terms, notice…)
|
||||
- **Onboarding 問卷**:`onboardingSurvey.steps.*`(name.title, status.title, status.options.* …)
|
||||
- **Onboarding 招呼語**:`onboardingSurvey.greeting`(例:Hi {{name}},)
|
||||
- **Home**:`home.*`
|
||||
- **Push 提示**:`push.*`
|
||||
- **主题**:`theme.*`
|
||||
- **我的/Profile**:`profile.*`
|
||||
- **主題**:`theme.*`
|
||||
- **我的 / Profile**:`profile.*`
|
||||
- **收藏**:`favorites.*`
|
||||
- **设置**:`settings.*`
|
||||
- **Onboarding(问卷)**:`onboardingSurvey.steps.*`
|
||||
- **Onboarding(兴趣)**:`intent.*`
|
||||
- **設定**:`settings.*`
|
||||
- **Mock 文案**:`mock.*`
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,12 @@ import { initReactI18next } from 'react-i18next';
|
||||
import { isTraditionalChineseLocaleTag } from './locale';
|
||||
|
||||
// 用 require 避免 TS 的 json module 配置差异导致无法编译
|
||||
// 繁中(zh-TW)唯一來源:locales/zh-TW.json,修改 Onboarding/開屏等繁中文案請只改該檔案
|
||||
// 本 App 未載入 zh-CN.json;若看到類似「你本就完美」「一切都会变好」等簡中 consent 文案,為舊 bundle 快取導致,請執行 clean:cache + start:clean 或卸載 App 重裝。
|
||||
// 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> };
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const zhTW = require('./locales/zh-TW.json') as Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 语言码约定:
|
||||
@@ -81,7 +85,8 @@ export async function initI18n(): Promise<void> {
|
||||
|
||||
await i18n.use(initReactI18next).init({
|
||||
resources: {
|
||||
'zh-TW': { translation: all['zh-TW'] as any },
|
||||
// 繁中唯一來源:zh-TW.json(all.json 的 zh-TW 區塊不會被載入)
|
||||
'zh-TW': { translation: zhTW },
|
||||
en: { translation: all.en as any },
|
||||
},
|
||||
lng: initialLang,
|
||||
@@ -91,6 +96,18 @@ export async function initI18n(): Promise<void> {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 臨時 debug:確認實際使用的 language 與 consent.title 值(方便驗證繁中來自 zh-TW.json)
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
const consentTitle = i18n.t('consent.title');
|
||||
console.log(
|
||||
'[i18n] 已初始化 language=',
|
||||
i18n.language,
|
||||
'| consent.title=',
|
||||
consentTitle,
|
||||
'| 繁中來源=zh-TW.json,英文來源=all.json 的 en 區塊'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Next",
|
||||
"skip": "Skip",
|
||||
"skipAll": "Skip onboarding",
|
||||
"skipAll": "Skip",
|
||||
"q1Title": "How are you feeling lately?",
|
||||
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
||||
"q2Title": "What kind of support do you want?",
|
||||
@@ -25,10 +25,11 @@
|
||||
"q4Desc": "You can skip. We’ll stay with you along the way."
|
||||
},
|
||||
"onboardingSurvey": {
|
||||
"greeting": "Hi {{name}},",
|
||||
"steps": {
|
||||
"name": { "title": "What do you want to be called?", "placeholder": "Mama" },
|
||||
"name": { "title": "What should we call you?", "placeholder": "Mama" },
|
||||
"status": {
|
||||
"title": "Which stage of motherhood are you in?",
|
||||
"title": "Where are you at right now?",
|
||||
"options": {
|
||||
"pregnant": "Pregnant / Preparing",
|
||||
"has_kids": "Parenting",
|
||||
@@ -66,7 +67,7 @@
|
||||
"balance": "Rest & balance"
|
||||
}
|
||||
},
|
||||
"reminder": { "title": "How many reminders do you want per day?" }
|
||||
"reminder": { "title": "How often would you like a gentle reminder?" }
|
||||
}
|
||||
},
|
||||
"intent": {
|
||||
@@ -87,7 +88,7 @@
|
||||
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"title": "Hey Mama",
|
||||
"like": "Like",
|
||||
"dislike": "Dislike",
|
||||
"favorites": "Favorites",
|
||||
@@ -115,6 +116,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "Daily Reminder",
|
||||
"timesUnit": "times",
|
||||
"timesUnitSingular": "time",
|
||||
"pushLabel": "Push Reminder",
|
||||
"ok": "Ok",
|
||||
"minus": "Decrease",
|
||||
@@ -125,7 +127,7 @@
|
||||
"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”.",
|
||||
"howToDesc2": "Search “Hey Mama”, 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."
|
||||
},
|
||||
@@ -139,16 +141,17 @@
|
||||
"language": "Language",
|
||||
"version": "Version",
|
||||
"widgetTitle": "iOS Widget",
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Mindfulness” → add a size you like."
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Hey Mama” → add a size you like."
|
||||
},
|
||||
"consent": {
|
||||
"title": "You Are Perfect.",
|
||||
"subtitle": "Everything\nWill Be Better.",
|
||||
"title": "Hey mama.",
|
||||
"subtitle": "You’re doing okay\nright now.",
|
||||
"subtitleSecondary": "",
|
||||
"agree": "Agree & Continue",
|
||||
"privacy": "Privacy Policy",
|
||||
"terms": "Terms of Use",
|
||||
"notice": "By continuing, you agree to the Privacy Policy and Terms of Use.",
|
||||
"noticeRich": "By continuing, you agree to the <privacy>{{privacyLabel}}{{privacySuffix}}</privacy> and <terms>{{termsLabel}}{{termsSuffix}}</terms>.",
|
||||
"noticeRich": "By continuing,\nyou agree to the <privacy>{{privacyLabel}}{{privacySuffix}}</privacy> and <terms>{{termsLabel}}{{termsSuffix}}</terms>.",
|
||||
"linkUnavailable": "Failed to load the policy link. Please check your network and try again.",
|
||||
"linkUnavailableDev": "Failed to load the policy link. Please check your network or API_BASE_URL: {{baseUrl}}",
|
||||
"linkLoadingSuffix": " (loading…)"
|
||||
@@ -183,7 +186,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "下一步",
|
||||
"skip": "跳過",
|
||||
"skipAll": "跳過整個引導",
|
||||
"skipAll": "跳過",
|
||||
"q1Title": "你最近的感受更接近哪一種?",
|
||||
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
||||
"q2Title": "你更希望獲得哪種支持?",
|
||||
@@ -194,18 +197,19 @@
|
||||
"q4Desc": "你可以直接跳過,我們會在之後繼續陪你。"
|
||||
},
|
||||
"onboardingSurvey": {
|
||||
"greeting": "Hi {{name}},",
|
||||
"steps": {
|
||||
"name": { "title": "我可以怎麼稱呼你?", "placeholder": "媽媽" },
|
||||
"name": { "title": "怎麼稱呼你呢?", "placeholder": "媽媽" },
|
||||
"status": {
|
||||
"title": "媽媽的狀態?",
|
||||
"title": "你現在正處在哪個階段呢?",
|
||||
"options": {
|
||||
"pregnant": "懷孕中/準備成為媽媽",
|
||||
"pregnant": "懷孕中/正在準備迎接寶寶",
|
||||
"has_kids": "已經有孩子",
|
||||
"no_fill": "不想填寫"
|
||||
"no_fill": "我暫時不想說"
|
||||
}
|
||||
},
|
||||
"emotion": {
|
||||
"title": "當下情緒狀態?",
|
||||
"title": "今天的你,還好嗎?",
|
||||
"options": {
|
||||
"happy": "愉悅、滿足",
|
||||
"calm": "平靜、安穩",
|
||||
@@ -256,7 +260,7 @@
|
||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||
},
|
||||
"home": {
|
||||
"title": "正念",
|
||||
"title": "Hey Mama",
|
||||
"like": "喜歡",
|
||||
"dislike": "不喜歡",
|
||||
"favorites": "收藏",
|
||||
@@ -284,6 +288,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "每日提醒",
|
||||
"timesUnit": "次",
|
||||
"timesUnitSingular": "次",
|
||||
"pushLabel": "推送提醒",
|
||||
"ok": "確定",
|
||||
"minus": "減少次數",
|
||||
@@ -292,9 +297,9 @@
|
||||
"widget": {
|
||||
"lockScreen": "鎖屏小工具",
|
||||
"homeScreen": "桌面小工具",
|
||||
"howToTitle": "如何添加小工具",
|
||||
"howToTitle": "如何加入小工具",
|
||||
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||
"howToDesc2": "搜尋「正念」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||
"howToDesc2": "搜尋「Hey Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||
},
|
||||
@@ -308,11 +313,12 @@
|
||||
"language": "語言",
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小工具",
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Hey Mama」 → 添加你喜歡的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"title": "你很完美。",
|
||||
"subtitle": "一切\n都會更好。",
|
||||
"title": "我們知道,",
|
||||
"subtitle": "當媽媽很不容易。",
|
||||
"subtitleSecondary": "這裡給你一些溫柔的肯定與提醒",
|
||||
"agree": "同意並繼續",
|
||||
"privacy": "隱私協議",
|
||||
"terms": "用戶使用協議",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Next",
|
||||
"skip": "Skip",
|
||||
"skipAll": "Skip onboarding",
|
||||
"skipAll": "Skip",
|
||||
"q1Title": "How are you feeling lately?",
|
||||
"q1Desc": "No right or wrong. You can skip and adjust later.",
|
||||
"q2Title": "What kind of support do you want?",
|
||||
@@ -32,7 +32,7 @@
|
||||
"errorDesc": "It’s okay if enabling fails. You can keep using the app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"title": "Hey Mama",
|
||||
"like": "Like",
|
||||
"dislike": "Dislike",
|
||||
"favorites": "Favorites",
|
||||
@@ -59,6 +59,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "Daily Reminder",
|
||||
"timesUnit": "times",
|
||||
"timesUnitSingular": "time",
|
||||
"pushLabel": "Push Reminder",
|
||||
"ok": "Ok",
|
||||
"minus": "Decrease",
|
||||
@@ -79,7 +80,7 @@
|
||||
"language": "Language",
|
||||
"version": "Version",
|
||||
"widgetTitle": "iOS Widget",
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Mindfulness” → add a size you like."
|
||||
"widgetDesc": "Put gentle reminders on your home screen: long-press → tap “+” → search “Hey Mama” → add a size you like."
|
||||
},
|
||||
"consent": {
|
||||
"title": "You Are Perfect.",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Siguiente",
|
||||
"skip": "Saltar",
|
||||
"skipAll": "Saltar introducción",
|
||||
"skipAll": "Saltar",
|
||||
"q1Title": "¿Cómo te sientes últimamente?",
|
||||
"q1Desc": "No hay respuestas correctas. Puedes saltar y ajustar después.",
|
||||
"q2Title": "¿Qué tipo de apoyo quieres?",
|
||||
@@ -30,7 +30,7 @@
|
||||
"errorDesc": "No pasa nada si falla. Puedes seguir usando la app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"title": "Hey Mama",
|
||||
"like": "Me gusta",
|
||||
"dislike": "No me gusta",
|
||||
"favorites": "Favoritos",
|
||||
@@ -57,6 +57,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "Recordatorio diario",
|
||||
"timesUnit": "veces",
|
||||
"timesUnitSingular": "vez",
|
||||
"pushLabel": "Recordatorio Push",
|
||||
"ok": "Ok",
|
||||
"minus": "Disminuir",
|
||||
@@ -77,7 +78,7 @@
|
||||
"language": "Idioma",
|
||||
"version": "Versión",
|
||||
"widgetTitle": "Widget de iOS",
|
||||
"widgetDesc": "Pon recordatorios en tu pantalla: mantén pulsado → “+” → busca “Mindfulness” → añade el tamaño."
|
||||
"widgetDesc": "Pon recordatorios en tu pantalla: mantén pulsado → “+” → busca “Hey Mama” → añade el tamaño."
|
||||
},
|
||||
"consent": {
|
||||
"agree": "Aceptar y Continuar",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "Próximo",
|
||||
"skip": "Pular",
|
||||
"skipAll": "Pular introdução",
|
||||
"skipAll": "Pular",
|
||||
"q1Title": "Como você tem se sentido ultimamente?",
|
||||
"q1Desc": "Não há certo ou errado. Você pode pular e ajustar depois.",
|
||||
"q2Title": "Que tipo de apoio você quer?",
|
||||
@@ -30,7 +30,7 @@
|
||||
"errorDesc": "Tudo bem se falhar. Você pode continuar usando o app."
|
||||
},
|
||||
"home": {
|
||||
"title": "Mindfulness",
|
||||
"title": "Hey Mama",
|
||||
"like": "Curtir",
|
||||
"dislike": "Não curtir",
|
||||
"favorites": "Favoritos",
|
||||
@@ -57,6 +57,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "Lembrete diário",
|
||||
"timesUnit": "vezes",
|
||||
"timesUnitSingular": "vez",
|
||||
"pushLabel": "Lembrete Push",
|
||||
"ok": "Ok",
|
||||
"minus": "Diminuir",
|
||||
@@ -77,7 +78,7 @@
|
||||
"language": "Idioma",
|
||||
"version": "Versão",
|
||||
"widgetTitle": "Widget do iOS",
|
||||
"widgetDesc": "Coloque lembretes na tela inicial: pressione e segure → “+” → procure “Mindfulness” → adicione o tamanho."
|
||||
"widgetDesc": "Coloque lembretes na tela inicial: pressione e segure → “+” → procure “Hey Mama” → adicione o tamanho."
|
||||
},
|
||||
"consent": {
|
||||
"agree": "Concordar e Continuar",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "下一步",
|
||||
"skip": "跳过",
|
||||
"skipAll": "跳过整个引导",
|
||||
"skipAll": "跳过",
|
||||
"q1Title": "你最近的感受更接近哪一种?",
|
||||
"q1Desc": "没有对错,你可以跳过,之后也可以慢慢调整。",
|
||||
"q2Title": "你更希望获得哪种支持?",
|
||||
@@ -33,7 +33,7 @@
|
||||
"errorDesc": "开启失败,请稍后重试(模拟器可能无法获取推送 Token,建议用真机测试)。"
|
||||
},
|
||||
"home": {
|
||||
"title": "正念",
|
||||
"title": "Hey Mama",
|
||||
"like": "点赞",
|
||||
"dislike": "讨厌",
|
||||
"favorites": "收藏",
|
||||
@@ -60,6 +60,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "每日提醒",
|
||||
"timesUnit": "次",
|
||||
"timesUnitSingular": "次",
|
||||
"pushLabel": "推送提醒",
|
||||
"ok": "确定",
|
||||
"minus": "减少次数",
|
||||
@@ -80,7 +81,7 @@
|
||||
"language": "语言",
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小组件",
|
||||
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“正念” → 添加你喜欢的尺寸。"
|
||||
"widgetDesc": "把温柔提醒放到桌面上:长按主屏幕 → 点“+” → 搜索“Hey Mama” → 添加你喜欢的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"title": "你本就完美。",
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
"common": {
|
||||
"ok": "確定",
|
||||
"cancel": "取消",
|
||||
"back": "返回"
|
||||
"back": "返回",
|
||||
"error": "錯誤",
|
||||
"notice": "提示",
|
||||
"openLinkError": "無法打開鏈接",
|
||||
"close": "關閉"
|
||||
},
|
||||
"onboarding": {
|
||||
"title": "歡迎",
|
||||
"progress": "{{current}}/{{total}}",
|
||||
"next": "下一步",
|
||||
"skip": "跳過",
|
||||
"skipAll": "跳過整個引導",
|
||||
"skipAll": "跳過",
|
||||
"q1Title": "你最近的感受更接近哪一種?",
|
||||
"q1Desc": "沒有對錯,你可以跳過,之後也能慢慢調整。",
|
||||
"q2Title": "你更希望獲得哪種支持?",
|
||||
@@ -19,6 +23,64 @@
|
||||
"q4Title": "給自己一句溫柔的話",
|
||||
"q4Desc": "你可以直接跳過,我們會在之後繼續陪你。"
|
||||
},
|
||||
"onboardingSurvey": {
|
||||
"greeting": "Hi {{name}},",
|
||||
"steps": {
|
||||
"name": {
|
||||
"title": "怎麼稱呼你呢?",
|
||||
"placeholder": "媽媽"
|
||||
},
|
||||
"status": {
|
||||
"title": "你現在正處在哪個階段呢?",
|
||||
"options": {
|
||||
"pregnant": "懷孕中/正在準備迎接寶寶",
|
||||
"has_kids": "已經有孩子",
|
||||
"no_fill": "我暫時不想說"
|
||||
}
|
||||
},
|
||||
"emotion": {
|
||||
"title": "今天的你,還好嗎?",
|
||||
"options": {
|
||||
"happy": "愉悅、滿足",
|
||||
"calm": "平靜、安穩",
|
||||
"okay": "還可以、普通",
|
||||
"tired": "疲累、沒什麼力氣",
|
||||
"stressed": "被壓得有點喘不過氣",
|
||||
"low": "情緒低落"
|
||||
}
|
||||
},
|
||||
"influence": {
|
||||
"title": "是什麼影響了你最近的感受?",
|
||||
"options": {
|
||||
"family": "家庭與孩子",
|
||||
"work": "工作或學習",
|
||||
"relationship": "親密關係",
|
||||
"friends": "朋友與人際",
|
||||
"health": "身心健康"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
"title": "最需要什麼支持?",
|
||||
"options": {
|
||||
"emotional": "情緒支持",
|
||||
"parenting": "育兒壓力",
|
||||
"self_worth": "自我價值",
|
||||
"anxiety": "焦慮舒緩",
|
||||
"balance": "休息與平衡"
|
||||
}
|
||||
},
|
||||
"reminder": {
|
||||
"title": "你希望一天收到幾次肯定語?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"intent": {
|
||||
"title": "你希望得到什麼幫助?",
|
||||
"love": "愛情",
|
||||
"life": "生活",
|
||||
"travel": "旅遊",
|
||||
"work": "職場"
|
||||
},
|
||||
"push": {
|
||||
"title": "通知",
|
||||
"cardTitle": "開啟溫柔提醒",
|
||||
@@ -30,7 +92,7 @@
|
||||
"errorDesc": "開啟失敗也沒關係,你仍然可以繼續使用應用。"
|
||||
},
|
||||
"home": {
|
||||
"title": "正念",
|
||||
"title": "Hey Mama",
|
||||
"like": "喜歡",
|
||||
"dislike": "不喜歡",
|
||||
"favorites": "收藏",
|
||||
@@ -41,7 +103,8 @@
|
||||
"theme": {
|
||||
"title": "主題",
|
||||
"scenery": "風景",
|
||||
"color": "顏色"
|
||||
"color": "顏色",
|
||||
"suixin": "隨心"
|
||||
},
|
||||
"profile": {
|
||||
"title": "我的",
|
||||
@@ -57,6 +120,7 @@
|
||||
"dailyReminder": {
|
||||
"title": "每日提醒",
|
||||
"timesUnit": "次",
|
||||
"timesUnitSingular": "次",
|
||||
"pushLabel": "推送提醒",
|
||||
"ok": "確定",
|
||||
"minus": "減少次數",
|
||||
@@ -65,24 +129,49 @@
|
||||
"widget": {
|
||||
"lockScreen": "鎖屏小工具",
|
||||
"homeScreen": "桌面小工具",
|
||||
"howToTitle": "如何加入小工具",
|
||||
"howToDesc1": "長按主畫面空白處進入編輯,點左上角「+」新增小工具。",
|
||||
"howToDesc2": "搜尋「Hey Mama」,選擇喜歡的尺寸,點「加入小工具」。",
|
||||
"previewDate": "1月29日週四 · 已至臘月十一",
|
||||
"previewQuote": "我也對現在的自己感到滿意,即使我仍在努力成為想成為的人。"
|
||||
},
|
||||
"favorites": {
|
||||
"title": "收藏夾",
|
||||
"empty": "這裡還沒有收藏內容。"
|
||||
"empty": "這裡還沒有收藏內容。",
|
||||
"unknownText": "這條文案暫時無法顯示。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"language": "語言",
|
||||
"version": "版本",
|
||||
"widgetTitle": "iOS 小工具",
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「正念」 → 添加你喜歡的尺寸。"
|
||||
"widgetDesc": "把溫柔提醒放到桌面上:長按主畫面 → 點「+」 → 搜尋「Hey Mama」 → 添加你喜歡的尺寸。"
|
||||
},
|
||||
"consent": {
|
||||
"title": "我們知道,",
|
||||
"subtitle": "當媽媽很不容易。",
|
||||
"subtitleSecondary": "這裡給你一些溫柔的肯定與提醒",
|
||||
"agree": "同意並繼續",
|
||||
"privacy": "隱私協議",
|
||||
"terms": "用戶使用協議"
|
||||
"terms": "用戶使用協議",
|
||||
"notice": "繼續使用即代表你同意《隱私協議》與《用戶使用協議》。",
|
||||
"noticeRich": "繼續使用即代表你同意<privacy>《{{privacyLabel}}》{{privacySuffix}}</privacy>與<terms>《{{termsLabel}}》{{termsSuffix}}</terms>。",
|
||||
"linkUnavailable": "協議鏈接載入失敗,請檢查網路後重試。",
|
||||
"linkUnavailableDev": "協議鏈接載入失敗,請檢查網路或 API_BASE_URL 設定:{{baseUrl}}",
|
||||
"linkLoadingSuffix": "(載入中…)"
|
||||
},
|
||||
"permissions": {
|
||||
"notificationsDenied": "系統權限已被拒絕,請前往手機設定開啟通知。"
|
||||
},
|
||||
"language": {
|
||||
"zhTW": "繁體中文",
|
||||
"en": "English"
|
||||
},
|
||||
"mock": {
|
||||
"c1": "你已經很努力了,今天也值得被溫柔對待。",
|
||||
"c2": "深呼吸三次,把注意力帶回當下。",
|
||||
"c3": "允許自己慢一點,情緒會像雲一樣飄過。",
|
||||
"c4": "你不需要完美,你已經足夠好。",
|
||||
"c5": "把手放在心口,對自己說一句:辛苦了。"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { fetchRecoWidget } from '@/src/services/recoApi';
|
||||
import i18n from 'i18next';
|
||||
import { getUserProfileScoring } from '@/src/storage/appStorage';
|
||||
import { getLocalDayKey } from '@/src/utils/date';
|
||||
import { wrapText } from '@/src/features/textWrap';
|
||||
|
||||
import {
|
||||
appGroupGetString,
|
||||
@@ -40,6 +41,12 @@ export type WidgetDailyRecoV1 = {
|
||||
item: null | {
|
||||
content_id: number;
|
||||
text: string;
|
||||
/**
|
||||
* 预换行文案(由 App 侧使用 Text Wrap 算法生成,写入 App Group,供 Widget 直接渲染)。
|
||||
* - key 以 WidgetFamily 归一化:small/medium/large
|
||||
* - 值使用 `\n` 分行;Widget SwiftUI 的 Text 会按换行符显示
|
||||
*/
|
||||
wrapped_text_by_family?: Partial<Record<'small' | 'medium' | 'large', string>>;
|
||||
final_score?: number;
|
||||
fallback_level_final?: number;
|
||||
};
|
||||
@@ -56,6 +63,58 @@ function safeJsonParse<T>(raw: string | null): T | null {
|
||||
}
|
||||
}
|
||||
|
||||
type WidgetFamilyKey = 'small' | 'medium' | 'large';
|
||||
|
||||
function widgetPreset(family: WidgetFamilyKey) {
|
||||
// 与 iOS Widget(`EmotionWidget.swift`)保持一致的显示口径(字体/行数/内边距)
|
||||
// 注意:这里的 widthPt 是“常见 iPhone widget 尺寸”的近似值,用于把 pt 宽度换算为“可容纳 token 数”
|
||||
// 真实渲染仍由系统决定,但这个近似能让算法在不同 family 下更稳定地产出更好看的换行。
|
||||
switch (family) {
|
||||
case 'small':
|
||||
return { widthPt: 155, padding: 14, fontSize: 16, maxLines: 5 };
|
||||
case 'medium':
|
||||
return { widthPt: 329, padding: 16, fontSize: 18, maxLines: 6 };
|
||||
case 'large':
|
||||
return { widthPt: 329, padding: 18, fontSize: 22, maxLines: 8 };
|
||||
}
|
||||
}
|
||||
|
||||
function approxCapacityFromPt(args: { lang: 'EN' | 'TC'; usablePt: number; fontSize: number }): number {
|
||||
// 与 wrapText(APP 近似降级) 的换算口径一致:把像素/pt 宽度近似换成“可容纳 token 数”
|
||||
const usable = Math.max(0, args.usablePt);
|
||||
const fs = Math.max(1, Math.round(args.fontSize));
|
||||
const denom = args.lang === 'EN' ? Math.max(1, Math.round(fs * 0.55)) : Math.max(1, Math.round(fs * 0.95));
|
||||
return Math.max(1, Math.floor(usable / denom));
|
||||
}
|
||||
|
||||
async function buildWrappedTextByFamily(args: { text: string; lang: 'EN' | 'TC' }): Promise<Record<WidgetFamilyKey, string>> {
|
||||
const families: WidgetFamilyKey[] = ['small', 'medium', 'large'];
|
||||
const out: Partial<Record<WidgetFamilyKey, string>> = {};
|
||||
|
||||
for (const f of families) {
|
||||
const p = widgetPreset(f);
|
||||
const usablePt = Math.max(0, p.widthPt - p.padding * 2);
|
||||
const capacity = approxCapacityFromPt({ lang: args.lang, usablePt, fontSize: p.fontSize });
|
||||
|
||||
const res = await wrapText({
|
||||
text: args.text,
|
||||
lang: args.lang,
|
||||
context: 'WIDGET',
|
||||
availableWidth: capacity,
|
||||
maxLines: p.maxLines,
|
||||
overflowMode: 'ELLIPSIS',
|
||||
lineMode: 'AUTO',
|
||||
configVersion: 'v1-widget',
|
||||
debug: false,
|
||||
// Widget 侧不依赖真实测量:默认 APPROX 即可(确保可在 Extension 独立工作)
|
||||
});
|
||||
|
||||
out[f] = res.wrappedText;
|
||||
}
|
||||
|
||||
return out as Record<WidgetFamilyKey, string>;
|
||||
}
|
||||
|
||||
function pickUserProfileV1_2(scoringProfile: UserProfileV1_2_Extended): UserProfileV1_2 {
|
||||
return {
|
||||
profile_version: scoringProfile.profile_version,
|
||||
@@ -126,7 +185,29 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
||||
|
||||
const today = getLocalDayKey(new Date());
|
||||
const cached = await getWidgetDailyRecoCache();
|
||||
if (cached?.schema_version === 1 && cached.day_key === today && cached.item?.text) return;
|
||||
// 若今日已有缓存,但缺少预换行字段:补齐后触发 reload(不必请求后端)
|
||||
if (cached?.schema_version === 1 && cached.day_key === today && cached.item?.text) {
|
||||
const hasWrapped = Boolean(cached.item.wrapped_text_by_family && Object.keys(cached.item.wrapped_text_by_family).length > 0);
|
||||
if (hasWrapped) return;
|
||||
|
||||
const wrapLang: 'EN' | 'TC' = cached.lang === 'en' ? 'EN' : 'TC';
|
||||
try {
|
||||
const wrapped = await buildWrappedTextByFamily({ text: cached.item.text, lang: wrapLang });
|
||||
await setWidgetDailyRecoCache({
|
||||
...cached,
|
||||
saved_at: new Date().toISOString(),
|
||||
item: { ...cached.item, wrapped_text_by_family: wrapped },
|
||||
source: cached.source ?? 'app',
|
||||
});
|
||||
await appGroupReloadAllTimelines();
|
||||
} catch (e) {
|
||||
// 预换行失败不阻塞;Widget 仍可用原文 + 系统换行
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
console.log('[DailyWidgetReco] 预换行补齐失败:', args?.reason ?? 'unknown', e);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const scoringProfile = args?.scoringProfile ?? (await getUserProfileScoring());
|
||||
if (!scoringProfile) return;
|
||||
@@ -146,6 +227,8 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
||||
if (!top?.text) return;
|
||||
|
||||
const lang = toBackendLocaleFromLanguageTag(i18n.language);
|
||||
const wrapLang: 'EN' | 'TC' = lang === 'en' ? 'EN' : 'TC';
|
||||
const wrapped = await buildWrappedTextByFamily({ text: top.text, lang: wrapLang });
|
||||
await setWidgetDailyRecoCache({
|
||||
schema_version: 1,
|
||||
saved_at: new Date().toISOString(),
|
||||
@@ -155,6 +238,7 @@ export async function ensureDailyWidgetRecoUpToDate(args?: {
|
||||
item: {
|
||||
content_id: top.content_id,
|
||||
text: top.text,
|
||||
wrapped_text_by_family: wrapped,
|
||||
final_score: top.final_score,
|
||||
fallback_level_final: top.fallback_level_final,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user