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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ jobs:
- name: Check broken links
shell: bash
continue-on-error: false
# Exclude checking locales for each link 'lang=*'
run: |
set +e
set +o pipefail
npx broken-link-checker http://localhost:8053/_test --recursive --ordered ---host-requests 50 -e --filter-level 3 --exclude '*/donate' | tee blc.log
npx broken-link-checker http://localhost:8053/_test --recursive --ordered ---host-requests 50 -e --filter-level 3 --exclude '*/donate' --exclude '*lang=*' | tee blc.log
echo "BLC_RESULT=${PIPESTATUS[0]}" >> "$GITHUB_ENV"

- name: Report on broken links
Expand Down
56 changes: 56 additions & 0 deletions _includes/2020/templates/Menu.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,56 @@ public static function render(array $fields): void {
Menu::render_top_menu($fields);
}

/**
* Generate the URL with query to change the UI language
* @param language - language tag to use
*/
private static function change_ui_language($language): string {
// Parse the current URI for populating the UI dropdown
$url = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
$parts = parse_url($url);

if (!empty($parts['query'])) {
parse_str($parts['query'], $queryParams);
} else {
$queryParams = [];
}

// Set the language query
$queryParams['lang'] = $language;
$query = http_build_query($queryParams);

return $parts['path'] . "?" . $query;
}

/**
* Render the globe dropdown for changing the UI language.
* As UI languages get added, we'll need to update this
* @param number - Div number, default 0.
*/
private static function render_globe_dropdown($number = 0): void {
$divID = ($number == 1) ? "ui-language1" : "ui-language";
echo <<<END
<p>
<div id='$divID' class="menu-item">
END;
?>
<img src="<?php echo Util::cdn("img/globe.png"); ?>" alt="UI globe dropdown" />
<div class="menu-item-dropdown">
<div class="menu-dropdown-inner">
<ul>
<!-- Just use autonyms -->
<li><a href="<?= Menu::change_ui_language('en'); ?>">English</a></li>
<li><a href="<?= Menu::change_ui_language('es'); ?>">Español</a></li>
<li><a href="<?= Menu::change_ui_language('fr'); ?>">Français</a></li>
</ul>
</div>
</div>
</div>
</p>
<?php
}

private static function render_phone_menu(object $fields): void {
?>

Expand Down Expand Up @@ -117,6 +167,9 @@ private static function render_top_menu(object $fields): void {
</form>
<p id="donate"><a href="/donate">Donate</a></p>
<p><a href="<?= KeymanHosts::Instance()->help_keyman_com ?>" target="blank">Support<img src="<?php echo Util::cdn("img/helpIcon.png"); ?>" alt="help icon"></a></p>
<?php
Menu::render_globe_dropdown();
?>
</div>
</div>
<div id="top-menu-bg"></div>
Expand All @@ -132,6 +185,9 @@ private static function render_top_menu(object $fields): void {
</form>
<a id='help1-donate' href="/donate">Donate</a>
<a href="<?= KeymanHosts::Instance()->help_keyman_com ?>"><img id="top-menu-icon2" src="<?php echo Util::cdn("img/helpIcon.png"); ?>" alt="help icon" /></a>
<?php
Menu::render_globe_dropdown(1);
?>
</div>
<div class="wrapper">
<div class="menu-item" id="keyboards">
Expand Down
6 changes: 5 additions & 1 deletion _includes/autoload.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
if(preg_match('/^Keyman\\\\Site\\\\com\\\\keyman\\\\(.+)/', $class_name, $matches)) {
// Fix namespace pathing for Linux
$filename = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $matches[1]);
$success = include(__DIR__ . "/2020/{$filename}.php");
if ($filename == 'Locale') {
$success = include(__DIR__ . "/locale/{$filename}.php");
} else {
$success = include(__DIR__ . "/2020/{$filename}.php");
}
if($success === FALSE) {
debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
die("Unable to find class $class_name");
Expand Down
3 changes: 3 additions & 0 deletions _includes/includes/template.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

// *Don't* use autoloader here because of potential side-effects in older pages
require_once(__DIR__ . '/../2020/Util.php');
require_once(__DIR__ . '/../locale/Locale.php');
require_once(__DIR__ . '/../../_common/KeymanVersion.php');
require_once(__DIR__ . '/../2020/templates/Head.php');

use Keyman\Site\com\keyman\Locale;

function template_finish($foot) {
//ob_end_flush();

Expand Down
169 changes: 169 additions & 0 deletions _includes/locale/Locale.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

/*
* Keyman is copyright (C) SIL Global. MIT License.
*/

declare(strict_types=1);

namespace Keyman\Site\com\keyman;

use \Keyman\Site\Common\KeymanHosts;

class Locale {
public const DEFAULT_LOCALE = 'en';

// array of the support locales
// xx-YY locale as specified in crowdin %locale%
private static $currentLocales = [];

// strings is an array of domains.
// Each domain is an array of locales
// Each locale is an object? with loaded flag and array of strings
private static $strings = [];

/**
* Return the current locales. Fallback to 'en'
* @return $currentLocales
*/
public static function currentLocales() {
return self::$currentLocales;
}

/**
* Set the current locales, with an array of fallbacks, ending in 'en'.
* @param $locale - the new current locale (xx-YY as specified in crowdin %locale%)
*/
public static function setLocale($locale) {
// Clear current locales
self::$currentLocales == [];

if (!empty($locale)) {
self::$currentLocales = self::calculateFallbackLocales($locale);
}

// Push default fallback locale to the end
array_push(self::$currentLocales, Locale::DEFAULT_LOCALE);
}

/**
* Load the strings for the given domain
* @param $domain - the domain of strings
* @return boolean - true if successfully loaded strings
*/
public static function loadDomain($domain) {
self::$strings[$domain] = [];
$path = __DIR__ . '/strings/' . $domain . '/*.php';
$files = glob(__DIR__ . '/strings/' . $domain . '/*.php');
if ($files == false) {
return false;
}
foreach ($files as $file) {
// files are named by locale
$file = pathinfo($file, PATHINFO_FILENAME);
self::$strings[$domain][$file] = (object)[
'strings' => [],
'loaded' => false
];
}
return true;
}

/**
* Reads localized strings from the specified $domain-locale.php file
* @param $domain - base filename of the .php file containing localized strings
* (name not including -xx-YY locale)
* path is relative to _includes/locale/
* @param $locale - locale for the strings to load
*/
public static function loadStrings($domain, $locale) {
$currentLocaleFilename = sprintf("%s/%s/%s",
__DIR__ . '/strings/',
$domain,
$locale . '.php');

if (file_exists($currentLocaleFilename)) {
self::$strings[$domain][$locale]-> strings = require $currentLocaleFilename;
self::$strings[$domain][$locale]-> loaded = true;
}
}

/**
* Given a locale, return an array of fallback locales
* For example: es-ES --> [es, es-ES]
* TODO: Use an existing fallback algorthim like
* https://cldr.unicode.org/development/development-process/design-proposals/language-distance-data
* @param $locale - the locale to determine fallback locales
* @return array of fallback locales
*/
private static function calculateFallbackLocales($locale) {
// Start with the given locale
$fallback = [$locale];

// Support other fallbacks such as es-419 -> es
$parts = explode('-', $locale);
for ($i = count($parts)-1; $i > 0; $i--) {
$lastPosition = strrpos($locale, $parts[$i]) - 1;
// Insert language tag substring to head
array_unshift($fallback, substr($locale, 0, $lastPosition));
}

return $fallback;
}

/**
* Wrapper to lookup string. Fallback to English
* @param $domain - the domain file
* @param $id - the key
* @return localized string, or fallback to English string, and then $id
*/
private static function getString($domain, $id) {
if (!array_key_exists($domain, self::$strings)) {
// Load the domain if it doesn't exist
if (!self::loadDomain($domain)) {
die('Domain ' . $domain . "doesn't exist");
}
}

if (count(self::currentLocales()) == 0) {
die('Current locales haven\'t been set by session');
}

foreach (self::currentLocales() as $locale) {
if (!array_key_exists($locale, self::$strings[$domain])) {
continue;
}

if (!self::$strings[$domain][$locale]->loaded) {
// Will set -> loaded = true
self::loadStrings($domain, $locale);
}

if (array_key_exists($id, self::$strings[$domain][$locale]->strings)) {
return self::$strings[$domain][$locale]->strings[$id];
}
}

// String not found in any localization -
if(KeymanHosts::Instance()->Tier() == KeymanHosts::TIER_DEVELOPMENT) {
die('string ' . $id . ' is missing in all the l10ns');
}
return $id;
}

/**
* Wrapper to lookup localized string for webpage domain.
* Formatted string using optional variable args for placeholders
* should escape like %1\$s
* @param $domain - the PHP file using the localized strings
* @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.

$str = self::getString($domain, $id);
if (count($args) == 0) {
return $str;
}
return vsprintf($str, ...$args);
}
}
63 changes: 63 additions & 0 deletions _includes/locale/strings/keyboards/context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Guidance on strings for keyboards/index.php

## Keyman is copyright (C) SIL Global. MIT License.

1. **`page_title`**
- **Context**: Page Title

2. **`page_description`**
- **Context**: Page Description

3. **`keyboard_search"`**
- **Context**: Keyboard search bar

4. **`enter_language`**
- **Context**: Search bar placeholder

5. **`search`**
- **Context**: Search Button Value

6. **`new_search`**
- **Context**: Link to start a new keyboard search

7. **`enter_name`**
- **Context**: Search box instruction (Popular keyboards | All keyboards)

8. **`popular_keyboards`**
- **Context**: Search box link for popular keyboards

9. **`all_keyboards`**
- **Context**: Search box link for all Keyman keyboards

10. **`hints`**
- **Context**: Search box hint: List header

11. **`searchbox_description`**
- **Context**: Search box hint: Description

12. **`searchbox_hint`**
- **Context**: Search box hint: available prefixes to use in the search

13. **`(keyboards)`**
- **Context**: (keyboards)

14. **`(languages)`**
- **Context**: (languages)

15. **`(scripts, writing systems) or`**
- **Context**: (scripts, writing systems) or...

16. **`(countries) to filter your search results. For example`**
- **Context**: (countries) to filter your search results...

17. **`searches for keyboards for languages used in Thailand.`**
- **Context**: Search box hint: example of country search

18. **`use_prefix`**
- **Context**: Search box hint: BCP 47 prefix

19. **`to search for a BCP 47 language tag, for example`**
- **Context**: Search box hint: BCP 47 language example

20. **`searches_tigrigna`**
- **Context**: Search box hint: BCP 47 language example
Loading