Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 54 additions & 25 deletions apps/app/src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,37 +94,66 @@ export const setLocale = (newLocale: Language) => {
};

/**
* Translation function with fallback behavior
* Fallback chain: target language → English → key itself
*
* @param key - Translation key
* @param localeOverride - Optional locale override (defaults to current locale)
* @returns Translated string or fallback
* Resolve a translation entry with the locale → English → null fallback chain.
*/
export const t = (key: string, params?: Record<string, string | number> & { lng?: Language }): string => {
const loc = params?.lng ?? locale();
const lookupEntry = (loc: Language, candidateKey: string): string | null => {
if (TRANSLATIONS[loc]?.[candidateKey]) return TRANSLATIONS[loc][candidateKey];
if (loc !== "en" && TRANSLATIONS.en?.[candidateKey]) return TRANSLATIONS.en[candidateKey];
return null;
};

// Try target language first
let result: string;
if (TRANSLATIONS[loc]?.[key]) {
result = TRANSLATIONS[loc][key];
} else if (loc !== "en" && TRANSLATIONS.en?.[key]) {
// Fallback to English
result = TRANSLATIONS.en[key];
} else {
// Final fallback to key itself (prevents raw keys from showing in UI)
return key;
const pluralRulesCache = new Map<Language, Intl.PluralRules>();
const pluralRule = (loc: Language, count: number): Intl.LDMLPluralRule => {
let rules = pluralRulesCache.get(loc);
if (!rules) {
rules = new Intl.PluralRules(loc);
pluralRulesCache.set(loc, rules);
}
return rules.select(count);
};

// Replace params if provided (skip the lng meta-key)
if (params) {
for (const [k, v] of Object.entries(params)) {
if (k === "lng") continue;
result = result.replace(`{${k}}`, String(v));
}
/**
* Pick the right key variant for a count. Tries `${key}_zero` (only when count === 0),
* then `${key}_${rule}` (e.g. `_one` / `_other`), then `${key}_other`, then the bare
* key. Asian locales (no grammatical plural) define only the bare key and hit the
* final step. Each candidate runs through the locale → English fallback so an
* untranslated key still resolves to the English `_one` / `_other` variant.
*/
const resolvePluralKey = (loc: Language, key: string, count: number): string => {
const candidates: string[] = [];
if (count === 0) candidates.push(`${key}_zero`);
candidates.push(`${key}_${pluralRule(loc, count)}`, `${key}_other`, key);

for (const candidate of candidates) {
if (lookupEntry(loc, candidate) !== null) return candidate;
}
return key;
};

return result;
/**
* Translation function with fallback behavior.
* - Locale fallback: target language → English → key itself.
* - Plural fallback: when params include a numeric `count`, the lookup picks
* `${key}_one` / `${key}_other` (or `${key}_zero` when count === 0) per
* `Intl.PluralRules`, and falls back to the bare key when no variants exist.
*/
export const t = (key: string, params?: Record<string, string | number> & { lng?: Language }): string => {
const loc = params?.lng ?? locale();

const lookupKey =
typeof params?.count === "number" ? resolvePluralKey(loc, key, params.count) : key;

const result = lookupEntry(loc, lookupKey);
if (result === null) return key;

if (!params) return result;

let out = result;
for (const [k, v] of Object.entries(params)) {
if (k === "lng") continue;
out = out.replace(`{${k}}`, String(v));
}
return out;
};

/**
Expand Down
23 changes: 12 additions & 11 deletions apps/app/src/i18n/locales/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,12 @@ export default {
"den.status_browser_signup": "Acabeu la creació del compte al vostre navegador per connectar OpenWork.",
"den.status_cloud_signed_in_as": "OpenWork Cloud connectat com a {email}.",
"den.status_cloud_signin_done": "OpenWork Cloud connectat.",
"den.status_loaded_orgs": "S'ha carregat {count} org{plural}.",
"den.status_loaded_skills": "S'han carregat {count} Skill{plural} del núvol per a {name}.",
"den.status_loaded_workers": "S'ha carregat {count} worker{plural} per a {name}.",
"den.status_loaded_orgs_one": "S'ha carregat {count} org.",
"den.status_loaded_orgs_other": "S'ha carregat {count} orgs.",
"den.status_loaded_skills_one": "S'han carregat {count} Skill del núvol per a {name}.",
"den.status_loaded_skills_other": "S'han carregat {count} Skills del núvol per a {name}.",
"den.status_loaded_workers_one": "S'ha carregat {count} worker per a {name}.",
"den.status_loaded_workers_other": "S'ha carregat {count} workers per a {name}.",
"den.status_no_skills": "No s'han trobat Skills al núvol per a {name}.",
"den.status_no_workers": "No s'ha trobat cap worker per a {name}.",
"den.status_opened_worker": "S'ha obert {name} a OpenWork.",
Expand All @@ -477,13 +480,13 @@ export default {
"den.worker_provider_label": "{provider} worker",
"den.worker_secondary_cloud": "Worker Cloud",
"extensions.app_count_one": "Aplicació {count} connectada",
"extensions.app_count_many": "Aplicacions {count} connectades",
"extensions.app_count_other": "Aplicacions {count} connectades",
"extensions.apps_mcp_header": "Aplicacions (MCP)",
"extensions.filter_all": "Tots",
"extensions.filter_apps": "Aplicacions",
"extensions.filter_plugins": "Plugins",
"extensions.plugin_count_one": "{count} plugin",
"extensions.plugin_count_many": "{count} plugins",
"extensions.plugin_count_other": "{count} plugins",
"extensions.plugins_opencode_header": "Plugins (OpenCode)",
"extensions.subtitle": "Les apps (MCP) i els Plugins d'OpenCode són al mateix lloc.",
"extensions.title": "Extensions",
Expand Down Expand Up @@ -828,7 +831,6 @@ export default {
"message_list.open_session": "Sessió oberta",
"message_list.step_updates_progress": "Progrés de les actualitzacions",
"message_list.subagent_loading_transcript": "S'està carregant la transcripció",
"message_list.subagent_message_count": "missatge {count}{plural}",
"message_list.subagent_running": "Córrer",
"message_list.subagent_session_fallback": "Sessió de subagent",
"message_list.subagent_type_task": "tasca {agentType}",
Expand Down Expand Up @@ -884,8 +886,8 @@ export default {
"model_picker.connect_provider_hint": "Connecta aquest proveïdor per veure i desar models",
"model_picker.default_model_desc": "Tria el model predeterminat per a xats nous i, a continuació, ajusteu els perfils de raonament a la seva targeta abans de prémer Fet.",
"model_picker.default_model_title": "Model per defecte",
"model_picker.model_count": "Models {count}",
"model_picker.model_count_one": "1 model",
"model_picker.model_count_one": "{count} model",
"model_picker.model_count_other": "{count} models",
"model_picker.more_providers": "Més proveïdors",
"model_picker.no_results": "No hi ha cap model que coincideixi amb la teva cerca.",
"model_picker.other_connected_models": "Altres models connectats",
Expand Down Expand Up @@ -913,7 +915,6 @@ export default {
"onboarding.create_first_workspace": "Crea el teu primer workspace",
"onboarding.create_workspace": "Crea un workspace",
"onboarding.engine_running": "El motor ja funciona",
"onboarding.folders_allowed": "Carpeta {count} {plural} permesa",
"onboarding.getting_ready": "Preparant-ho tot",
"onboarding.install": "Instal·la OpenCode",
"onboarding.install_instruction": "Instal·la OpenCode per habilitar el servidor local (no cal cap terminal).",
Expand Down Expand Up @@ -1215,7 +1216,6 @@ export default {
"session.share_worker_url_phones_hint": "Fes servir-lo en telèfons o ordinadors portàtils connectats a aquest worker.",
"session.share_worker_url_resolving_hint": "S'està resolent l'URL del worker; mentrestant es mostra l'URL del host com a alternativa.",
"session.shared_folder_upload_failed": "La càrrega de la carpeta compartida ha fallat",
"session.show_earlier": "Mostra {count} missatge{plural} anteriors",
"session.status_active": "Sessió activa",
"session.status_compacting": "Context compactant",
"session.status_delegating": "Delegant",
Expand Down Expand Up @@ -1932,7 +1932,8 @@ export default {
"status.mcp_connected": "{count} MCP connectat",
"status.open_docs": "Obre la documentació",
"status.openwork_ready": "OpenWork llest",
"status.providers_connected": "Proveïdor {count}{plural} connectat",
"status.providers_connected_one": "{count} proveïdor connectat",
"status.providers_connected_other": "{count} proveïdors connectat",
"status.ready_for_tasks": "Preparat per a noves tasques",
"status.reloading_engine": "Recàrrega del motor",
"status.restarting_engine": "Reiniciant el motor",
Expand Down
23 changes: 12 additions & 11 deletions apps/app/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,9 +460,12 @@ export default {
"den.status_browser_signup": "Finish account creation in your browser to connect OpenWork.",
"den.status_cloud_signed_in_as": "Connected OpenWork Cloud as {email}.",
"den.status_cloud_signin_done": "Connected OpenWork Cloud.",
"den.status_loaded_orgs": "Connected {count} organization{plural}.",
"den.status_loaded_skills": "Loaded {count} cloud skill{plural} for {name}.",
"den.status_loaded_workers": "Loaded {count} worker{plural} for {name}.",
"den.status_loaded_orgs_one": "Connected {count} organization.",
"den.status_loaded_orgs_other": "Connected {count} organizations.",
"den.status_loaded_skills_one": "Loaded {count} cloud skill for {name}.",
"den.status_loaded_skills_other": "Loaded {count} cloud skills for {name}.",
"den.status_loaded_workers_one": "Loaded {count} worker for {name}.",
"den.status_loaded_workers_other": "Loaded {count} workers for {name}.",
"den.status_no_skills": "No cloud skills found for {name}.",
"den.status_no_workers": "No workers found for {name}.",
"den.status_opened_worker": "Opened {name} in OpenWork.",
Expand All @@ -480,13 +483,13 @@ export default {
"den.worker_provider_label": "{provider} worker",
"den.worker_secondary_cloud": "Cloud worker",
"extensions.app_count_one": "{count} app connected",
"extensions.app_count_many": "{count} apps connected",
"extensions.app_count_other": "{count} apps connected",
"extensions.apps_mcp_header": "Apps (MCP)",
"extensions.filter_all": "All",
"extensions.filter_apps": "Apps",
"extensions.filter_plugins": "Plugins",
"extensions.plugin_count_one": "{count} plugin",
"extensions.plugin_count_many": "{count} plugins",
"extensions.plugin_count_other": "{count} plugins",
"extensions.plugins_opencode_header": "Plugins (OpenCode)",
"extensions.subtitle": "Apps (MCP) and OpenCode plugins live in one place.",
"extensions.title": "Extensions",
Expand Down Expand Up @@ -836,7 +839,6 @@ export default {
"message_list.open_session": "Open session",
"message_list.step_updates_progress": "Updates progress",
"message_list.subagent_loading_transcript": "Loading transcript",
"message_list.subagent_message_count": "{count} message{plural}",
"message_list.subagent_running": "Running",
"message_list.subagent_session_fallback": "Subagent session",
"message_list.subagent_type_task": "{agentType} task",
Expand Down Expand Up @@ -892,8 +894,8 @@ export default {
"model_picker.connect_provider_hint": "Connect this provider to browse and save models",
"model_picker.default_model_desc": "Choose the default model for new chats, then fine-tune reasoning profiles on its card before pressing Done.",
"model_picker.default_model_title": "Default model",
"model_picker.model_count": "{count} models",
"model_picker.model_count_one": "1 model",
"model_picker.model_count_one": "{count} model",
"model_picker.model_count_other": "{count} models",
"model_picker.more_providers": "More providers",
"model_picker.no_results": "No models match your search.",
"model_picker.other_connected_models": "Other connected models",
Expand Down Expand Up @@ -921,7 +923,6 @@ export default {
"onboarding.create_first_workspace": "Create your first workspace",
"onboarding.create_workspace": "Create a workspace",
"onboarding.engine_running": "Engine already running",
"onboarding.folders_allowed": "{count} folder{plural} allowed",
"onboarding.getting_ready": "Getting everything ready",
"onboarding.install": "Install OpenCode",
"onboarding.install_instruction": "Install OpenCode to enable the local server (no terminal required).",
Expand Down Expand Up @@ -1248,7 +1249,6 @@ export default {
"session.share_worker_url_phones_hint": "Use on phones or laptops connecting to this worker.",
"session.share_worker_url_resolving_hint": "Worker URL is resolving; host URL shown as fallback.",
"session.shared_folder_upload_failed": "Shared folder upload failed",
"session.show_earlier": "Show {count} earlier message{plural}",
"session.status_active": "Session Active",
"session.status_compacting": "Compacting Context",
"session.status_delegating": "Delegating",
Expand Down Expand Up @@ -2016,7 +2016,8 @@ export default {
"status.mcp_connected": "{count} MCP connected",
"status.open_docs": "Open documentation",
"status.openwork_ready": "OpenWork Ready",
"status.providers_connected": "{count} provider{plural} connected",
"status.providers_connected_one": "{count} provider connected",
"status.providers_connected_other": "{count} providers connected",
"status.ready_for_tasks": "Ready for new tasks",
"status.reloading_engine": "Reloading engine",
"status.restarting_engine": "Restarting engine",
Expand Down
23 changes: 12 additions & 11 deletions apps/app/src/i18n/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,12 @@ export default {
"den.status_browser_signup": "Termina de crear tu cuenta en el navegador para conectar OpenWork.",
"den.status_cloud_signed_in_as": "Conectado a OpenWork Cloud como {email}.",
"den.status_cloud_signin_done": "OpenWork Cloud conectado.",
"den.status_loaded_orgs": "Se cargaron {count} organización{plural}.",
"den.status_loaded_skills": "Se cargaron {count} Skill{plural} de Cloud para {name}.",
"den.status_loaded_workers": "Se cargaron {count} worker{plural} para {name}.",
"den.status_loaded_orgs_one": "Se cargaron {count} organización.",
"den.status_loaded_orgs_other": "Se cargaron {count} organizacións.",
"den.status_loaded_skills_one": "Se cargaron {count} Skill de Cloud para {name}.",
"den.status_loaded_skills_other": "Se cargaron {count} Skills de Cloud para {name}.",
"den.status_loaded_workers_one": "Se cargaron {count} worker para {name}.",
"den.status_loaded_workers_other": "Se cargaron {count} workers para {name}.",
"den.status_no_skills": "No se encontraron Skills en Cloud para {name}.",
"den.status_no_workers": "No se encontraron workers para {name}.",
"den.status_opened_worker": "Se abrió {name} en OpenWork.",
Expand All @@ -477,13 +480,13 @@ export default {
"den.worker_provider_label": "Worker de {provider}",
"den.worker_secondary_cloud": "Worker de Cloud",
"extensions.app_count_one": "Aplicación {count} conectada",
"extensions.app_count_many": "Aplicaciones {count} conectadas",
"extensions.app_count_other": "Aplicaciones {count} conectadas",
"extensions.apps_mcp_header": "Aplicaciones (MCP)",
"extensions.filter_all": "Todo",
"extensions.filter_apps": "Aplicaciones",
"extensions.filter_plugins": "Plugins",
"extensions.plugin_count_one": "Plugin {count}",
"extensions.plugin_count_many": "Plugins {count}",
"extensions.plugin_count_other": "Plugins {count}",
"extensions.plugins_opencode_header": "Plugins (OpenCode)",
"extensions.subtitle": "Las aplicaciones (MCP) y los Plugins OpenCode se encuentran en un solo lugar.",
"extensions.title": "Extensiones",
Expand Down Expand Up @@ -828,7 +831,6 @@ export default {
"message_list.open_session": "sesión abierta",
"message_list.step_updates_progress": "Progreso de las actualizaciones",
"message_list.subagent_loading_transcript": "Cargando transcripción",
"message_list.subagent_message_count": "Mensaje {count}{plural}",
"message_list.subagent_running": "En ejecución",
"message_list.subagent_session_fallback": "sesión de subagente",
"message_list.subagent_type_task": "tarea {agentType}",
Expand Down Expand Up @@ -884,8 +886,8 @@ export default {
"model_picker.connect_provider_hint": "Conecta este proveedor para buscar y guardar modelos",
"model_picker.default_model_desc": "Elige el modelo predeterminado para nuevos chats, luego ajuste los perfiles de razonamiento en su tarjeta antes de presionar Listo.",
"model_picker.default_model_title": "Modelo predeterminado",
"model_picker.model_count": "Modelos {count}",
"model_picker.model_count_one": "1 modelo",
"model_picker.model_count_one": "{count} modelo",
"model_picker.model_count_other": "{count} modelos",
"model_picker.more_providers": "Más proveedores",
"model_picker.no_results": "Ningún modelo coincide con tu búsqueda.",
"model_picker.other_connected_models": "Otros modelos conectados",
Expand Down Expand Up @@ -913,7 +915,6 @@ export default {
"onboarding.create_first_workspace": "Crea tu primer espacio de trabajo",
"onboarding.create_workspace": "Crear un espacio de trabajo",
"onboarding.engine_running": "Motor ya en marcha",
"onboarding.folders_allowed": "{count} carpeta{plural} permitida{plural}",
"onboarding.getting_ready": "Preparando todo",
"onboarding.install": "Instalar OpenCode",
"onboarding.install_instruction": "Instala OpenCode para habilitar el servidor local (no se necesita terminal).",
Expand Down Expand Up @@ -1215,7 +1216,6 @@ export default {
"session.share_worker_url_phones_hint": "Úsalo en móviles o portátiles que se conecten a este worker.",
"session.share_worker_url_resolving_hint": "La URL del worker se está resolviendo; mientras tanto se muestra la URL del host como alternativa.",
"session.shared_folder_upload_failed": "Falló la subida a la carpeta compartida",
"session.show_earlier": "Mostrar {count} mensaje{plural} anterior",
"session.status_active": "Sesión activa",
"session.status_compacting": "Compactando contexto",
"session.status_delegating": "Delegar",
Expand Down Expand Up @@ -1932,7 +1932,8 @@ export default {
"status.mcp_connected": "{count} MCP conectado",
"status.open_docs": "Abrir documentación",
"status.openwork_ready": "OpenWork listo",
"status.providers_connected": "Proveedor {count}{plural} conectado",
"status.providers_connected_one": "{count} proveedor conectado",
"status.providers_connected_other": "{count} proveedors conectado",
"status.ready_for_tasks": "Listo para nuevas tareas",
"status.reloading_engine": "Recarga del motor",
"status.restarting_engine": "Reiniciando el motor",
Expand Down
Loading
Loading