Skip to content

Conversation

@darcywong00
Copy link
Contributor

@darcywong00 darcywong00 commented Sep 9, 2025

Replaces the PR chain #516 in addressing #384 of localizing the keyboard search

From the A19S11 sprint design meeting, we pivoted from using .po locale files to localizing strings in associative arrays.

This PR reuses earlier Locale.php work to go in that direction on the main keyboard search page.
For now, locale is passed with the query parameter lang= (e.g. lang=es or lang=fr).

This also adds a globe icon to the top right for selecting UI language (passing the lang parameter)

image

Limitation: Currently session is set in keyboards/session.php so the rest of the site is ignoring the lang query parameter

TODO's on follow-on PRs

User Testing

Setup - Run the keyman.com and website-local-proxy sites locally with docker

  • TEST_ENGLISH - Verifies default strings
  1. Navigate to http://keyman.com.localhost/keyboards
  2. Verify English strings match the live site on https://keyman.com/keyboards
  • TEST_SPANISH - Verifies search page appear in Spanish
  1. Navigate to http://keyman.com.localhost/keyboards
  2. Hover on the globe key in the top right (next to the rest donate section) and select Spanish - Español
  3. Verify the main keyboard search page displays in Spanish
image
  • TEST_FRENCH - Verifies search page appear in French
  1. Navigate to http://keyman.com.localhost/keyboards?lang=fr-FR
  2. Hover on the globe key in the top right (next to the rest donate section) and select French - Français
  3. Verify keyboard search page is in French
image

@keymanapp-test-bot
Copy link

keymanapp-test-bot bot commented Sep 9, 2025

User Test Results

Test specification and instructions

Copy link
Member

@mcdurdin mcdurdin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice clean start. But I am asking for a bunch of changes to make it more extensible long-term. Thanks!

Comment on lines 14 to 18
public const CROWDIN_LOCALES = array(
'en',
'es-ES',
'fr-FR'
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be in a separate file, or perhaps determined on a per-domain basis, because we have multiple l10n domains (i.e. keyboards is one localization domain). See my comments re filenames and folder names further down

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with the glob refactor below, this can go away

Comment on lines 54 to 70
public static function validateLocale($locale) {
return in_array($locale, Locale::CROWDIN_LOCALES);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may not be viable because we may not have localizations for a given l10n domain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give an example?
We can set the list of available languages on
https://crowdin.com/project/keymancom/settings#languages

(would need the sil_ltops login to add custom languages)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed with the glob refactor.

private static $langArrayEn = [];

/**
* Return the current locale. Fallback to 'en'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback should really be through a path, e.g. given es-ES, we should fallback first to es, then to en.

Copy link
Contributor Author

@darcywong00 darcywong00 Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should setLocale() split on dashes for determining fallback.
e.g. with es-ES we end up with

self::$currentLocales = ['es', 'es-ES', 'en'];

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are algorithms for fallback already. See https://cldr.unicode.org/development/development-process/design-proposals/language-distance-data

That might be too much for us. @srl295 may have suggestions on simpler approaches.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to calculateFallbackLocales() where we can always reimplement in a TODO

Comment on lines 16 to 17
'es-ES',
'fr-FR'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should be minimal BCP-47 tags - i.e. we would use es-419 for Latin America and Caribbean, es for Spain. Similar for French, should be just fr, not fr-FR (we would use fr-CA for example).

* @param $s - the format string
* @param $args - optional remaining args to the format string
*/
public static function _s($s, ...$args) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto on no _ needed. I would have the one public function though (and make _m into a private getString()):

public static function m($domain, $id, ...args) {
  $str = self::getString($domain, $id);
  if(count($args) == 0) return $str;
  return vsprintf($str, $args);
}

* Return the current locale. Fallback to 'en'
* @return $currentLocale
*/
public static function currentLocale() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return an array of available locales in priority order, e.g. for es-ES, we'd go ['es-ES', 'es', 'en']. This way, we can support fallback more neatly in the future, even if we initially only go with one fallback to en.

$head_options = [
'title' =>'Keyboard Search',
'description' => 'Keyman Keyboard Search',
'title' =>Locale::_m('page_title'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'title' =>Locale::_m('page_title'),
'title' => _m('page_title'),

use Keyman\Site\com\keyman\templates\Foot;
use Keyman\Site\com\keyman\Locale;

Locale::localize('keyboards');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// To make the localization inserts cleaner and DRY, we can do this:

Suggested change
Locale::localize('keyboards');
function _m($id) { return Locale::m('keyboards', $id); }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it get messy if every page we localize has to add?

function_m($id) {...}

or should we just move it to Localize.php()?

Comment on lines 35 to 48
if(isset($_REQUEST['lang'])) {
\Keyman\Site\com\keyman\Locale::overrideCurrentLocale($_REQUEST['lang']);
} else if (isset($_SESSION['lang'])) {
\Keyman\Site\com\keyman\Locale::overrideCurrentLocale($_SESSION['lang']);
}
$_SESSION['lang'] = \Keyman\Site\com\keyman\Locale::currentLocale();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to look at best practices around embedding locale in query strings vs path or domain etc. We need to consider our technology stack in making this decision.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I've copied this as a bullet point on the issue to investigate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From SO: "Does the locale belong on the path or as a request parameter on the URL?
https://stackoverflow.com/a/16471122

If you need it as request parameter or part of the url depends of what you want to achieve. If you want to serve static content, you should have it be part of the path. If you want to act dynamically on the chosen locale, you should use it as request parameter, since you don't want to have your scripts replicated several times over different paths just to add different locales.

@darcywong00 darcywong00 modified the milestones: A19S11, A19S12 Sep 13, 2025
* @param $id - the id for the string
* @param $args - optional remaining args to the format string
*/
public static function m($domain, $id, ...$args) {
Copy link
Contributor

@ermshiperete ermshiperete Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why m?

I might overlook the obvious, but currently I have a hard time seeing a connection between m and getting a localized string. I could make sense of s, t, or l, but for m I'm missing the association (which then makes it harder to use the function if I can't remember it).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to any clear name. In the previous iteration #516, we were using Locale::_s() for formatted strings.
That version was also using _ gettext, but we're not setting locales this time.

@darcywong00 darcywong00 force-pushed the feat/localize/keyboards-map branch from 739ee14 to 70f48e6 Compare September 16, 2025 01:55

return [
# Page Title
"page_title" => "Keyboard Search",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One drawback of not using the .po format is we lose the in-line context comments on Crowdin.

The map file is quite cryptic

Image

Crowdin allows us to use an accompanying Markdown file to provide some context, but it covers the entire file

Image

@darcywong00
Copy link
Contributor Author

darcywong00 commented Sep 17, 2025

I think this is ready for another round of review comments.

This one is still unresolved.
#604 (comment)

It would be good to look at best practices around embedding locale in query strings vs path or domain etc. We need to consider our technology stack in making this decision.

For now, I'm just using ?lang=es or ?lang=fr for testing.

@darcywong00 darcywong00 marked this pull request as ready for review September 17, 2025 01:06
@ermshiperete
Copy link
Contributor

ermshiperete commented Sep 17, 2025

This one is still unresolved. #604 (comment)

It would be good to look at best practices around embedding locale in query strings vs path or domain etc. We need to consider our technology stack in making this decision.

For now, I'm just using ?lang=es or ?lang=fr for testing.

Can we use the Accept-Language header for this? (Accept-Language, qa-lang-priorities)

'description' => 'Keyman Keyboard Search',
'title' => _m('page_title'),
'description' => _m('page_description'),
'language' => isset($_SESSION['lang']) ? $_SESSION['lang'] : 'en',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will set the lang attribute for the entire page.

Unless we need to apply it separately to all the

tags involving localized strings below...

@darcywong00
Copy link
Contributor Author

Can we use the Accept-Language header for this?

Maybe as a starting point? We do eventually need a way for:

  • desktop users to toggle the UI language with a dropdown selector (next to "Support" in the top right banner)
  • Keyman mobile apps to pass their UI locale to the embedded keyboard search. (which could be different from the device's locale)

@darcywong00 darcywong00 force-pushed the feat/localize/keyboards-map branch from 6b929d9 to 76c9d0a Compare September 19, 2025 01:06
@darcywong00 darcywong00 force-pushed the feat/localize/keyboards-map branch from e687164 to ed1c38b Compare October 27, 2025 03:39
@darcywong00 darcywong00 marked this pull request as draft October 29, 2025 03:23
@darcywong00 darcywong00 marked this pull request as ready for review October 31, 2025 02:42
@keyman-server keyman-server modified the milestones: A19S15, A19S16 Nov 8, 2025
Copy link
Member

@mcdurdin mcdurdin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go with this -- it's reasonably generalized now and we can at least move forward with some localization!

private static function render_globe_dropdown(): void {
?>
<p>
<div id="ui-language" class="menu-item">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will result in two divs with the same id, which is not really right. We need to keep them unique.

Copy link
Contributor Author

@darcywong00 darcywong00 Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to pass a div number so we can assign "id" = ui-language or ui-language1
in f1f8a62

@darcywong00
Copy link
Contributor Author

Hmm. something to chase down on why the link checker is now 7x longer
Reference GHA
https://github.com/keymanapp/keyman.com/actions/runs/19664278223/job/56317324603?pr=624

PR Links Found Links Excluded Elapsed Time
#750 (baseline) 318,228 315,763 21 minutes, 24 seconds
This PR 2,377,385 2,366,016 1 hour, 50 minutes

@darcywong00 darcywong00 changed the title feat: Use associate array for strings on keyboard search page 🗺️ feat: Use associative array for strings on keyboard search page 🗺️ Dec 1, 2025
@darcywong00
Copy link
Contributor Author

And the link checker is back to 20 min with 4fe6b0c

@Meng-Heng
Copy link
Collaborator

Meng-Heng commented Dec 3, 2025

Prerequisites

  1. Fetch & Pull feat/localize/keyboards-map
  2. Run keyman.com & website-local-proxy on Chrome

Test Results

  • TEST_ENGLISH (PASSED):
  1. Open http://keyman.com.localhost:8053/keyboards/
  2. Verified: The page content is in English & match the content on keyman.com/keyboards.
  • TEST_SPANISH (PASSED):
  1. Return to Home page, in English
  2. Open http://keyman.com.localhost:8053/keyboards/, the page content remains in English.
  3. Hover the globe key, click on Español
  4. Verified: The page content translates to Spanish.
  • TEST_FRENCH (PASSED):
  1. Return to Home page, in Spanish
  2. Open http://keyman.com.localhost:8053/keyboards/, the page content is still in Spanish
  3. Hover the globe key, click on Français
  4. Verified: The page content translates to French
  5. Return to Home page, in French
  6. Open http://keyman.com.localhost:8053/keyboards/, the page content remains in French
  7. Hover on the globe key, click on English
  8. Return to Home page, open http://keyman.com.localhost:8053/keyboards/?lang=fr-Fr
  9. Verified: the page opens up in French.

@darcywong00 darcywong00 merged commit 0e1b284 into master Dec 3, 2025
5 checks passed
@darcywong00 darcywong00 deleted the feat/localize/keyboards-map branch December 3, 2025 04:44
@github-project-automation github-project-automation bot moved this from Todo to Done in Keyman Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

6 participants