From 0a8a94ecc7806606e1c77a1fa211a06343659b75 Mon Sep 17 00:00:00 2001 From: Alexandra Goff Date: Fri, 21 Jun 2024 12:14:30 -0700 Subject: [PATCH 01/10] [C] refactor i18next setup --- lib/i18n.js | 26 -- lib/i18n/client.js | 30 ++ lib/i18n/index.js | 28 ++ lib/i18n/settings.js | 23 ++ lib/localeStrings/es.json | 368 ------------------- lib/localeStrings/index.js | 8 - package.json | 4 +- pages/[[...uriSegments]].js | 2 +- pages/_app.js | 49 +-- pages/_error.js | 2 +- public/localeStrings/en/translation.json | 441 +++++++++++++++++++++++ public/localeStrings/es/translation.json | 365 +++++++++++++++++++ yarn.lock | 36 +- 13 files changed, 941 insertions(+), 441 deletions(-) delete mode 100644 lib/i18n.js create mode 100644 lib/i18n/client.js create mode 100644 lib/i18n/index.js create mode 100644 lib/i18n/settings.js delete mode 100644 lib/localeStrings/es.json delete mode 100644 lib/localeStrings/index.js create mode 100644 public/localeStrings/en/translation.json create mode 100644 public/localeStrings/es/translation.json diff --git a/lib/i18n.js b/lib/i18n.js deleted file mode 100644 index d61a73e1..00000000 --- a/lib/i18n.js +++ /dev/null @@ -1,26 +0,0 @@ -import i18n from "i18next"; -import { initReactI18next } from "react-i18next"; -import { localeData } from "./localeStrings"; - -export const updateI18n = (lang) => { - i18n.language !== lang && i18n.changeLanguage(lang); -}; - -i18n - .use(initReactI18next) // passes i18n down to react-i18next - .init({ - // debug: true, - lng: "en", - resources: localeData, - interpolation: { - escapeValue: false, - }, - fallbackLng: { - default: ["en"], - }, - react: { - transSupportBasicHtmlNodes: true, - }, - }); - -export default i18n; diff --git a/lib/i18n/client.js b/lib/i18n/client.js new file mode 100644 index 00000000..d425c2e5 --- /dev/null +++ b/lib/i18n/client.js @@ -0,0 +1,30 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import { languages, getOptions, namespaces, cookieName } from "./settings"; +import { loadResources } from "./index"; + +const runsOnServerSide = typeof window === "undefined"; + +export const updateI18n = (lang) => { + i18n.language !== lang && i18n.changeLanguage(lang); +}; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .use(LanguageDetector) + .use(loadResources) + .init({ + ...getOptions(), + ns: namespaces, + fallbackNS: namespaces, + lng: undefined, // let detect the language on client side + detection: { + excludeCacheFor: languages, + lookupCookie: cookieName, + order: ["htmlTag", "navigator"], + }, + preload: runsOnServerSide ? languages : [], + }); + +export default i18n; diff --git a/lib/i18n/index.js b/lib/i18n/index.js new file mode 100644 index 00000000..5236b0eb --- /dev/null +++ b/lib/i18n/index.js @@ -0,0 +1,28 @@ +import resourcesToBackend from "i18next-resources-to-backend"; + +export const loadResources = resourcesToBackend( + (language, namespace, callback) => { + switch (namespace) { + // case "epo-react-lib": + // import( + // `@rubin-epo/epo-react-lib/localeStrings/${language}/${namespace}.json` + // ) + // .then(({ default: resources }) => { + // callback(null, resources); + // }) + // .catch((error) => { + // callback(error, null); + // }); + // break; + default: + import(`../../public/localeStrings/${language}/${namespace}.json`) + .then(({ default: resources }) => { + callback(null, resources); + }) + .catch((error) => { + callback(error, null); + }); + break; + } + } +); diff --git a/lib/i18n/settings.js b/lib/i18n/settings.js new file mode 100644 index 00000000..8124d03d --- /dev/null +++ b/lib/i18n/settings.js @@ -0,0 +1,23 @@ +export const fallbackLng = "en"; +export const languages = [fallbackLng, "es"]; +export const defaultNS = "translation"; +export const namespaces = [defaultNS]; +export const cookieName = "NEXT_LOCALE"; + +export function getOptions(lng = fallbackLng, ns = defaultNS) { + return { + // debug: process.env.NODE_ENV === "development", + supportedLngs: languages, + fallbackLng, + lng, + fallbackNS: defaultNS, + defaultNS, + ns, + react: { + transSupportBasicHtmlNodes: true, + }, + backend: { + loadPath: `${process.env.NEXT_PUBLIC_BASE_URL}/localeStrings/{{lng}}/{{ns}}.json`, + }, + }; +} diff --git a/lib/localeStrings/es.json b/lib/localeStrings/es.json deleted file mode 100644 index 7db769ff..00000000 --- a/lib/localeStrings/es.json +++ /dev/null @@ -1,368 +0,0 @@ -{ - "key": "en", - "translation": { - "english-site-name": "Inglés", - "espanol-site-name": "Español", - "language-select-label": "Idioma", - "breaking-news": "Últimas noticias", - "cell": "Celda", - "localize-content": "Localizar el contenido del sitio", - "loading": "Cargando…", - "media": "Multimedia", - "read-more": "Leer más", - "related-content": "Contenido relacionado", - "related-content-loading": "Cargando contenido relacionado", - "error-title": "Error", - "error-text": "Ha ocurrido un error.", - "page-not-found-title": "Página no encontrada", - "page-not-found-text": "La página que busca no se encontró.", - "published": "Publicado", - "search-clear": "Borrar todo", - "search-no-results": "Sin resultados. Por favor intente una nueva búsqueda.", - "search-placeholder": "Escriba su búsqueda y presione enter…", - "search-filter": "Buscar", - "search-filter-placeholder": "Buscar", - "search-site": "Buscar en el sitio web", - "skip-to-content": "Saltar al contenido principal", - "submit-search": "Enviar búsqueda", - "tags": "Hashtags", - "toggle-nav": "Alternar menú de navegación", - "toggle-search": "Alternar búsqueda", - "homepage": "Página principal", - "contact-form": { - "contact-us": "Contáctenos", - "success": "Gracias. Hemos recibido su mensaje.", - "error": "Parece que encontramos un problema." - }, - "form": { - "email": "Correo", - "email_required": "Correo(requerido)", - "first_name": "Nombre", - "last_name": "Apellido", - "topic": "Asunto", - "inquiry": "Consulta", - "password": "Contraseña", - "password_required": "Contraseña (requerida)", - "current_password": "Contraseña actual", - "new_password": "Nueva contraseña", - "confirm_password": "Confirmar nueva contraseña", - "show_password": "Mostrar contraseña", - "hide_password": "Ocultar contraseña", - "create_password": "Crear contraseña (requerido)", - "create_password_instructions": "Las contraseñas deben tener al menos 8 caracteres y una mezcla de letras, números y otros símbolos.", - "school": "Institución", - "preferred_language": "Idioma preferido", - "email_subscription": "Suscripción de correo", - "send": "Enviar", - "sending": "Enviando…", - "submitting": "Enviando...", - "reset": "Reestablecer formulario", - "cancel": "Cancelar", - "error": "Error:", - "confirm": "¡Entendido!" - }, - "filters": { - "sort": "Ordenar por", - "ascending": "Ascendente", - "descending": "Descendente", - "all": "Todo", - "pages": "Páginas", - "events": "Eventos", - "gallery-items": "Items de la galería", - "jobs": "Trabajos", - "news": "Noticias", - "staff-profiles": "Voces de Rubin", - "slideshows": "Diapositivas", - "image": "Imagen", - "video": "Video", - "3d": "Archivos 3D", - "non-scientific-staff": "No científicos", - "scientific-staff": "Científicos", - "press-release": "Boletín", - "news-post": "Noticias", - "administrative": "Administrativo", - "engineering": "Ingeniería", - "science": "Ciencia", - "technical": "Técnico", - "special-event": "Evento especial", - "scientific-event": "Evento científico", - "press-event": "Evento de prensa" - }, - "alert": { - "solar-systems-found": "Sistemas solares encontrados", - "broker-classifications": "Clasificaciones del broker", - "total-to-date": "Total a la fecha" - }, - "events": { - "event": "Evento", - "open": "Inscripciones abiertas", - "closed": "Inscripciones cerradas", - "related-events": "Otros eventos", - "go-to-events": "Ir a eventos", - "upcoming": "Próximos eventos", - "past": "Eventos anteriores", - "loading": "Cargando el Calendario..." - }, - "gallery": { - "about-the-image": "Acerca de la imagen", - "about-the-video": "Acerca del video", - "about-the-3d": "Acerca del modelo 3D", - "about-the-gallery": "Acerca de la galería", - "additional-information": "Información adicional", - "available-sizes": "Tamaños disponibles", - "curated-slideshows": "Presentaciones curadas", - "loading-slideshows": "Cargando las Presentaciones...", - "download-image": "Descargar imagen", - "download-video": "Descargar video", - "download-3d": "Descargar modelo 3D", - "download-gallery": "Descargar", - "tags": "Hashtags", - "back-to-image": "Volver a imágenes", - "back-to-video": "Volver a videos", - "back-to-3d": "Volver a modelos 3D", - "back-to-gallery": "Volver a galerías", - "back-to-slideshows": "Volver a diapositivas", - "image": "Imagen", - "video": "Video", - "3d": "3D", - "gallery": "Galería", - "gallery-item": "Item de la galería", - "plural-image": "Imágenes", - "plural-video": "Videos", - "plural-3d": "Archivos 3D", - "plural-gallery": "Galerías", - "see-all": "Ver todo", - "slideshow": "Diapositivas", - "start-slideshow": "Comenzar presentación", - "original": "Original", - "extraLarge": "Extra grande", - "large": "Grande", - "medium": "Mediano", - "small": "Pequeño", - "thumbnail": "Miniatura", - "4k": "4k", - "1080p": "1080p", - "720p": "720p", - "480p": "480p", - "320p": "320p", - "loading": "Cargando la Galería..." - }, - "jobs": { - "job": "Trabajo", - "jobs": "Trabajos", - "read-more": "Leer más sobre este trabajo.", - "open": "Vacante", - "closed": "Cerrado", - "loading": "Cargando los Trabajos..." - }, - "news": { - "news": "News", - "related-posts": "Noticias relacionadas", - "back-to-posts": "Volver a noticias", - "loading": "Cargando las noticias...", - "more-info": "Información Adicional", - "release-link": "Obtenga más información sobre este boletín en NOIRlab.edu", - "links": "Links", - "contacts": "Contactos" - }, - "pages": { - "page": "Pagina" - }, - "pagination": { - "label": "Paginación", - "previous": "Anterior", - "next": "Siguiente", - "of": "de", - "current-slide": "Diapositiva {{current}}", - "showing-current-slide": "Mostrando diapositiva {{current}} de {{length}}", - "showing-range": "Mostrando {{from}} a {{to}} de {{length}}", - "previous_name": "Anterior ({{name}})", - "next_name": "Siguiente ({{name}})", - "back_to_name": "Volver a {{name}}" - }, - "social": { - "email-rubin": "Envíe un email al Observatorio Rubin", - "visit-rubin": "Visite el Observatorio Rubin en", - "connect": "Conecta con nosotros" - }, - "staff": { - "rubin-voices": "Voces de Rubin", - "scientific-staff": "Perfil del equipo científico", - "non-scientific-staff": "Perfil del equipo no científico", - "browse-more": "Navegar más perfiles", - "back-to-profiles": "Volver a perfiles", - "trading-card": "Carta coleccionable", - "loading": "Cargando las Voces de Rubin..." - }, - "telescope": { - "state": "Estado", - "in-operations": "en operaciones", - "point": "Actualmente apuntando a", - "temp": "Temperatura en la cima", - "temp-data": "Humedad", - "image": "Imagen actual del cielo", - "image-data": "completado", - "weather": "Clima", - "cloudy": "Nublado", - "moon": "Fase lunar", - "first-quarter": "Primer cuarto" - }, - "auth": { - "log_in": "Iniciar sesión", - "log_out": "Cerrar sesión", - "logged_in_as": "Sesión iniciada como {{name}}", - "logged_out": "Sesión finalizada", - "sign_up": "Registrarse" - }, - "sign_in": { - "header": "¡Bienvenido de vuelta!", - "sso_description": "Para iniciar sesión usando Google o Facebook, por favor seleccione si usted es estudiante o educador antes de continuar.", - "email_sign_in_description": "O inicie sesión usando su correo y contraseña.", - "forgot_password_link": "¿Olvidó su contraseña?", - "create_account_link": "Crear una cuenta", - "continue_with_google": "Continuar con Google", - "continue_with_facebook": "Continuar con Facebook", - "submit": "Iniciar sesión", - "submit_pending": "Iniciando sesión…", - "success": "¡Éxito!", - "success_message": "Se ha registrado como {{username}}.", - "redirecting_google": "Redireccionando a Google…", - "redirecting_facebook": "Redireccionando a Facebook…", - "loading_google": "Iniciando sesión con Google…", - "loading_facebook": "Iniciando sesión con Facebook…", - "error": "¡Error!", - "error_message": "Ha ocurrido un error en el registro.", - "error_message_google": "Ha ocurrido un error al registrarse con Google.", - "error_message_facebook": "Ha ocurrido un error al registrarse con Facebook." - }, - "reset_password": { - "header": "Cambiar contraseña", - "instructions": "Indique su email para cambiar contraseña", - "submit": "Cambiar contraseña", - "success": "¡Éxito!", - "success_message": "Le hemos enviado un correo a <0>{{email}} con instrucciones para cambiar su contraseña.", - "error": "¡Error!", - "error_message": "Ocurrió un error al cambiar su contraseña.", - "confirm_button": "¡Entendido!" - }, - "set_password": { - "header": "Crear una nueva contraseña", - "submit": "Enviar", - "success": "¡Éxito!", - "success_message": "Su contraseña ha sido actualizada.", - "error": "¡Error!", - "error_message": "Ocurrió un error al actualizar su contraseña.", - "confirm_button": "¡Entendido!" - }, - "activate": { - "success": "¡Éxito!", - "success_message": "Su cuenta ha sido activada. Por favor, inicie sesión para comenzar a usar el sitio como usuario registrado.", - "success_message_educator": "Su cuenta ha sido activada, pero deberá ser aprobada por un administrador antes de que pueda acceder a contenidos de educador. Recibirá un correo cuando haya sido aprobado.", - "error": "¡Error!", - "error_message": "Ocurrió un error al activar su cuenta.", - "loading": "Activando su cuenta…", - "confirm_button": "¡Entendido!" - }, - "join": { - "as_students": "Soy estudiante", - "as_educators": "Soy educador", - "header_students": "Aprenda astronomía de verdad", - "header_educators": "Enseñe astronomía de verdad", - "description_students": "Únase para aprender cómo los astrónomos aprenden e interactúan con datos astronómicos reales. Gratis para siempre.", - "description_educators": "Únase para acceder a datos y herramientas astronómicas reales para su clase. Gratis para siempre.", - "sign_in_link": "¿Ya tiene una cuenta?", - "continue_with_google": "Continuar con Google", - "continue_with_facebook": "Continuar con Facebook", - "sign_up_with_email": "Registrarse con un correo" - }, - "register": { - "header": "Registrarse", - "header_students": "Registrarse como estudiante", - "header_educators": "Registrarse como educador", - "instructions_students": "Por favor, complete lo siguiente para crear una cuenta de estudiante", - "instructions_educators": "Por favor, complete lo siguiente para crear una cuenta de educador", - "submit_button": "Registrarse", - "success_students": "Activación pendiente", - "success_message_students": "Su cuenta ha sido creada, pero la activación está pendiente. Recibirá un correo con instrucciones para activar su cuenta y disfrutar de acceso a datos y herramientas astronómicas reales. <0>Gratis para siempre.", - "success_educators": "Activación pendiente", - "success_message_educators": "Su cuenta ha sido creada, pero la activación está pendiente. Recibirá un correo con instrucciones para activar su cuenta y disfrutar de acceso a datos y herramientas astronómicas reales. <0> Gratis para siempre.", - "confirm_button": "¡Entendido!", - "loading": "Registrando…", - "error": "¡Error!", - "error_message": "Ha ocurrido un error al iniciar sesión con {{service}}." - }, - "sso": { - "success": "¡Éxito!", - "success_pending": "Activación pendiente", - "success_message": "Ha iniciado sesión como {{username}}.", - "success_message_pending": "Su cuenta ha sido creada, pero la activación está pendiente. Recibirá un correo con instrucciones para activar su cuenta y disfrutar de acceso a datos y herramientas astronómicas reales. <0>Gratis para siempre." - }, - "account": { - "header": "Cuenta", - "profile": "Perfil", - "preferences": "Preferencias", - "password": "Contraseña", - "loading": "Cargando…", - "edit": "Editar", - "submit_profile": "Actualizar perfil y preferencias", - "submit_password": "Actualizar contraseña", - "submit_pending": "Enviando…", - "submit_success": "¡Actualizado!", - "error": "Ocurrió un error al obtener la información de su cuenta.", - "error_session_expired": "Su sesión ha expirado. Por favor, inicie sesión nuevamente.", - "error_delete_failed": "Ocurrió un error al enviar su solicitud de eliminación. Actualice la página e intente otra vez.", - "delete": "Eliminar mi cuenta", - "delete_pending": "Enviando…", - "delete_confirm_header": "¿Eliminar cuenta?", - "delete_confirm_description": "Lamentamos verle partir, pero siempre será bienvenido de vuelta. Estaremos aquí para usted, con investigaciones gratis para todos.. <0>¿Está seguro de que quiere eliminar su cuenta?", - "delete_success_header": "Su solicitud para eliminar su cuenta ha sido aceptada", - "delete_success_description": "Hemos recibido su solicitud. Un administrador eliminará su cuenta prontamente.", - "delete_confirm_label": "¡Sí, eliminar!", - "delete_error_header": "¡Error!" - }, - "glossary": { - "glossary-term": "Término del glosario", - "back_button": "Volver al glosario", - "loading": "Cargando el Glosario..." - }, - "restricted": { - "header_approval_pending": "Contenido no disponible aún", - "message_approval_pending": "Este contenido aún no está disponible, pues su aprobación está pendiente. Si desea acelerar el proceso, por favor envíe un correo a <0>epo-feedback@lsst.org.", - "header_restricted": "Contenido no disponible", - "message_restricted": "Ese contenido no está disponible para usted. Si cree que esto es un error, por favor envíe un correo a <0>epo-feedback@lsst.org.", - "header_not_authorized": "Contenido no disponible", - "message_not_authorized": "Debe iniciar session para acceder a esta página.", - "header_deletion_pending": "Contenido no disponible", - "message_deletion_pending": "Ha solicitado eliminar su cuenta. El proceso está pendiente. Si cree que está visualizando este mensaje por error, por favor envíe un correo a <0>epo-feedback@lsst.org.", - "button_confirm": "¡Entendido!" - }, - "investigation": { - "investigation": "Investigación", - "total_duration": "Duración total de la investigación", - "start": "Comenzar investigación", - "coming_soon": "(Pronto)", - "early_access": { - "full": "Acceso Anticipado", - "line_1_text": "Acceso", - "line_2_text": "Anticipado" - } - }, - "share": { - "label_current": "Compartir la página actual", - "label_item": "Compartir {{title}}", - "copy_url": "Copiar URL", - "email": "Correo", - "email_subject": "¡Hola! Mira esta información de la web del Observatorio Rubin: {{title}}", - "facebook_quote": "¡Hola! Mira esta información de la web del Observatorio Rubin: {{title}}", - "twitter_title": "¡Hola! Mira esta información de la web del Observatorio Rubin: {{title}}" - }, - "summit_dashboard": { - "unit_localization": { - "north_abbr": "N", - "south_abbr": "S", - "east_abbr": "E", - "west_abbr": "O" - } - } - } -} diff --git a/lib/localeStrings/index.js b/lib/localeStrings/index.js deleted file mode 100644 index 268ab6d0..00000000 --- a/lib/localeStrings/index.js +++ /dev/null @@ -1,8 +0,0 @@ -// Import a translation file for each available language -import en from "./en.json"; -import es from "./es.json"; - -export const localeData = { - en, - es, -}; diff --git a/package.json b/package.json index abbb5aeb..b1de2f66 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "graphql-request": "^5.0.0", "hoist-non-react-statics": "^3.3.2", "i18next": "^22.0.4", + "i18next-resources-to-backend": "^1.2.1", "json-to-scss": "^1.6.2", "jwt-decode": "^3.1.2", "next": "^14.2.3", @@ -114,8 +115,7 @@ "eslint-plugin-storybook": "^0.6.11", "eslint-plugin-unused-imports": "^2.0.0", "husky": "^8.0.3", - "i18next-browser-languagedetector": "^7.0.1", - "i18next-http-backend": "^2.2.0", + "i18next-browser-languagedetector": "^8.0.0", "jsconfig-paths-jest-mapper": "^1.0.0", "lint-staged": "^13.0.3", "local-cors-proxy": "^1.1.0", diff --git a/pages/[[...uriSegments]].js b/pages/[[...uriSegments]].js index fe25c91f..2cd41e85 100644 --- a/pages/[[...uriSegments]].js +++ b/pages/[[...uriSegments]].js @@ -24,7 +24,7 @@ import internalLinkShape, { import siteInfoShape from "@/shapes/siteInfo"; import footerContentShape from "@/shapes/footerContent"; import rootPagesShape from "@/shapes/rootPages"; -import { updateI18n } from "@/lib/i18n"; +import { updateI18n } from "@/lib/i18n/client"; import { setEdcLog } from "@/lib/edc-log"; import { purgeNextjsStaticFiles } from "@/lib/purgeStaticFiles"; const glob = require("glob"); diff --git a/pages/_app.js b/pages/_app.js index 7dc7f5e7..80a23a24 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,13 +1,14 @@ /* eslint-disable unused-imports/no-unused-imports */ import PropTypes from "prop-types"; -import "@/lib/i18n"; +import i18n from "@/lib/i18n/client"; import "focus-visible"; import Script from "next/script"; import { AuthenticationContextProvider } from "@/contexts/Authentication"; import useAuthentication from "@/hooks/useAuthentication"; import GlobalStyles from "@/styles/globalStyles"; import styles from "@/styles/styles.scss"; +import { I18nextProvider } from "react-i18next"; const PLAUSIBLE_DOMAIN = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN; const SURVEY_SPARROW = process.env.NEXT_PUBLIC_SURVEY_SPARROW; @@ -22,22 +23,23 @@ function Client({ Component, pageProps }) { }); return ( - - {PLAUSIBLE_DOMAIN && ( -