Add zh-CN translation, update I18nManager, and refine formatting for localized UI and serving units

This commit is contained in:
James Clifford Spratt 2025-07-10 17:47:58 +01:00
parent 0cc2abe444
commit 9ae2844453
7 changed files with 1318 additions and 1064 deletions

View file

@ -211,39 +211,44 @@ export class I18nManager {
*/
private detectUserLocale(): string {
try {
// Get locale from Obsidian's native settings
// Get locale from Obsidian's native settings using safe methods
const obsidianLocale =
this.getMomentLocale() || // Use existing safe method
document.documentElement.lang ||
(this.app as any).vault?.config?.userInterfaceMode ||
this.getMomentLocale() ||
(this.app as any).locale;
if (obsidianLocale && this.isLocaleSupported(obsidianLocale)) {
this.plugin.logger.debug(`Detected Obsidian locale: ${obsidianLocale}`);
return obsidianLocale;
}
this.plugin.logger.debug(`Detected Obsidian locale: ${obsidianLocale}`);
// If Obsidian locale isn't supported, try language code only
if (obsidianLocale) {
const languageCode = obsidianLocale.split('-')[0];
// Handle Chinese locale variants - map both to zh-CN
if (obsidianLocale.toLowerCase().startsWith('zh')) {
this.plugin.logger.debug(`Mapping Chinese variant ${obsidianLocale} to zh-CN`);
return 'zh-CN';
}
// Check if exactly supported
if (this.isLocaleSupported(obsidianLocale)) {
return obsidianLocale;
}
// Try language code only (e.g., en-GB -> en)
const languageCode = obsidianLocale.split('-')[0].toLowerCase();
if (this.isLocaleSupported(languageCode)) {
this.plugin.logger.debug(`Using language code from Obsidian locale: ${languageCode}`);
this.plugin.logger.debug(`Using language code: ${languageCode}`);
return languageCode;
}
}
// Fall back to browser locale as last resort
// Fallback to browser locale
const browserLocale = navigator.language || navigator.languages?.[0];
if (browserLocale) {
// Try exact match first
if (this.isLocaleSupported(browserLocale)) {
this.plugin.logger.debug(`Using browser locale: ${browserLocale}`);
return browserLocale;
}
// Try language code only (e.g., 'en' from 'en-US')
const languageCode = browserLocale.split('-')[0];
if (this.isLocaleSupported(languageCode)) {
this.plugin.logger.debug(`Using language code from browser: ${languageCode}`);
return languageCode;
}
}

View file

@ -1,4 +1,4 @@
import MacrosPlugin from './../../main';
import MacrosPlugin from '../main';
import { Chart, ChartConfiguration, ChartDataset, TooltipItem, registerables } from 'chart.js';
import { ChartLoader, MacrosState, DOMUtils } from '../utils';
import {

View file

@ -13,6 +13,7 @@ import {
ProgressBarFactory,
} from '../../../utils';
import { parseGrams } from '../../../utils/parsingUtils';
import { formatServing } from '../../../utils/formatters';
import { Notice, Modal, ButtonComponent } from 'obsidian';
import { t } from '../../../lang/I18nManager';
@ -585,7 +586,7 @@ export class RowRenderer {
const quantityCell = r.insertCell();
quantityCell.classList.add('editable-quantity');
safeAttachTooltip(quantityCell, t('table.actions.clickToEdit'), this.plugin);
quantityCell.textContent = row.serving;
quantityCell.textContent = formatServing(row.serving);
const servingValue = parseGrams(row.serving);

View file

@ -179,8 +179,9 @@ export class MacrosDashboard {
text: value,
});
// Create custom tooltip with proper energy unit handling
// Create custom tooltip with proper energy unit handling and Chinese word order
let tooltipMessage: string;
const currentLocale = this.plugin.i18nManager.getCurrentLocale();
if (label === t('table.headers.calories')) {
const currentUnit = this.plugin.settings.energyUnit;
@ -193,12 +194,20 @@ export class MacrosDashboard {
const remainingKj = targetKj - consumedKj;
const percentage = targetKj > 0 ? (consumedKj / targetKj) * 100 : 0;
tooltipMessage = `${consumedKj.toFixed(1)} kJ • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remainingKj > 0) {
tooltipMessage += `${remainingKj.toFixed(1)} kJ ${t('general.remaining')}`;
} else if (remainingKj < 0) {
tooltipMessage += `${Math.abs(remainingKj).toFixed(1)} kJ ${t('table.summary.over')}`;
if (currentLocale === 'zh-CN') {
tooltipMessage = `${consumedKj.toFixed(1)} kJ • 占 ${Math.round(percentage)}%`;
if (remainingKj > 0) {
tooltipMessage += ` • 剩余 ${remainingKj.toFixed(1)} kJ`;
} else if (remainingKj < 0) {
tooltipMessage += ` • 超出 ${Math.abs(remainingKj).toFixed(1)} kJ`;
}
} else {
tooltipMessage = `${consumedKj.toFixed(1)} kJ • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remainingKj > 0) {
tooltipMessage += `${remainingKj.toFixed(1)} kJ ${t('general.remaining')}`;
} else if (remainingKj < 0) {
tooltipMessage += `${Math.abs(remainingKj).toFixed(1)} kJ ${t('table.summary.over')}`;
}
}
} else {
// Use original kcal values for kcal display
@ -207,18 +216,39 @@ export class MacrosDashboard {
const remainingKcal = targetKcal - consumedKcal;
const percentage = targetKcal > 0 ? (consumedKcal / targetKcal) * 100 : 0;
tooltipMessage = `${consumedKcal.toFixed(1)} kcal • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remainingKcal > 0) {
tooltipMessage += `${remainingKcal.toFixed(1)} kcal ${t('general.remaining')}`;
} else if (remainingKcal < 0) {
tooltipMessage += `${Math.abs(remainingKcal).toFixed(1)} kcal ${t('table.summary.over')}`;
if (currentLocale === 'zh-CN') {
tooltipMessage = `${consumedKcal.toFixed(1)} kcal • 占 ${Math.round(percentage)}%`;
if (remainingKcal > 0) {
tooltipMessage += ` • 剩余 ${remainingKcal.toFixed(1)} kcal`;
} else if (remainingKcal < 0) {
tooltipMessage += ` • 超出 ${Math.abs(remainingKcal).toFixed(1)} kcal`;
}
} else {
tooltipMessage = `${consumedKcal.toFixed(1)} kcal • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remainingKcal > 0) {
tooltipMessage += `${remainingKcal.toFixed(1)} kcal ${t('general.remaining')}`;
} else if (remainingKcal < 0) {
tooltipMessage += `${Math.abs(remainingKcal).toFixed(1)} kcal ${t('table.summary.over')}`;
}
}
}
} else {
// Use the standard formatDashboardTooltip for non-calorie metrics
const numericValue = parseFloat(value.replace('g', ''));
tooltipMessage = formatDashboardTooltip(numericValue, target, label);
if (currentLocale === 'zh-CN') {
const percentage = target > 0 ? (numericValue / target) * 100 : 0;
const remaining = target - numericValue;
tooltipMessage = `${numericValue.toFixed(1)} g ${label.toLowerCase()} • 占 ${Math.round(percentage)}%`;
if (remaining > 0) {
tooltipMessage += ` • 剩余 ${remaining.toFixed(1)} g`;
} else if (remaining < 0) {
tooltipMessage += ` • 超出 ${Math.abs(remaining).toFixed(1)} g`;
}
} else {
tooltipMessage = formatDashboardTooltip(numericValue, target, label);
}
}
safeAttachTooltip(card, tooltipMessage, this.plugin);

View file

@ -83,11 +83,11 @@ export class ManualFoodEntryModal extends Modal {
// Energy Fields Container (kcal and kJ side by side)
const energyContainer = nutritionContainer.createDiv({ cls: 'energy-fields-container' });
// Calories Field
// Calories Field with bold label
const caloriesGroup = energyContainer.createDiv({ cls: 'form-group energy-field' });
caloriesGroup.createEl('label', {
text: t('food.manual.calories'),
cls: 'form-label required',
cls: 'form-label required energy-label-bold',
});
this.caloriesInput = caloriesGroup.createEl('input', {
type: 'number',
@ -100,11 +100,11 @@ export class ManualFoodEntryModal extends Modal {
},
});
// kJ Field
// kJ Field with bold label
const kjGroup = energyContainer.createDiv({ cls: 'form-group energy-field' });
kjGroup.createEl('label', {
text: t('food.manual.energy') + ' (kJ)',
cls: 'form-label',
cls: 'form-label energy-label-bold',
});
this.kjInput = kjGroup.createEl('input', {
type: 'number',

View file

@ -20,14 +20,25 @@ function getCurrentEnergyUnit(): 'kcal' | 'kJ' {
return formatterPlugin?.settings?.energyUnit || 'kcal';
}
/**
* Get the current locale from the plugin
*/
function getCurrentLocale(): string {
try {
return formatterPlugin?.i18nManager?.getCurrentLocale() || 'en';
} catch {
return 'en';
}
}
/**
* Format calories with proper unit conversion based on settings
* @param calories Calorie value in kcal
* @returns Formatted string with appropriate unit
* @returns Formatted string with appropriate unit (with proper spacing)
*/
export function formatCalories(calories: number): string {
if (!formatterPlugin) {
// Fallback if plugin not set
// Fallback if plugin not set - with proper spacing
return `${calories.toFixed(1)} kcal`;
}
@ -35,19 +46,38 @@ export function formatCalories(calories: number): string {
if (currentUnit === 'kJ') {
const kj = convertEnergyUnit(calories, 'kcal', 'kJ');
return `${kj.toFixed(1)} kJ`;
return `${kj.toFixed(1)} kJ`; // Proper spacing (already correct)
} else {
return `${calories.toFixed(1)} kcal`;
return `${calories.toFixed(1)} kcal`; // Proper spacing (already correct)
}
}
/**
* Format grams with one decimal place
* Format grams with one decimal place and proper spacing
* @param grams Gram value
* @returns Formatted string
* @returns Formatted string with space between value and unit
*/
export function formatGrams(grams: number): string {
return `${grams.toFixed(1)}g`;
return `${grams.toFixed(1)} g`; // Added space for consistency with energy units
}
/**
* Format weight with proper spacing (following NIST standards)
* @param weight Weight value in grams
* @returns Formatted string with space between value and unit
*/
export function formatWeight(weight: number): string {
return `${weight.toFixed(1)} g`; // Added space between number and unit
}
/**
* Format macronutrient values with proper spacing
* @param value Numeric value
* @param unit Unit string (default: 'g')
* @returns Formatted string with space between value and unit
*/
export function formatMacro(value: number, unit: string = 'g'): string {
return `${value.toFixed(1)} ${unit}`; // Added space between number and unit
}
/**
@ -59,12 +89,34 @@ export function formatPercentage(percentage: number): string {
return Math.round(percentage).toString();
}
/**
* Format tooltip text with locale-specific word order and proper spacing
* Chinese: 7% · 95.2 g
* Others: 7% of target · 95.2 g remaining
*/
export function formatTooltipText(
percentage: number,
remaining: number,
unit: string,
locale?: string
): string {
const currentLocale = locale || getCurrentLocale();
if (currentLocale === 'zh-CN') {
// Chinese word order with proper spacing: 占 7% · 剩余 95.2 g
return `${Math.round(percentage)}% · 剩余 ${remaining.toFixed(1)} ${unit}`;
} else {
// Default word order with proper spacing: 7% of target · 95.2 g remaining
return `${Math.round(percentage)}% ${t('tooltips.dailyTarget')} · ${remaining.toFixed(1)} ${unit} ${t('tooltips.remaining')}`;
}
}
/**
* Generate tooltip text for dashboard metric cards
* @param currentValue Current consumed value
* @param targetValue Target value
* @param macroName Name of the macro (for translation)
* @returns Formatted tooltip string
* @returns Formatted tooltip string with proper spacing
*/
export function formatDashboardTooltip(
currentValue: number,
@ -73,16 +125,31 @@ export function formatDashboardTooltip(
): string {
const percentage = targetValue > 0 ? (currentValue / targetValue) * 100 : 0;
const remaining = targetValue - currentValue;
const currentLocale = getCurrentLocale();
let tooltipText = `${currentValue.toFixed(1)}g ${macroName.toLowerCase()}${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (currentLocale === 'zh-CN') {
// Chinese word order: 占 7% · 剩余 95.2 g
let tooltipText = `${currentValue.toFixed(1)} g ${macroName.toLowerCase()} • 占 ${Math.round(percentage)}%`;
if (remaining > 0) {
tooltipText += `${remaining.toFixed(1)}g ${t('general.remaining')}`;
} else if (remaining < 0) {
tooltipText += `${Math.abs(remaining).toFixed(1)}g ${t('table.summary.over')}`;
if (remaining > 0) {
tooltipText += ` • 剩余 ${remaining.toFixed(1)} g`;
} else if (remaining < 0) {
tooltipText += ` • 超出 ${Math.abs(remaining).toFixed(1)} g`;
}
return tooltipText;
} else {
// English word order
let tooltipText = `${currentValue.toFixed(1)} g ${macroName.toLowerCase()}${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remaining > 0) {
tooltipText += `${remaining.toFixed(1)} g ${t('general.remaining')}`;
} else if (remaining < 0) {
tooltipText += `${Math.abs(remaining).toFixed(1)} g ${t('table.summary.over')}`;
}
return tooltipText;
}
return tooltipText;
}
/**
@ -163,15 +230,15 @@ export function getSummaryHeader(id: string): string {
}
/**
* Format calories for tooltips with proper unit handling
* Format calories for tooltips with proper unit handling and spacing
* @param calories Calorie value in kcal
* @param target Target calorie value in kcal
* @param percentage Percentage of target
* @returns Formatted tooltip string
* @returns Formatted tooltip string with proper spacing
*/
export function formatCalorieTooltip(calories: number, target: number, percentage: number): string {
if (!formatterPlugin) {
// Fallback if plugin not set
// Fallback if plugin not set - with proper spacing
const remaining = target - calories;
if (remaining > 0) {
return `${calories.toFixed(1)} kcal • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}${remaining.toFixed(1)} kcal ${t('general.remaining')}`;
@ -181,33 +248,50 @@ export function formatCalorieTooltip(calories: number, target: number, percentag
}
const currentUnit = getCurrentEnergyUnit();
const currentLocale = getCurrentLocale();
if (currentUnit === 'kJ') {
const consumedKj = convertEnergyUnit(calories, 'kcal', 'kJ');
const targetKj = convertEnergyUnit(target, 'kcal', 'kJ');
const remainingKj = targetKj - consumedKj;
let tooltipText = `${consumedKj.toFixed(1)} kJ • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remainingKj > 0) {
tooltipText += `${remainingKj.toFixed(1)} kJ ${t('general.remaining')}`;
} else if (remainingKj < 0) {
tooltipText += `${Math.abs(remainingKj).toFixed(1)} kJ ${t('table.summary.over')}`;
if (currentLocale === 'zh-CN') {
let tooltipText = `${consumedKj.toFixed(1)} kJ • 占 ${Math.round(percentage)}%`;
if (remainingKj > 0) {
tooltipText += ` • 剩余 ${remainingKj.toFixed(1)} kJ`;
} else if (remainingKj < 0) {
tooltipText += ` • 超出 ${Math.abs(remainingKj).toFixed(1)} kJ`;
}
return tooltipText;
} else {
let tooltipText = `${consumedKj.toFixed(1)} kJ • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remainingKj > 0) {
tooltipText += `${remainingKj.toFixed(1)} kJ ${t('general.remaining')}`;
} else if (remainingKj < 0) {
tooltipText += `${Math.abs(remainingKj).toFixed(1)} kJ ${t('table.summary.over')}`;
}
return tooltipText;
}
return tooltipText;
} else {
const remaining = target - calories;
let tooltipText = `${calories.toFixed(1)} kcal • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remaining > 0) {
tooltipText += `${remaining.toFixed(1)} kcal ${t('general.remaining')}`;
} else if (remaining < 0) {
tooltipText += `${Math.abs(remaining).toFixed(1)} kcal ${t('table.summary.over')}`;
if (currentLocale === 'zh-CN') {
let tooltipText = `${calories.toFixed(1)} kcal • 占 ${Math.round(percentage)}%`;
if (remaining > 0) {
tooltipText += ` • 剩余 ${remaining.toFixed(1)} kcal`;
} else if (remaining < 0) {
tooltipText += ` • 超出 ${Math.abs(remaining).toFixed(1)} kcal`;
}
return tooltipText;
} else {
let tooltipText = `${calories.toFixed(1)} kcal • ${Math.round(percentage)}% ${t('table.summary.dailyTarget')}`;
if (remaining > 0) {
tooltipText += `${remaining.toFixed(1)} kcal ${t('general.remaining')}`;
} else if (remaining < 0) {
tooltipText += `${Math.abs(remaining).toFixed(1)} kcal ${t('table.summary.over')}`;
}
return tooltipText;
}
return tooltipText;
}
}
@ -216,7 +300,7 @@ export function formatCalorieTooltip(calories: number, target: number, percentag
* @param macro Macro name
* @param value Macro value in grams
* @param percentage Percentage of total macros
* @returns Formatted tooltip string
* @returns Formatted tooltip string with proper spacing
*/
export function formatMacroCompositionTooltip(
macro: string,
@ -234,7 +318,7 @@ export function formatMacroCompositionTooltip(
* Format target tooltip
* @param target Target value
* @param unit Unit (g, kcal, kJ)
* @returns Formatted tooltip string
* @returns Formatted tooltip string with proper spacing
*/
export function formatTargetTooltip(target: number, unit: string): string {
return t('tooltips.target', {
@ -244,12 +328,12 @@ export function formatTargetTooltip(target: number, unit: string): string {
}
/**
* Format percentage tooltip for macro cells
* Format percentage tooltip for macro cells with proper spacing
* @param value Macro value in grams
* @param macro Macro name
* @param percentage Percentage of daily target
* @param target Daily target value
* @returns Formatted tooltip string
* @returns Formatted tooltip string with proper spacing
*/
export function formatMacroPercentageTooltip(
value: number,
@ -258,6 +342,7 @@ export function formatMacroPercentageTooltip(
target: number
): string {
const remaining = target - value;
const currentLocale = getCurrentLocale();
let tooltipText = t('tooltips.percentage', {
value: value.toFixed(1),
@ -266,19 +351,27 @@ export function formatMacroPercentageTooltip(
});
if (remaining > 0) {
tooltipText += `${remaining.toFixed(1)}g ${t('general.remaining')}`;
if (currentLocale === 'zh-CN') {
tooltipText += ` • 剩余 ${remaining.toFixed(1)} g`;
} else {
tooltipText += `${remaining.toFixed(1)} g ${t('general.remaining')}`;
}
} else if (remaining < 0) {
tooltipText += `${t('tooltips.over', { over: Math.abs(remaining).toFixed(1) })}`;
if (currentLocale === 'zh-CN') {
tooltipText += ` • 超出 ${Math.abs(remaining).toFixed(1)} g`;
} else {
tooltipText += `${t('tooltips.over', { over: Math.abs(remaining).toFixed(1) })}`;
}
}
return tooltipText;
}
/**
* Format energy value with automatic unit conversion
* Format energy value with automatic unit conversion and proper spacing
* @param valueInKcal Energy value in kcal
* @param showUnit Whether to include the unit in the output
* @returns Formatted energy string
* @returns Formatted energy string with proper spacing
*/
export function formatEnergy(valueInKcal: number, showUnit: boolean = true): string {
if (!formatterPlugin) {
@ -289,12 +382,22 @@ export function formatEnergy(valueInKcal: number, showUnit: boolean = true): str
if (currentUnit === 'kJ') {
const kj = convertEnergyUnit(valueInKcal, 'kcal', 'kJ');
return showUnit ? `${kj.toFixed(1)} kJ` : kj.toFixed(1);
return showUnit ? `${kj.toFixed(1)} kJ` : kj.toFixed(1); // Proper spacing
} else {
return showUnit ? `${valueInKcal.toFixed(1)} kcal` : valueInKcal.toFixed(1);
return showUnit ? `${valueInKcal.toFixed(1)} kcal` : valueInKcal.toFixed(1); // Proper spacing
}
}
/**
* Format serving size with proper spacing
* @param serving Serving size string
* @returns Formatted serving size with space between number and unit
*/
export function formatServing(serving: string): string {
// Handle various serving size formats and ensure proper spacing
return serving.replace(/(\d+(?:\.\d+)?)\s*g/i, '$1 g');
}
/**
* Get the current energy unit as a string
* @returns Current energy unit setting
@ -315,11 +418,11 @@ export function formatNumber(value: number, decimals: number = 1): string {
}
/**
* Format meal summary text
* Format meal summary text with proper spacing
* @param mealName Name of the meal
* @param itemCount Number of items in the meal
* @param calories Total calories
* @returns Formatted meal summary
* @returns Formatted meal summary with proper spacing
*/
export function formatMealSummary(mealName: string, itemCount: number, calories: number): string {
const calorieText = formatCalories(calories);
@ -328,18 +431,18 @@ export function formatMealSummary(mealName: string, itemCount: number, calories:
}
/**
* Format remaining/over values for summary rows
* Format remaining/over values for summary rows with proper spacing
* @param remaining Remaining value (negative if over target)
* @param unit Unit string
* @returns Formatted remaining/over string
* @returns Formatted remaining/over string with proper spacing
*/
export function formatRemaining(remaining: number, unit: string): string {
if (remaining < 0) {
return `${formatNumber(Math.abs(remaining))}${unit} (${t('table.summary.over')})`;
return `${formatNumber(Math.abs(remaining))} ${unit} (${t('table.summary.over')})`;
} else if (remaining === 0) {
return `0${unit}`;
return `0 ${unit}`;
} else {
return `${formatNumber(remaining)}${unit}`;
return `${formatNumber(remaining)} ${unit}`;
}
}

2073
styles.css

File diff suppressed because it is too large Load diff