From a0e52f1f7c0d132bc16b801aea17f77694f7f53d Mon Sep 17 00:00:00 2001 From: SherlynS Date: Sun, 12 Jul 2026 22:04:07 -0400 Subject: [PATCH] Add full Spanish (es-ES) translation support for Asgardeo JavaScript SDK --- .changeset/spanish-translation-bundle.md | 5 + CONTRIBUTING.md | 50 ++++- .../constants/TranslationBundleConstants.ts | 5 +- packages/i18n/src/translations/es-ES.ts | 192 ++++++++++++++++++ packages/i18n/src/translations/index.ts | 17 +- 5 files changed, 257 insertions(+), 12 deletions(-) create mode 100644 .changeset/spanish-translation-bundle.md create mode 100644 packages/i18n/src/translations/es-ES.ts diff --git a/.changeset/spanish-translation-bundle.md b/.changeset/spanish-translation-bundle.md new file mode 100644 index 000000000..aeac9cf17 --- /dev/null +++ b/.changeset/spanish-translation-bundle.md @@ -0,0 +1,5 @@ +--- +"@asgardeo/i18n": minor +--- + +Added to the Asgardeo JavaScript SDK support for Spanish (Español - es-ES). Overall accessibility for Spanish-speaking users. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d41c0e759..b0f82966f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -239,9 +239,11 @@ export {default as fr_FR} from './fr-FR'; Build the package and test the new language in a sample application to ensure all translations are working correctly: ```bash -pnpm build --filter @asgardeo/i18n +pnpm nx build @asgardeo/i18n ``` +> **Note:** This repo uses [Nx](https://nx.dev/), so build a single package with the Nx syntax above. Don't use `pnpm build --filter @asgardeo/i18n` — Nx doesn't recognize `--filter` and passes it to `tsc`, which fails with `error TS5023: Unknown compiler option '--filter'`. + To test your new language translation, you have two options: ##### Option 1: Using a symlink @@ -306,9 +308,50 @@ pnpm install # The sample will automatically use your local i18n package changes ``` +The samples reference the workspace packages via `workspace:*`, so `pnpm install` symlinks them to your local `packages/*` and your changes are picked up without any extra configuration. + +> [!IMPORTANT] +> After changing translation files, rebuild the i18n package so the sample (which imports the built `dist/`, not the source) sees your changes: +> +> ```bash +> npx nx build @asgardeo/i18n +> ``` + +###### Configure the sample with a `.env.local` file + +The sample apps don't ship with any Asgardeo credentials — they read their configuration from environment variables at runtime (e.g. `import.meta.env.VITE_ASGARDEO_BASE_URL`). These values are specific to **your** Asgardeo organization and application, so they must not be committed. For that reason, `.env.local` is git-ignored (only a `.env.local.example` template is committed), and you need to create your own before the sample can connect to Asgardeo. + +Vite automatically loads `.env.local` on `pnpm dev` and exposes every `VITE_`-prefixed variable to the app. Without this file, the base URL and client ID are `undefined` and sign-in/sign-up won't work. + +1. Copy the template: + +```bash +cp .env.local.example .env.local +``` + +2. Fill in the values from your Asgardeo application (Console → **Applications → your app → Protocol / Info**): + +```bash +VITE_ASGARDEO_BASE_URL='https://api.asgardeo.io/t/' +VITE_ASGARDEO_CLIENT_ID='' +``` + +3. Start the dev server (from the sample directory): + +```bash +pnpm dev +``` + +###### Explore the running app and confirm your translations + +Open the URL the dev server prints (`teamspace-react` runs on `https://localhost:[host-number]`. If you get a warning on the site just accept the self-signed cert warning the first time) and walk through the auth screens. + +> [!NOTE] +> In the embedded sign-in/sign-up flows, most in-form text — headings, subtitles, input field labels, and the form's own submit/social buttons — is served by the Asgardeo server, not your bundle, so it stays in English regardless of your translation. Your bundle reliably translates error/validation messages and standalone SDK buttons (like the Sign In / Sign Up call-to-action buttons). Getting that server-driven text into your language is something you'll need to explore separately in the Asgardeo Console (its text/branding and flow settings) while testing. + ##### Testing with AsgardeoProvider -Once you have your testing environment set up (using either option above), configure the AsgardeoProvider to use your new language: +Once you have your testing environment set up (using either option above), configure the AsgardeoProvider to use your new language. The following steps take place in `samples/teamspace-react/src/main.tsx`: - **Import your new language bundle** @@ -340,6 +383,9 @@ For more details on the i18n preferences interface, see the [`I18nPreferences`]( - **Verify the translations** +> [!NOTE] +> This checklist is aspirational — it assumes every string comes from your bundle. As noted under [Explore the running app](#explore-the-running-app-and-confirm-your-translations), most in-form auth text is server-driven and won't reflect your bundle, so don't expect "all UI text" to change. Focus verification on the bundle-driven strings (errors, validation, standalone SDK buttons, post-login UI). + - Navigate through your application's authentication flows - Check that all UI text appears in your new language - Verify that buttons, labels, error messages, and other UI elements display the correct translations diff --git a/packages/i18n/src/constants/TranslationBundleConstants.ts b/packages/i18n/src/constants/TranslationBundleConstants.ts index f292729dd..aeb7b13a4 100644 --- a/packages/i18n/src/constants/TranslationBundleConstants.ts +++ b/packages/i18n/src/constants/TranslationBundleConstants.ts @@ -16,7 +16,7 @@ * under the License. */ -import {en_US} from '../translations'; +import {en_US, es_ES} from '../translations'; /** * Constants related to internationalization (i18n) translation bundles. @@ -39,8 +39,9 @@ const TranslationBundleConstants: { * * Current default locales: * - `en-US` - English (United States) + * - `es-ES` - Spanish (Spain) */ - DEFAULT_LOCALES: [en_US.metadata.localeCode], + DEFAULT_LOCALES: [en_US.metadata.localeCode, es_ES.metadata.localeCode], /** * Default locale code used as fallback when no specific locale is provided. diff --git a/packages/i18n/src/translations/es-ES.ts b/packages/i18n/src/translations/es-ES.ts new file mode 100644 index 000000000..a55dce382 --- /dev/null +++ b/packages/i18n/src/translations/es-ES.ts @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* eslint-disable sort-keys */ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { I18nTranslations, I18nMetadata, I18nBundle } from '../models/i18n'; + +const translations: I18nTranslations = { + /* |---------------------------------------------------------------| */ + /* | Elements | */ + /* |---------------------------------------------------------------| */ + + /* Buttons */ + 'elements.buttons.signin.text': 'Iniciar sesión', + 'elements.buttons.signout.text': 'Cerrar sesión', + 'elements.buttons.signup.text': 'Registrarse', + 'elements.buttons.submit.text': 'Continuar', + 'elements.buttons.facebook.text': 'Continuar con Facebook', + 'elements.buttons.google.text': 'Continuar con Google', + 'elements.buttons.github.text': 'Continuar con GitHub', + 'elements.buttons.microsoft.text': 'Continuar con Microsoft', + 'elements.buttons.linkedin.text': 'Continuar con LinkedIn', + 'elements.buttons.ethereum.text': 'Continuar con Iniciar sesión con Ethereum', + 'elements.buttons.smsotp.text': 'Continuar con SMS OTP', + 'elements.buttons.multi.option.text': 'Continuar con {connection}', + 'elements.buttons.social.text': 'Continuar con {connection}', + + /* Display */ + 'elements.display.divider.or_separator': 'O', + 'elements.display.copyable_text.copy': 'Copiar', + 'elements.display.copyable_text.copied': 'Copiado!', + + /* Fields */ + 'elements.fields.generic.placeholder': 'Ingresa tu {field}', + 'elements.fields.username.label': 'Nombre de usuario', + 'elements.fields.username.placeholder': 'Ingresa tu nombre de usuario', + 'elements.fields.password.label': 'Contraseña', + 'elements.fields.password.placeholder': 'Ingresa tu Contraseña', + 'elements.fields.first_name.label': 'Nombre', + 'elements.fields.first_name.placeholder': 'Ingresa tu nombre', + 'elements.fields.last_name.label': 'Apellido', + 'elements.fields.last_name.placeholder': 'Ingresa tu apellido', + 'elements.fields.email.label': 'Correo electrónico', + 'elements.fields.email.placeholder': 'Correo electrónico', + 'elements.fields.organization.name.label': 'Nombre de la organización', + 'elements.fields.organization.handle.label': 'Identificador de la organización', + 'elements.fields.organization.description.label': 'Descripción de la organización', + 'elements.fields.organization.select.label': 'Seleccionar organización', + 'elements.fields.organization.select.placeholder': 'Elige una organización', + + /* Validation */ + 'validations.required.field.error': 'Este campo es obligatorio', + + /* |---------------------------------------------------------------| */ + /* | Widgets | */ + /* |---------------------------------------------------------------| */ + + /* Base Sign In */ + 'signin.heading': 'Iniciar sesión', + 'signin.subheading': 'Bienvenido de nuevo! Por favor, inicia sesión para continuar.', + + /* Base Sign Up */ + 'signup.heading': 'Regístrate', + 'signup.subheading': 'Crea una nueva cuenta para empezar.', + + /* Email OTP */ + 'email.otp.heading': 'Verificación de OTP', + 'email.otp.subheading': 'Ingresa el código que te enviamos a tu correo electrónico.', + 'email.otp.buttons.submit.text': 'Continuar', + + /* Identifier First */ + 'identifier.first.heading': 'Iniciar sesión', + 'identifier.first.subheading': 'Ingresa tu nombre de usuario o dirección de correo.', + 'identifier.first.buttons.submit.text': 'Continuar', + + /* SMS OTP */ + 'sms.otp.heading': 'Verificación de OTP', + 'sms.otp.subheading': 'Ingresa el código que te enviaron a tu número de teléfono.', + 'sms.otp.buttons.submit.text': 'Continuar', + + /* TOTP */ + 'totp.heading': 'Verifica tu identidad', + 'totp.subheading': 'Introduce el código de tu aplicación de autenticación.', + 'totp.buttons.submit.text': 'Continuar', + + /* Username Password */ + 'username.password.heading': 'Iniciar sesión', + 'username.password.subheading': 'Ingresa tu usuario y contraseña para continuar.', + 'username.password.buttons.submit.text': 'Continuar', + + /* Passkeys */ + 'passkey.button.use': 'Iniciar sesión con clave de acceso', + 'passkey.signin.heading': 'Iniciar sesión con clave de acceso', + 'passkey.register.heading': 'Registrar clave de acceso', + 'passkey.register.description': 'Crea una clave de acceso para iniciar sesión en tu cuenta de manera segura sin contraseña.', + + /* |---------------------------------------------------------------| */ + /* | User Profile | */ + /* |---------------------------------------------------------------| */ + + 'user.profile.heading': 'Perfil', + 'user.profile.update.generic.error': 'Ocurrió un error al actualizar tu perfil. Por favor, inténtalo de nuevo.', + + /* |---------------------------------------------------------------| */ + /* | Organization Switcher | */ + /* |---------------------------------------------------------------| */ + + 'organization.switcher.switch.organization': 'Cambiar de organización', + 'organization.switcher.loading.placeholder.organizations': 'Cargando organizaciones...', + 'organization.switcher.members': 'miembros', + 'organization.switcher.member': 'miembro', + 'organization.switcher.create.organization': 'Crear organización', + 'organization.switcher.manage.organizations': 'Administrar organizaciones', + 'organization.switcher.buttons.manage.text': 'Administrar', + 'organization.switcher.organizations.heading': 'Organizaciones', + 'organization.switcher.buttons.switch.text': 'Cambiar', + 'organization.switcher.no.access': 'Sin Acceso', + 'organization.switcher.status.label': 'Estatus:', + 'organization.switcher.showing.count': 'Mostrando {showing} de {total} organizaciones', + 'organization.switcher.buttons.refresh.text': 'Actualizar', + 'organization.switcher.buttons.load_more.text': 'Cargar más organizaciones', + 'organization.switcher.loading.more': 'Cargando...', + 'organization.switcher.no.organizations': 'No se encontraron organizaciones', + 'organization.switcher.error.prefix': 'Error:', + + 'organization.profile.heading': 'Perfil de la organización', + 'organization.profile.loading': 'Cargando organización...', + 'organization.profile.error': 'No se pudo cargar la organización', + + 'organization.create.heading': 'Crear organización', + 'organization.create.buttons.create_organization.text': 'Crear organización', + 'organization.create.buttons.create_organization.loading.text': 'Creando...', + 'organization.create.buttons.cancel.text': 'Cancelar', + + /* |---------------------------------------------------------------| */ + /* | Messages | */ + /* |---------------------------------------------------------------| */ + + 'messages.loading.placeholder': 'Cargando...', + + /* |---------------------------------------------------------------| */ + /* | Errors | */ + /* |---------------------------------------------------------------| */ + + 'errors.heading': 'Error', + 'errors.signin.components.not.available': 'El formulario de inicio de sesión no está disponible en este momento. Por favor, inténtalo de nuevo más tarde.', + 'errors.signin.initialization': 'Ocurrió un error al iniciar. Por favor, inténtalo de nuevo más tarde.', + 'errors.signin.flow.failure': 'Ocurrió un error durante el proceso de inicio de sesión. Por favor, inténtalo de nuevo más tarde.', + 'errors.signin.flow.completion.failure': + 'Ocurrió un error al completar el proceso de inicio de sesión. Por favor, inténtalo de nuevo más tarde.', + 'errors.signin.flow.passkeys.failure': 'Ocurrió un error al iniciar sesión con clave de acceso. Por favor, inténtalo de nuevo más tarde.', + 'errors.signin.flow.passkeys.completion.failure': + 'Ocurrió un error al completar el inicio de sesión con claves de acceso. Por favor, intenta de nuevo más tarde.', + 'errors.signin.timeout': 'Se terminó el tiempo para completar el paso.', + + 'errors.signup.initialization': 'Ocurrió un error al iniciar. Por favor, inténtalo de nuevo más tarde.', + 'errors.signup.flow.failure': 'Ocurrió un error durante el proceso de registro. Por favor, inténtalo de nuevo más tarde.', + 'errors.signup.flow.initialization.failure': + 'Ocurrió un error al iniciar el proceso de registro. Por favor, inténtalo de nuevo más tarde.', + 'errors.signup.components.not.available': 'El formulario de registro no está disponible en este momento. Por favor, inténtalo de nuevo más tarde.', +}; + +const metadata: I18nMetadata = { + localeCode: 'es-ES', + countryCode: 'ES', + languageCode: 'es', + displayName: 'Español (España)', + direction: 'ltr', +}; + +const es_ES: I18nBundle = { + metadata, + translations, +}; + +export default es_ES; diff --git a/packages/i18n/src/translations/index.ts b/packages/i18n/src/translations/index.ts index ebb1a139c..c95692c5a 100644 --- a/packages/i18n/src/translations/index.ts +++ b/packages/i18n/src/translations/index.ts @@ -16,11 +16,12 @@ * under the License. */ -export {default as en_US} from './en-US'; -export {default as fr_FR} from './fr-FR'; -export {default as te_IN} from './te-IN'; -export {default as hi_IN} from './hi-IN'; -export {default as pt_BR} from './pt-BR'; -export {default as pt_PT} from './pt-PT'; -export {default as ta_IN} from './ta-IN'; -export {default as si_LK} from './si-LK'; +export { default as es_ES } from './es-ES'; +export { default as en_US } from './en-US'; +export { default as fr_FR } from './fr-FR'; +export { default as te_IN } from './te-IN'; +export { default as hi_IN } from './hi-IN'; +export { default as pt_BR } from './pt-BR'; +export { default as pt_PT } from './pt-PT'; +export { default as ta_IN } from './ta-IN'; +export { default as si_LK } from './si-LK';