Merge pull request #12258 from keymanapp/chore/merge-master-into-windows-updates

chore: merge master into windows-updates
This commit is contained in:
Marc Durdin 2024-08-22 08:19:34 +02:00 • committed by GitHub
commit 8db70ffbd0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
647 changed files with 12409 additions and 4428 deletions

View file

@ -1,5 +1,111 @@
# Keyman Version History
## 18.0.94 alpha 2024-08-21
* fix(core): look for `emcc` instead of `emcc.py` (#12235)
* fix(web): improve tokenization output when wordbreaker breaks spec for span properties in output (#12229)
* chore(android): Use RTL-aware alignment and padding for layouts (#12225)
* fix(web): disable fat-finger data use when mayCorrect = false (#12220)
* fix(android): Auto-mirror back and forward arrows for RTL support (#12227)
* feat(android): Add localization for Arabic (#12228)
* fix(android): Auto-mirror increment and decrement arrows for RTL support (#12230)
## 18.0.93 alpha 2024-08-20
* (#12188)
* fix(web): fix malformed reversion display strings (#12201)
* feat(android): Add menu to specify long-press delay (#12170)
* feat(android): Pass longpress delay to KeymanWeb (#12185)
* (#12223)
## 18.0.92 alpha 2024-08-19
* chore(deps-dev): bump @75lb/deep-merge from 1.1.1 to 1.1.2 (#12118)
* chore(deps): bump semver from 7.5.4 to 7.6.0 (#12119)
* fix(windows): "Keyman" is not localized in UI strings (#12162)
* feat(android): Enhance how ENTER key is handled in apps (#12125)
* refactor(web): move `lm-worker` → `worker-thread` (#12150)
* fix(developer): remove redundant check in LdmlKeyboardCompiler.validate() (#11858)
## 18.0.91 alpha 2024-08-16
* refactor(web): move `predictive-text` → `worker-main` (#12146)
* fix(web): restore flick functionality (#12187)
* refactor(web): move `lm-message-types` → `predictive-text/types` (#12149)
* fix(developer): enforce presence of kps Info.Description field in info compilers (#12204)
* fix(developer): enforce presence of Version field when FollowKeyboardVersion is not set, in package compiler (#12205)
## 18.0.90 alpha 2024-08-15
* refactor(web): move parts of `keyboard-processor` → `js-processor` (#12111)
* fix(web): allow `lm-worker` to build on Linux (#12181)
* refactor(web): move remaining parts of `keyboard-processor` → `keyboard` (#12131)
* docs: update .kmx documentation around bitmaps, modifier state (#12183)
* refactor(web): rename `package-cache` → `keyboard-storage` (#12135)
## 18.0.89 alpha 2024-08-14
* feat(web): test skipped prediction round handling (#12169)
* fix(web): support live configuration of longpress delay (#12175)
* feat(web): add osk.gestureParams for better gesture-config persistence (#12176)
* refactor(core): move utfcodec to common (#12171)
* refactor: move kmx_u16 to common and rename to km_u16 (#12177)
* refactor(developer): use npm or local source for server addons instead of github references (#12090)
* fix(developer): fix crash with Windows Clipboard by ignoring zero scan code in debugger (#12166)
* chore(developer): update SIL logo (#12168)
## 18.0.88 alpha 2024-08-13
* docs: add .kmx specification (#12163)
## 18.0.87 alpha 2024-08-12
* chore(web): drop flaky auto-test component (#12155)
* fix(common): show description even if child projects specifies path (#12145)
## 18.0.86 alpha 2024-08-09
* chore(android): Update splash screen with SIL Tai Heritage Pro logo (#12127)
* chore(common): Update localization for Greek Polytonic (#12112)
* chore(common): add support for build target platform exclusions (#12113)
* feat(common): add top-level build.sh (#12114)
* chore(developer): start building Developer on linux,mac (#12117)
* refactor(web): Move `common/web/recorder` → `web/src/tools/testing/recorder-core` (#12092)
## 18.0.85 alpha 2024-08-08
* fix(developer): update date for last git commit date fixture (#12122)
* refactor(android/engine): Parse keyboards.json for FirstVoices app (#11943)
## 18.0.84 alpha 2024-08-07
* refactor(common): move kpj-related files into developer-utils (#11531)
* refactor(common): move kps-file.ts to @keymanapp/developer-utils (#11763)
* refactor(common): move kvks-file to @keymanapp/developer-utils (#11764)
* refactor(common): move .keyman-touch-layout reader/writer to @keymanapp/developer-utils (#11765)
* refactor(common): move LDML keyboard .xml reader/writer and kmx-plus builder to @keymanapp/developer-utils (#12081)
* refactor(common): move compiler-interfaces to @keymanapp/developer-utils (#12088)
* refactor(common): move xml2js and related deps to @keymanapp/developer-utils (#12101)
## 18.0.83 alpha 2024-08-06
* chore(common): Update history from 17.0.328 (#12093)
* change(common/models): change model tokenization to also tokenize whitespace (#11975)
* feat(web): support transform tokenization when given a root context (#11998)
* change(web): track whitespace-aware tokenization for context + correction-search caching (#11979)
* change(web): leverage tokenization to preserve punctuation and whitespace when predicting (#11997)
* fix(web): patch up worker build to provide artifacts for its tests (#12082)
* chore(web): move `web/src/engine/paths/` → `web/src/engine/interfaces/` (#12064)
* chore(web): move `common/web/input-processor/` → `web/src/engine/main/` (#12066)
* refactor(web): refactor and harmonize constants (#12072)
* chore(common): add data versions to minimum-versions.inc.sh (#12103)
* fix(web): fix lm-worker test broken by botched merge conflict resolution (#12104)
## 18.0.82 alpha 2024-08-05
* chore(windows): remove QIT_VSHIFTDOWN QIT _VSHIFTUP (#11973)
* chore(developer): add language/reference (#11799)
## 18.0.81 alpha 2024-08-03
* chore(developer): Revert "chore(developer): remove redundant references from tsconfig.json" (#12076)
@ -138,7 +244,7 @@
## 18.0.63 alpha 2024-06-26
* feat(linux): implement Linux side of SimulateAltGr option :checkered_flag: (#11852)
* feat(linux): implement Linux side of SimulateAltGr option (#11852)
## 18.0.62 alpha 2024-06-25
@ -150,8 +256,8 @@
* feat(web): optimization via lazy preprocessing of keyboard touch-layout info (#11265)
* fix(android): clear globe highlight when displaying keyboard picker (#11826)
* refactor(linux): add KeymanOption class for options :checkered_flag: (#11850)
* refactor(linux): rename methods that deal with keyboard options :checkered_flag: (#11851)
* refactor(linux): add KeymanOption class for options (#11850)
* refactor(linux): rename methods that deal with keyboard options (#11851)
## 18.0.60 alpha 2024-06-21
@ -566,6 +672,10 @@
* chore(common): move to 18.0 alpha (#10713)
* chore: move to 18.0 alpha
## 17.0.328 stable 2024-07-27
* fix(web): add nullish test in setOsk (#12041)
## 17.0.327 stable 2024-07-25
* fix(android): include DOMRect polyfill for older ES6-supporting devices (#11654)

View file

@ -1,6 +1,6 @@
# License
Copyright (c) 2017-2022 SIL International. All rights reserved.
Copyright (c) 2017-2024 SIL Global. All rights reserved.
Licensed under the MIT License.
@ -10,7 +10,7 @@ Licensed under the MIT License.
The MIT License
Copyright (c) 2017-2022 SIL International
Copyright (c) 2017-2024 SIL Global
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -4,7 +4,7 @@
# License
Copyright (c) SIL International.
Copyright (c) SIL Global.
Keyman is an open source project distributed under the [MIT license](LICENSE.md).

View file

@ -1 +1 @@
18.0.82
18.0.95

View file

@ -297,6 +297,11 @@
android:configChanges="orientation"
android:label="@string/app_name"
android:theme="@style/AppTheme.Base" />
<activity
android:name=".AdjustLongpressDelayActivity"
android:configChanges="orientation"
android:label="@string/app_name"
android:theme="@style/AppTheme.Base" />
<!-- Put other WebViewActivities in a separate process so the Keyboard WebView doesn't lag.
Ref https://stackoverflow.com/questions/40650643/timed-out-waiting-on-iinputcontextcallback-with-custom-keyboard-on-android -->

View file

@ -4,6 +4,7 @@
package com.keyman.android;
import com.tavultesoft.kmapro.AdjustLongpressDelayActivity;
import com.tavultesoft.kmapro.BuildConfig;
import com.tavultesoft.kmapro.DefaultLanguageResource;
import com.tavultesoft.kmapro.KeymanSettingsActivity;
@ -175,6 +176,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
}
}
// Determine special handling for ENTER key
KMManager.setEnterMode(attribute.imeOptions, inputType);
InputConnection ic = getCurrentInputConnection();
if (ic != null) {
ExtractedText icText = ic.getExtractedText(new ExtractedTextRequest(), 0);
@ -244,6 +248,8 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
if (exText != null)
exText = null;
}
// Initialize keyboard options
KMManager.sendOptionsToKeyboard();
}
@Override

View file

@ -0,0 +1,149 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* (Optional description of this file)
*/
package com.tavultesoft.kmapro;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.View;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMManager;
/**
* Settings menu for adjusting the longpress delay time. The value for the current longpress delay time
* is saved in shared preferences as an integer (milliseconds).
*/
public class AdjustLongpressDelayActivity extends BaseActivity {
private static final String TAG = "AdjustLongpressDelay";
public static final String adjustLongpressDelayKey = "AdjustLongpressDelay";
// Keeps track of the adjusted longpress delay time for saving.
// Internally use milliseconds, but GUI displays seconds
private static int currentDelayTimeMS = KMManager.KMDefault_LongpressDelay; // ms
private static int minLongpressTime = 300; // ms
private static int maxLongpressTime = 1500; // ms
private static int delayTimeIncrement = 200; // ms
/**
* Convert currentDelayTimeMS to progress
* @return int
*/
private int delayTimeToProgress() {
return (currentDelayTimeMS / delayTimeIncrement) - 1;
}
/**
* Convert progress to currentDelayTimeMS
* @param progress
* @return int (milliseconds)
*/
private int progressToDelayTime(int progress) {
return (progress + 1) * delayTimeIncrement + 100;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = this;
setContentView(R.layout.activity_adjust_longpress_delay);
Toolbar toolbar = (Toolbar) findViewById(R.id.titlebar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(null);
actionBar.setDisplayUseLogoEnabled(false);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
actionBar.setDisplayShowTitleEnabled(false);
actionBar.setDisplayShowCustomEnabled(true);
actionBar.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.keyman_blue)));
}
TextView adjustLongpressDelayActivityTitle = (TextView) findViewById(R.id.bar_title);
String titleStr = getString(R.string.adjust_longpress_delay);
adjustLongpressDelayActivityTitle.setTextColor(ContextCompat.getColor(this, R.color.ms_white));
adjustLongpressDelayActivityTitle.setText(titleStr);
currentDelayTimeMS = KMManager.getLongpressDelay();
TextView adjustLongpressDelayText = (TextView) findViewById(R.id.delayTimeText);
String longpressDelayTextSeconds = String.format(getString(R.string.longpress_delay_time), (float)(currentDelayTimeMS/1000.0));
adjustLongpressDelayText.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
adjustLongpressDelayText.setText(longpressDelayTextSeconds);
final SeekBar seekBar = (SeekBar) findViewById(R.id.seekBar);
seekBar.setProgress(delayTimeToProgress());
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Do nothing
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// Do nothing
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// Update the text field.
// The keyboard options will be saved and sent to KeymanWeb when exiting the menu
currentDelayTimeMS = progressToDelayTime(progress);
String longpressDelayTextSeconds = String.format(getString(R.string.longpress_delay_time), (float)(currentDelayTimeMS/1000.0));
adjustLongpressDelayText.setText(longpressDelayTextSeconds);
}
});
findViewById(R.id.delayTimeDownButton).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentDelayTimeMS > minLongpressTime) {
currentDelayTimeMS -= delayTimeIncrement;
seekBar.setProgress(delayTimeToProgress());
}
}
});
findViewById(R.id.delayTimeUpButton).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentDelayTimeMS < maxLongpressTime) {
currentDelayTimeMS += delayTimeIncrement;
seekBar.setProgress(delayTimeToProgress());
}
}
});
}
@Override
public void onBackPressed() {
// Store the longpress delay as a reference
// and then update KeymanWeb with the longpress delay
KMManager.setLongpressDelay(currentDelayTimeMS);
KMManager.sendOptionsToKeyboard();
super.onBackPressed();
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
}

View file

@ -105,7 +105,7 @@ public final class KeyboardSettingsActivity extends AppCompatActivity {
hashMap = new HashMap<>();
final String customHelpLink = kbd.getHelpLink();
// Check if app declared FileProvider
icon = String.valueOf(R.drawable.ic_arrow_forward);
icon = String.valueOf(R.drawable.ic_action_forward);
// Don't show help link arrow if File Provider unavailable, or custom help doesn't exist
if ( (customHelpLink != null && !FileProviderUtils.exists(context)) ||
(customHelpLink == null && !packageID.equals(KMManager.KMDefault_UndefinedPackageID)) ) {

View file

@ -28,7 +28,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
private static Context context;
private Preference languagesPreference, installKeyboardOrDictionary, displayLanguagePreference,
adjustKeyboardHeight;
adjustKeyboardHeight, adjustLongpressDelay;
private ListPreference spacebarTextPreference;
private CheckBoxPreference setSystemKeyboardPreference;
private CheckBoxPreference setDefaultKeyboardPreference;
@ -99,6 +99,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
});
setDefaultKeyboardPreference.setOnPreferenceChangeListener(checkBlocker);
adjustKeyboardHeight = new Preference(context);
adjustKeyboardHeight.setKey(AdjustKeyboardHeightActivity.adjustKeyboardHeightKey);
adjustKeyboardHeight.setTitle(getString(R.string.adjust_keyboard_height));
@ -106,6 +107,13 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
Intent adjustKeyboardHeightIntent = new Intent(context, AdjustKeyboardHeightActivity.class);
adjustKeyboardHeight.setIntent(adjustKeyboardHeightIntent);
adjustLongpressDelay = new Preference(context);
adjustLongpressDelay.setKey(AdjustLongpressDelayActivity.adjustLongpressDelayKey);
adjustLongpressDelay.setTitle(getString(R.string.adjust_longpress_delay));
adjustLongpressDelay.setWidgetLayoutResource(R.layout.preference_duration_icon_layout);
Intent adjustLongpressDelayIntent = new Intent(context, AdjustLongpressDelayActivity.class);
adjustLongpressDelay.setIntent(adjustLongpressDelayIntent);
/* Spacebar Caption Preference */
spacebarTextPreference = new ListPreference(context);
@ -188,8 +196,6 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
sendCrashReportPreference.setSummaryOff(getString(R.string.show_send_crash_report_off));
sendCrashReportPreference.setDefaultValue(true);
screen.addPreference(languagesPreference);
screen.addPreference(installKeyboardOrDictionary);
screen.addPreference(displayLanguagePreference);
@ -197,6 +203,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
screen.addPreference(setDefaultKeyboardPreference);
screen.addPreference(adjustKeyboardHeight);
screen.addPreference(adjustLongpressDelay);
screen.addPreference(spacebarTextPreference);
screen.addPreference(hapticFeedbackPreference);

View file

@ -174,7 +174,7 @@ public final class LanguageSettingsActivity extends AppCompatActivity {
updateActiveLexicalModel();
ImageView imageView = (ImageView) layout.findViewById(R.id.image1);
imageView.setImageResource(R.drawable.ic_arrow_forward);
imageView.setImageResource(R.drawable.ic_action_forward);
layout.setEnabled(true);
layout.setOnClickListener(new View.OnClickListener() {
@Override
@ -198,7 +198,7 @@ public final class LanguageSettingsActivity extends AppCompatActivity {
* textView = (TextView) layout.findViewById(R.id.text1);
* textView.setText(getString(R.string.manage_dictionary));
* imageView = (ImageView) layout.findViewById(R.id.image1);
* imageView.setImageResource(R.drawable.ic_arrow_forward);
* imageView.setImageResource(R.drawable.ic_action_forward);
*/
listView.setAdapter(adapter);
@ -346,7 +346,7 @@ public final class LanguageSettingsActivity extends AppCompatActivity {
}
holder.text.setText(kbd.getResourceName());
holder.img.setImageResource(R.drawable.ic_arrow_forward);
holder.img.setImageResource(R.drawable.ic_action_forward);
return convertView;
}

View file

@ -203,7 +203,7 @@ public final class LanguagesSettingsActivity extends AppCompatActivity {
}
holder.textLang.setText(data.name);
holder.img.setImageResource(R.drawable.ic_arrow_forward);
holder.img.setImageResource(R.drawable.ic_action_forward);
holder.textCount.setText(getContext().getResources().getQuantityString(R.plurals.keyboard_count, data.keyboards.size(), data.keyboards.size()));

View file

@ -472,7 +472,8 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
@Override
public void onKeyboardLoaded(KeyboardType keyboardType) {
// Do nothing
// Initialize keyboard options
KMManager.sendOptionsToKeyboard();
}
@Override
@ -646,7 +647,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
final View textSizeController = inflater.inflate(R.layout.text_size_controller, null);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MainActivity.this);
dialogBuilder.setIcon(R.drawable.ic_light_action_textsize);
dialogBuilder.setTitle(String.format(getString(R.string.text_size), textSize));
dialogBuilder.setTitle(getTextSizeString());
dialogBuilder.setView(textSizeController);
dialogBuilder.setPositiveButton(getString(R.string.label_ok), new DialogInterface.OnClickListener() {
@Override
@ -677,7 +678,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
textSize = progress + minTextSize;
textView.setTextSize((float) textSize);
dialog.setTitle(String.format(getString(R.string.text_size), textSize));
dialog.setTitle(getTextSizeString());
}
});
@ -702,6 +703,16 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
});
}
/**
* Combine a localized string for "Text Size" plus Arabic numerals
* @return String
*/
private String getTextSizeString() {
// Instead of formatting the number, will truncate formatting and concat the actual textSize
String label = getString(R.string.text_size).replace("%1$d", "");
return label + KMString.format(" %d", textSize);
}
private void showClearTextDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(MainActivity.this);
dialogBuilder.setIcon(R.drawable.ic_light_action_trash);

View file

@ -81,7 +81,7 @@ public final class SelectPackageActivity extends AppCompatActivity {
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put(titleKey, keyboardName);
hashMap.put(subtitleKey, pkgID);
String icon = String.valueOf(R.drawable.ic_arrow_forward);
String icon = String.valueOf(R.drawable.ic_action_forward);
hashMap.put(iconKey, icon);
list.add(hashMap);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -15,7 +15,7 @@
<item>
<bitmap
android:src="@drawable/sil_logo_blue_2017_1"
android:src="@drawable/sil_logo_color_tai_heritage_pro"
android:gravity="center"/>
</item>

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_navigation_back"
android:tint="@color/keyman_blue" />

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_light_dialog_textsize_down"
android:tint="@android:color/black"
android:autoMirrored="true" />

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_light_dialog_textsize_up"
android:tint="@android:color/black"
android:autoMirrored="true"/>

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M16.24,7.76C15.07,6.59 13.54,6 12,6v6l-4.24,4.24c2.34,2.34 6.14,2.34 8.49,0 2.34,-2.34 2.34,-6.14 -0.01,-8.48zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/titlebar" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal"
android:paddingBottom="25dp"
android:paddingTop="60dp" >
<ImageButton
android:id="@+id/delayTimeDownButton"
style="?android:attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:contentDescription="@string/ic_delay_time_down"
android:src="@drawable/ic_light_dialog_textsize_down" />
<!-- Seekbar range 0 to 6 corresponds to longpress duration 300 ms to 1500 ms -->
<SeekBar
android:id="@+id/seekBar"
android:layout_width="180dp"
android:layout_height="wrap_content"
android:contentDescription="@string/ic_longpress_delay_slider"
android:progress="1"
android:max="6"
android:layout_gravity="center" />
<ImageButton
android:id="@+id/delayTimeUpButton"
style="?android:attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:contentDescription="@string/ic_delay_time_up"
android:src="@drawable/ic_light_dialog_textsize_up" />
</LinearLayout>
<TextView
android:id="@+id/delayTimeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/linearLayout"
android:gravity="center_horizontal"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:text="@string/longpress_delay_time"
android:textSize="@dimen/longpress_delay_label_textsize"
/>
</RelativeLayout>

View file

@ -19,11 +19,12 @@
<ImageButton
android:id="@+id/back_button"
style="?android:attr/actionButtonStyle"
app:tint="@color/image_button_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:contentDescription="@string/label_back"
android:src="@drawable/ic_navigation_back" />
android:src="@drawable/ic_action_back" />
<ImageButton
android:id="@+id/forward_button"
@ -32,7 +33,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/label_forward"
android:src="@drawable/ic_navigation_forward" />
android:src="@drawable/ic_action_forward" />
<ImageButton
android:id="@+id/close_button"

View file

@ -34,7 +34,7 @@
android:layout_marginEnd="5dp"
android:layout_weight="1"
android:contentDescription="@string/image_button"
android:src="@drawable/ic_navigation_back" />
android:src="@drawable/ic_action_back" />
<ImageButton
android:id="@+id/forwardButton"
@ -45,7 +45,7 @@
android:layout_marginEnd="5dp"
android:layout_weight="1"
android:contentDescription="@string/image_button"
android:src="@drawable/ic_navigation_forward" />
android:src="@drawable/ic_action_forward" />
<ImageButton
android:id="@+id/bookmarksButton"

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/preference_icon"
android:layout_height="@dimen/preference_icon"
android:src="@drawable/ic_action_timelapse" />

View file

@ -2,4 +2,4 @@
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/preference_icon"
android:layout_height="@dimen/preference_icon"
android:src="@drawable/ic_arrow_forward" />
android:src="@drawable/ic_action_forward" />

View file

@ -15,7 +15,7 @@
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:contentDescription="@string/ic_text_size_down"
android:src="@drawable/ic_light_dialog_textsize_down" />
android:src="@drawable/ic_action_decrement" />
<SeekBar
android:id="@+id/seekBar"
@ -32,6 +32,6 @@
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:contentDescription="@string/ic_text_size_up"
android:src="@drawable/ic_light_dialog_textsize_up" />
android:src="@drawable/ic_action_increment" />
</LinearLayout>

View file

@ -15,7 +15,6 @@
android:layout_height="match_parent"
android:layout_margin="3dp"
android:padding="4dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_above="@+id/progressBar"
android:weightSum="2"
@ -76,7 +75,6 @@
style="@style/Base.Widget.AppCompat.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="@dimen/keyman_bar_height"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
android:visibility="visible"

View file

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- app_name removed in 16.0 beta. Set in build.gradle -->
<!-- Context: Menu Action -->
<string name="action_share" comment="Menu action to send text content to another app">مشاركة</string>
<!-- Context: Menu Action -->
<string name="action_web" comment="Menu action to open Keyman browser">متصفح ويب</string>
<!-- Context: Menu Action -->
<string name="action_text_size" comment="Menu action to adjust text size">حجم النص</string>
<!-- Context: Menu Action -->
<string name="action_overflow" comment="Show additional menu items">المزيد</string>
<!-- Context: Menu Action -->
<string name="action_clear_text" comment="Menu action to erase text">محو النص</string>
<!-- Context: Menu Action -->
<string name="action_info" comment="Menu action to display Keyman help page">معلومات</string>
<!-- Context: Menu Action -->
<string name="action_settings" comment="Menu action to open Settings menu">الإعدادات</string>
<!-- Context: Menu Action -->
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">تنزيل التحديثات</string>
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">إصدار: %1$s</string>
<!-- Context: Text label when old Chrome version installed -->
<string name="text_require_chrome_57" comment="Banner text when old version of Chrome is installed">يتطلب Keyman إصدار 57 من Chrome أو أعلى.</string>
<!-- Context: Button label -->
<string name="button_update_chrome" comment="Prompt to upgrade Chrome in the Play Store">تحديث Chrome</string>
<!-- Context: Textfield prompt (&#8230; is the ellipsis character "...") -->
<string name="textview_hint" comment="Prompt to start typing">ابدأ الكتابة هنا&#8230;</string>
<!-- Context: Splash screen (version/copyright info currently disabled -->
<!-- Context: Splash screen (version/copyright info currently disabled -->
<!-- Context: Text Size dialog -->
<string name="text_size" comment="Current text size (number)">حجم النص: %1$d</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_up" comment="Make text bigger">تكبير حجم النص</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_down" comment="Make text smaller">تصغير حجم النص</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_slider" comment="Adjust text size">Text size slider</string>
<!-- Context: Clear Text dialog -->
<string name="all_text_will_be_cleared" comment="Erase all the text">\nسيتم محو كل النص\n</string>
<!-- Context: Get Started menu -->
<string name="get_started" comment="Menu for getting started">إبدأ الآن</string>
<!-- Context: Get Started menu -->
<string name="add_a_keyboard" comment="Menu item to add a keyboard">اضف لوحة مفاتيح بلغتك</string>
<!-- Context: Get Started menu -->
<string name="enable_system_keyboard" comment="Menu item to enable Keyman as system keyboard">تمكين Keyman لوحة المفاتيح الأساسية للنظام</string>
<!-- Context: Get Started menu -->
<string name="set_keyman_as_default" comment="Menu item to set Keyman as default system keyboard">إعداد Keyman لوحة المفاتيح الإفتراضية</string>
<!-- Context: Get Started menu -->
<string name="more_info" comment="Open the Keyman for Android help page">المزيد من المعلومات</string>
<!-- Context: Get Started menu -->
<string name="show_get_started" comment="Show the &quot;Get Started&quot; menu on startup">أظهر \"%1$s\" عند البدء</string>
<!-- Context: Android Storage Permission -->
<string name="request_storage_permission" comment="Request storage permission to access keyboard packages">لتثبيت حزم لوحات المفاتيح، امنح Keyman الإذن لقراءة التخزين الخارجي.</string>
<!-- Context: Android Storage Permission -->
<string name="storage_permission_denied" comment="Keyboard package installation may fail since Android storage permission not granted">تم رفض أذونات التخزين. قد يفشل تثبيت حزمة لوحة المفاتيح</string>
<string name="storage_permission_denied2" comment="Keyboard package installation failed. Recommend install from local file"> Storage permission request failed. Try Keyman Settings - Install from local file</string>
<!-- Context: Keyman Settings menu -->
<string name="keyman_settings" comment="Settings menu title">الإعدادات</string>
<!-- Context: Keyman Settings menu -->
<plurals name="installed_languages">
<item quantity="zero">اللغات المثبتة (%1$d)</item>
<item quantity="one">اللغات المثبتة (%1$d)</item>
<item quantity="two">اللغات المثبتة (%1$d)</item>
<item quantity="few">اللغات المثبتة (%1$d)</item>
<item quantity="many">اللغات المثبتة (%1$d)</item>
<item quantity="other">اللغات المثبتة (%1$d)</item>
</plurals>
<!-- Context: Keyman Settings menu -->
<string name="install_keyboard_or_dictionary" comment="Menu item to install keyboard or dictionary">تثبيت لوحة المفاتيح او القاموس</string>
<!-- Context: Keyman Settings menu -->
<string name="change_display_language" comment="Menu action to change interface language">لغة العرض</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">تعديل ارتفاع لوحة المفاتيح</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">نص مفتاح المسافة</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">لوحة المفاتيح</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_language" comment="Spacebar caption option - language">اللغة</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_language_keyboard" comment="Spacebar caption option - language + keyboard">اللغة + لوحة المفاتيح</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_blank" comment="Spacebar caption option - blank">فارغ</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_keyboard" comment="Spacebar caption hint - keyboard">أظهر اسم لوحة المفاتيح على مفتاح المسافة</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_language" comment="Spacebar caption hint - language">أظهر اسم اللغة على المسافة</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_language_keyboard" comment="Spacebar caption hint - language + keyboard">اللغة ولوحة المفاتيح على المسافة</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">لا تظهر نص على المسافة</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">الاهتزاز عند الكتابة</string>
<!-- Context: Keyman Settings menu -->
<string name="show_banner" comment="text suggestions banner">إظهار اللافتة دائماً</string>
<!-- Context: Keyman Settings menu -->
<string name="show_banner_on" comment="Description when toggle is on">سيتم تنفيذها</string>
<!-- Context: Keyman Settings menu -->
<string name="show_banner_off" comment="Description when toggle is off">عند إيقاف التشغيل، يظهر فقط عند تفعيل النص التنبؤي</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">السماح بإرسال تقارير توقف التشغيل المفاجئ عبر الشبك</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report_on" comment="Description when toggle is on">عند التشغيل، سيتم إرسال تقارير التوقف المفاجئ</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report_off" comment="Description when toggle is off">عند إيقاف التشغيل، لن يتم إرسال التقارير</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="install_from_keyman_dot_com" comment="Install package from Keyman server">تثبيت من keyman.com</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="install_from_local_file" comment="Install package file from local device">التثبيت من ملف محلي</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="install_from_other_device" comment="Scan QR Code">التثبيت من جهاز آخر</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="add_languages_to_installed_keyboard" comment="Install additional languages from a keyboard package">اضف لغات للوحة المفاتيح المثبتة</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="add_language_subtext" comment="Additional information for adding another language">(من حزمة لوحة المفاتيح)</string>
<!-- Context: Select Package and Select Languages menu -->
<string name="title_select_keyboard_package_list" comment="Select a keyboard package to add languages">اختر حزمة لوحة مفاتيح</string>
<!-- Context: Select Package and Select Languages menu -->
<string name="title_select_languages_for_package" comment="Select language names from the keyboard package">اختر لغة %1$s</string>
<!-- Context: Select Package and Select Languages menu -->
<string name="added_language_to_keyboard" comment="Notification that a language name is added to a keyboard">تمت اضافة %1$s إلى %2$s</string>
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">كل اللغات مثبتة مسبقا</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">اسحب لوحة المفاتيح لتغيير مدى ارتفاعها</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">دور الجهاز لضبط الوضع العمودي والوضع الأفقي</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">إعادة ضبط الافتراضيات</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">ابحث أو اكتب عنوان URL</string>
<!-- Context: Keyman Web Browser -->
<string name="title_bookmarks" comment="Title for bookmarks">علامات مرجعية</string>
<!-- Context: Keyman Web Browser -->
<string name="no_bookmarks" comment="Text when bookmark list is empty">لا تتوفر أي علامات مرجعية</string>
<!-- Context: Keyman Web Browser -->
<string name="add_bookmark" comment="Confirmation to add bookmark">أضف علامة مرجعية</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_title" comment="Page title for saved bookmark">العنوان</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_url" comment="Page URL for saved bookmark">عنوان Url</string>
<!-- Context: KMP Package strings -->
<string name="title_package_failed_to_install" comment="Error dialog title when package failed to install">فشل في تنزيل حزمة %1$s</string>
<!-- Context: KMP Package strings -->
<string name="downloading_keyboard_package" comment="Confirmation to download keyboard package filename">يتم الآن تحميل لوحة مفاتيح حزمة \n%1$s&#8230;</string>
<!-- Context: KMP Package strings -->
<string name="failed_to_extract" comment="Notification that keyboard package failed to unzip">فشل في الإستخراج</string>
<!-- Context: KMP Package strings -->
<string name="install_keyboard_package" comment="Title to install keyboard package">تثبيت لوحة المفاتيح</string>
<!-- Context: KMP Package strings -->
<string name="install_predictive_text_package" comment="Title to install dictionary package">تثبيت القاموس</string>
<!-- Context: KMP Package strings -->
<string name="not_valid_package_file" comment="Notification when invalid package file cannot be installed">ليس %1$s ملف حزمة صالحة لبرنامج Keyman.\n%2$s\"</string>
<!-- Context: KMP Package strings -->
<string name="no_new_touch_keyboards_to_install" comment="Notification when no touch-optimized keyboards can be installed">ليس في حزمة لوحة المفاتيح لوحات مفاتيح مكيّفة لشاشات اللمس</string>
<!-- Context: KMP Package strings -->
<string name="no_new_predictive_text_to_install" comment="Notification when no dictionaries can be installed">لا يوجد نصوص تنبؤية للتنزيل</string>
<!-- Context: KMP Package strings -->
<string name="no_targets_to_install" comment="Notification when a package doesn't contain keyboards or dictionaries">لا لوحات مفاتيح و نصوص تنبؤية للتنزيل</string>
<!-- Context: KMP Package strings -->
<string name="no_associated_languages" comment="Notification when a keyboard package doesn't contain languages">ليس لحزمة لوحة المفاتيح لغات ذات صلة لتثبيتها</string>
<!-- Context: KMP Package strings -->
<string name="invalid_metadata" comment="kmp.json metadata file is invalid or missing in package">غير صالح\ناقص البيانات الوصفية في الحزمة</string>
<!-- Context: KMP Package strings -->
<string name="minimum_keyboard_version_not_supported" comment="Notification when keyboard has functionality not supported by current Keyman">تتطلب لوحة المفاتيح إصدارًا أحدث من Keyman</string>
<!-- Context: anywhere -->
<string name="unable_to_open_browser" comment="Notification when a browser activity cannot be launched">غير قادر على تشغيل متصفح الويب</string>
</resources>

View file

@ -30,9 +30,9 @@
<!-- Context: Text Size dialog -->
<string name="text_size" comment="Current text size (number)">Μέγεθος κειμένου: %1$d</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_up" comment="Make text bigger">Μεγαλῶστε τὸ κείμενο</string>
<string name="ic_text_size_up" comment="Make text bigger">Μεγαλῶστε τὴν γραμματοσειρά</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_down" comment="Make text smaller">Μεγαλῶστε τὸ κείμενο</string>
<string name="ic_text_size_down" comment="Make text smaller">Μικρύνετε τὴν γραμματοσειρά</string>
<!-- Context: Text Size dialog -->
<string name="ic_text_size_slider" comment="Adjust text size">Ρύθμιση μεγέθους κειμένου</string>
<!-- Context: Clear Text dialog -->
@ -68,7 +68,7 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Μεταβολὴ ὕψους πληκτρολογίου</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Spacebar caption</string>
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Λεζάντα πλήκτρου διαστήματος</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">Πληκτρολόγιο</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
@ -80,9 +80,9 @@
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_keyboard" comment="Spacebar caption hint - keyboard">\'Ονομα πληκτρολογίου στὸ πλῆκτρο διαστήματος</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_language" comment="Spacebar caption hint - language">\'Ονομα γλώσσας στὸ πλῆκτρο διαστήματος</string>
<string name="spacebar_caption_hint_language" comment="Spacebar caption hint - language">Ὄνομα γλώσσας στὸ πλῆκτρο διαστήματος</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_language_keyboard" comment="Spacebar caption hint - language + keyboard">\'Ονομα πληκτρολογίου καὶ γλώσσας στὸ πλῆκτρο διαστήματος</string>
<string name="spacebar_caption_hint_language_keyboard" comment="Spacebar caption hint - language + keyboard">Ὄνομα πληκτρολογίου καὶ γλώσσας στὸ πλῆκτρο διαστήματος</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Καμμία λεζάντα στὸ πλῆκτρο διαστήματος</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
@ -92,13 +92,13 @@
<!-- Context: Keyman Settings menu -->
<string name="show_banner_on" comment="Description when toggle is on">Πρὸς ὑλοποίησιν</string>
<!-- Context: Keyman Settings menu -->
<string name="show_banner_off" comment="Description when toggle is off">Ὅταν εἶναι off, ἐμφανίζεται μόνο ὅταν ἔχει ἐνεργοποιηθεῖ τὸ προγνωστικὸ κείμενο</string>
<string name="show_banner_off" comment="Description when toggle is off">OFF: ἐμφάνιση μόνον ὅταν ἔχει ἐνεργοποιηθεῖ ἡ πρόβλεψη κειμένου</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">Ἐπιτρέψτε τὴν ἀποστολὴ ἀναφορῶν κατάρρευσης μέσῳ δικτύου</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report_on" comment="Description when toggle is on">Ὅταν εἶναι ΟΝ, θὰ ἀποστέλλονται ἀναφορὲς κατάρρευσης</string>
<string name="show_send_crash_report_on" comment="Description when toggle is on">ON: Ἀποστολὴ ἀναφορῶν κατάρρευσης</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report_off" comment="Description when toggle is off">Ὅταν εἶναι off, δὲν θὰ ἀποστέλλονται ἀναφορὲς κατάρρευσης</string>
<string name="show_send_crash_report_off" comment="Description when toggle is off">OFF: Δὲν ἀποστέλλονται ἀναφορὲς κατάρρευσης</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
<string name="install_from_keyman_dot_com" comment="Install package from Keyman server">Ἐγκατάσταση ἀπὸ τὸ keyman.com</string>
<!-- Context: Keyman Settings "Install Keyboard or Dictionary" menu -->
@ -118,9 +118,9 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Ὅλες οἱ γλῶσσες ἔχουν ἤδη ἐγκατασταθεῖ</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Σύρετε τὸ πληκτρολόγιο γιὰ νὰ ἀλλάξετε τὸ ὕψος</string>
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Σύρετε τὸ πληκτρολόγιο γιὰ νὰ ἀλλάξετε τὸ ὕψος του</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Περιστρέψτε τὴν συσκευὴ γιὰ λειτουργία πορτραίτου καὶ τοπίου</string>
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Περιστρέψτε τὴν συσκευὴ γιὰ προσαρμογὴ τοῦ ὕψους σὲ λειτουργία πορτραίτου καὶ τοπίου</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Ἐπαναφέρετε τὶς προεπιλεγμένες ρυθμίσεις</string>
<!-- Context: Keyman Web Browser -->
@ -150,9 +150,9 @@
<!-- Context: KMP Package strings -->
<string name="no_new_touch_keyboards_to_install" comment="Notification when no touch-optimized keyboards can be installed">Τὸ πακέτο πληκτρολογίου δὲν ἔχει βελτιστοποιημένα πληκτρολόγια ἀφῆς πρὸς ἐγκατάστασιν</string>
<!-- Context: KMP Package strings -->
<string name="no_new_predictive_text_to_install" comment="Notification when no dictionaries can be installed">Δὲν ὑπάρχει νέο προγνωστικό κείμενο πρὸς ἐγκατάστασιν</string>
<string name="no_new_predictive_text_to_install" comment="Notification when no dictionaries can be installed">Δὲν ὑπάρχουν προγνώσεις κειμένων πρὸς ἐγκατάστασιν</string>
<!-- Context: KMP Package strings -->
<string name="no_targets_to_install" comment="Notification when a package doesn't contain keyboards or dictionaries">Δὲν ὑπάρχουν πληκτρολόγια ἢ προγνωστικό κείμενο πρὸς ἐγκατάστασιν</string>
<string name="no_targets_to_install" comment="Notification when a package doesn't contain keyboards or dictionaries">Δὲν ὑπάρχουν πληκτρολόγια ἢ προγνώσεις κειμένων πρὸς ἐγκατάστασιν</string>
<!-- Context: KMP Package strings -->
<string name="no_associated_languages" comment="Notification when a keyboard package doesn't contain languages">Τὸ πακέτο πληκτρολογίου δὲν ἔχει σχετικὲς μὲ αὐτὸ γλῶσσες πρὸς ἐγκατάστασιν</string>
<!-- Context: KMP Package strings -->

View file

@ -23,4 +23,5 @@
<dimen name="preference_icon">32dp</dimen>
<dimen name="update_indicator_size">16dp</dimen>
<dimen name="update_indicator_bg_margin">0dp</dimen>
<dimen name="longpress_delay_label_textsize">24sp</dimen>
</resources>

View file

@ -113,6 +113,9 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Adjust keyboard height</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Adjust longpress delay</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Spacebar caption</string>
@ -198,6 +201,17 @@
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Reset to Defaults</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Delay Time: %1$.1f seconds</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">Delay time longer</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Delay time shorter</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Longpress delay time slider</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">Search or type URL</string>

View file

@ -112,6 +112,17 @@ function notifyHost(event, params) {
}, 10);
}
// Update the KeymanWeb longpress delay
// delay is in milliseconds
function setLongpressDelay(delay) {
if (keyman.osk) {
keyman.osk.gestureParams.longpress.waitLength = delay;
console.debug('setLongpressDelay('+delay+')');
} else {
window.console.log('setLongpressDelay error: keyman.osk undefined');
}
}
// Update the KMW banner height
// h is in dpi (different from iOS)
function setBannerHeight(h) {

View file

@ -40,6 +40,7 @@ public class DisplayLanguages {
DisplayLanguageType[] languages = {
new DisplayLanguageType(unspecifiedLocale, context.getString(R.string.default_locale)),
new DisplayLanguageType("am-ET", "አማርኛ (Amharic)"),
new DisplayLanguageType("ar-SA", "(Arabic) العربية"),
new DisplayLanguageType("az-AZ", "Azərbaycanca (Azəricə)"),
new DisplayLanguageType("bwr-NG", "Bura-Pabir"),
new DisplayLanguageType("cs-CZ", "čeština (Czech)"),

View file

@ -20,6 +20,7 @@ import android.webkit.JavascriptInterface;
import static android.content.Context.VIBRATOR_SERVICE;
import com.keyman.engine.KMManager.EnterModeType;
import com.keyman.engine.KMManager.KeyboardType;
import com.keyman.engine.data.Keyboard;
import com.keyman.engine.util.CharSequenceUtil;
@ -153,7 +154,53 @@ public class KMKeyboardJSHandler {
}
if (s.length() > 0 && s.charAt(0) == '\n') {
keyDownUp(KeyEvent.KEYCODE_ENTER, 0);
if (k.keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
// Special handling of ENTER key
switch (KMManager.enterMode) {
// Go action
case GO :
ic.performEditorAction(EditorInfo.IME_ACTION_GO);
break;
// Search action
case SEARCH :
ic.performEditorAction(EditorInfo.IME_ACTION_SEARCH);
break;
// Send action
case SEND :
ic.performEditorAction(EditorInfo.IME_ACTION_SEND);
break;
// Next action
case NEXT :
ic.performEditorAction(EditorInfo.IME_ACTION_NEXT);
break;
// Done action
case DONE :
ic.performEditorAction(EditorInfo.IME_ACTION_DONE);
break;
// Previous action
case PREVIOUS :
ic.performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
break;
// Messaging apps
case NEWLINE :
// Send newline and advance cursor
ic.commitText("\n", 1);
break;
// Default ENTER action
default:
keyDownUp(KeyEvent.KEYCODE_ENTER, 0);
}
} else {
// In-app keyboard uses default ENTER action
keyDownUp(KeyEvent.KEYCODE_ENTER, 0);
}
ic.endBatchEdit();
return;
}

View file

@ -25,7 +25,6 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Typeface;
import android.inputmethodservice.InputMethodService;
@ -180,6 +179,19 @@ public final class KMManager {
}
}
// Enum for how the System Keyboard ENTER key is handled for the EditorInfo action
// Reference: https://developer.android.com/reference/android/view/inputmethod/EditorInfo#summary
public enum EnterModeType {
GO, // Go action
SEARCH, // Search action
SEND, // Send action
NEXT, // Next action
DONE, // Done action
PREVIOUS, // Previous action
NEWLINE, // Send newline character
DEFAULT, // Default ENTER action
}
protected static InputMethodService IMService;
private static boolean debugMode = false;
@ -218,6 +230,9 @@ public final class KMManager {
// regardless what the Settings preference is.
private static boolean mayPredictOverride = false;
// Determine how system keyboard handles ENTER key
public static EnterModeType enterMode = EnterModeType.DEFAULT;
// Boolean for whether a keyboard can send embedded KMW crash reports to Sentry
// When maySendCrashReport is false, KMW will still attempt to send crash reports, but it
// will be blocked.
@ -248,6 +263,8 @@ public final class KMManager {
public static final String KMKey_KeyboardHeightPortrait = "keyboardHeightPortrait";
public static final String KMKey_KeyboardHeightLandscape = "keyboardHeightLandscape";
public static final String KMKey_LongpressDelay = "longpressDelay";
public static final String KMKey_CustomHelpLink = "CustomHelpLink";
public static final String KMKey_KMPLink = "kmp";
public static final String KMKey_UserKeyboardIndex = "UserKeyboardIndex";
@ -293,6 +310,9 @@ public final class KMManager {
public static final String KMDefault_DictionaryVersion = "0.1.4";
public static final String KMDefault_DictionaryKMP = KMDefault_DictionaryPackageID + FileUtils.MODELPACKAGE;
// Default KeymanWeb longpress delay in milliseconds
public static final int KMDefault_LongpressDelay = 500;
// Keyman files
protected static final String KMFilename_KeyboardHtml = "keyboard.html";
protected static final String KMFilename_JSEngine = "keymanweb-webview.js";
@ -1251,6 +1271,61 @@ public final class KMManager {
}
}
/**
* Sets enterMode which specifies how the System keyboard ENTER key is handled
*
* @param imeOptions EditorInfo.imeOptions
* @param inputType InputType
*/
public static void setEnterMode(int imeOptions, int inputType) {
if ((inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0) {
enterMode = EnterModeType.NEWLINE;
return;
}
int imeActions = imeOptions & EditorInfo.IME_MASK_ACTION;
EnterModeType value = EnterModeType.DEFAULT;
switch (imeActions) {
case EditorInfo.IME_ACTION_GO:
value = EnterModeType.GO;
break;
case EditorInfo.IME_ACTION_SEARCH:
value = EnterModeType.SEARCH;
break;
case EditorInfo.IME_ACTION_SEND:
value = EnterModeType.SEND;
break;
case EditorInfo.IME_ACTION_NEXT:
value = EnterModeType.NEXT;
break;
case EditorInfo.IME_ACTION_DONE:
value = EnterModeType.DONE;
break;
case EditorInfo.IME_ACTION_PREVIOUS:
value = EnterModeType.PREVIOUS;
break;
default:
value = EnterModeType.DEFAULT;
}
enterMode = value;
}
/**
* Get the value of enterMode
* @return EnterModeType
*/
public static EnterModeType getEnterMode() {
return enterMode;
}
/**
* Sets mayPredictOverride true if the InputType field is a hidden password text field
* (either TYPE_TEXT_VARIATION_PASSWORD or TYPE_TEXT_VARIATION_WEB_PASSWORD
@ -2010,6 +2085,45 @@ public final class KMManager {
return Configuration.ORIENTATION_UNDEFINED;
}
/**
* Get the longpress delay (in milliseconds) from stored preference. Defaults to 500ms
* @return int - longpress delay in milliseconds
*/
public static int getLongpressDelay() {
SharedPreferences prefs = appContext.getSharedPreferences(
appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
return prefs.getInt(KMKey_LongpressDelay, KMDefault_LongpressDelay);
}
/**
* Set the longpress delay (in milliseconds) as a stored preference.
* @param longpressDelay - int longpress delay in milliseconds
*/
public static void setLongpressDelay(int longpressDelay) {
SharedPreferences prefs = appContext.getSharedPreferences(
appContext.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt(KMKey_LongpressDelay, longpressDelay);
editor.commit();
}
/**
* Sends options to the KeymanWeb keyboard.
* 1. number of milliseconds to trigger a longpress gesture.
* This method requires a keyboard to be loaded for the value to take effect.
*/
public static void sendOptionsToKeyboard() {
int longpressDelay = getLongpressDelay();
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) {
InAppKeyboard.loadJavascript(KMString.format("setLongpressDelay(%d)", longpressDelay));
}
if (SystemKeyboard != null) {
SystemKeyboard.loadJavascript(KMString.format("setLongpressDelay(%d)", longpressDelay));
}
}
public static int getBannerHeight(Context context) {
int bannerHeight = 0;
if (InAppKeyboard != null && InAppKeyboard.getBanner() != BannerType.BLANK) {

View file

@ -88,7 +88,7 @@ public final class KeyboardInfoActivity extends BaseActivity {
hashMap = new HashMap<String, String>();
final String customHelpLink = kbd.getHelpLink();
// Check if app declared FileProvider
String icon = String.valueOf(R.drawable.ic_arrow_forward);
String icon = String.valueOf(R.drawable.ic_action_forward);
// Don't show help link arrow if it's a local help file and File Provider unavailable,
// or custom help doesn't exist
if ( (customHelpLink != null && ! KMManager.isTestMode() &&

View file

@ -81,7 +81,7 @@ public final class ModelInfoActivity extends BaseActivity {
final String customHelpLink = lm.getHelpLink();
// Check if app declared FileProvider
// Currently, model help only available if custom link exists
icon = String.valueOf(R.drawable.ic_arrow_forward);
icon = String.valueOf(R.drawable.ic_action_forward);
// Don't show help link arrow if both custom help and File Provider don't exist
// TODO: Update this when model help available on help.keyman.com
if ( (!customHelpLink.equals("") && !FileProviderUtils.exists(context)) ||

View file

@ -321,7 +321,7 @@ public final class ModelPickerActivity extends BaseActivity {
// once it has its own backing Dataset instance.
// Is this an installed model or not?
holder.imgDetails.setImageResource(R.drawable.ic_arrow_forward);
holder.imgDetails.setImageResource(R.drawable.ic_action_forward);
if (KeyboardPickerActivity.containsLexicalModel(context, modelKey)) {
holder.imgInstalled.setImageResource(R.drawable.ic_check);
} else {

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_arrow_back"
android:tint="@android:color/black"
android:autoMirrored="true" />

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_arrow_forward"
android:tint="@color/keyman_blue" />
android:tint="@android:color/black"
android:autoMirrored="true"/>

View file

@ -15,8 +15,8 @@
android:layout_gravity="center"
android:gravity="center"
android:paddingBottom="10dp"
android:paddingLeft="2dp"
android:paddingRight="2dp"
android:paddingStart="2dp"
android:paddingEnd="2dp"
android:text="@string/help_bubble_text"
android:textColor="#404040"
android:textSize="12sp" />

View file

@ -44,7 +44,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:src="@drawable/ic_arrow_forward"
android:src="@drawable/ic_action_forward"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:layout_marginEnd="12dp"

View file

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Device type -->
<!-- Application name (Keyman Engine for Android) -->
<!-- Context: Title for list -->
<plurals name="title_keyboards">
<item quantity="zero">لوحة مفاتيح</item>
<item quantity="one">لوحات مفاتيح</item>
<item quantity="two">لوحتا مفاتيح</item>
<item quantity="few">لوحات مفاتيح</item>
<item quantity="many">لوحة مفاتيح</item>
<item quantity="other">لوحة مفاتيح</item>
</plurals>
<!-- Context: Title for list -->
<plurals name="title_other_input_methods">
<item quantity="zero">طريقة إداخل أخرى</item>
<item quantity="one">طريقة إدخال أخرى</item>
<item quantity="two">طريقتا إدخال أخريتان</item>
<item quantity="few">طرق إدخال</item>
<item quantity="many">طريقة إدخال أخرى</item>
<item quantity="other">طريقة إدخال أخرى</item>
</plurals>
<!-- Context: Title for list -->
<string name="title_add_keyboard" comment="Add a new keyboard">إضافة لوحة مفاتيح جديدة</string>
<!-- Context: Title for list -->
<string name="title_languages_settings" comment="List of installed languages">اللغات المثبتة</string>
<!-- Context: Title for list -->
<string name="title_language_settings" comment="Keyman Settings for a language">إعدادات %1$s</string>
<!-- Context: Button label -->
<string name="label_add" comment="Button to add an item">إضافة</string>
<!-- Context: Button label -->
<string name="label_back" comment="Button to go back">عودة</string>
<!-- Context: Button label -->
<string name="label_cancel" comment="Button to cancel an action">إلغاء</string>
<!-- Context: Button label -->
<string name="label_close" comment="Close a dialog">إغلاق</string>
<!-- Context: Button label. Removed in Keyman 15 -->
<string name="label_close_keyman" comment="Close the Keyman application">إغلاق Keyman</string>
<!-- Context: Button label -->
<string name="label_forward" comment="Button to go forward">الأمام</string>
<!-- Context: Button label -->
<string name="label_next_input_method" comment="Switch to next input method (replaces label_close_keyman)">طريقة الإدخال التالية</string>
<!-- Context: Button label -->
<string name="label_download" comment="Button to confirm downloading an item">تحميل</string>
<!-- Context: Button label -->
<string name="label_install" comment="Confirmation to install a package">تثبيت</string>
<!-- Context: Button label -->
<string name="label_later" comment="Do an action at a later time">لاحقًا</string>
<!-- Context: Button label -->
<string name="label_next" comment="Go to the next screen">التالي</string>
<!-- Context: Button label -->
<string name="label_ok" comment="Button to acknowledge a dialog">موافق</string>
<!-- Context: Button label -->
<string name="label_update" comment="Dialog button to update a resource">تحديث</string>
<!-- Context: No internet for updates -->
<string name="no_internet_connection" comment="Internet connection missing">لا يوجد اتصال بالإنترنت</string>
<!-- Context: Keyboard Updates -->
<string name="cannot_connect" comment="Error message when network connection fails">لا يمكن الاتصال بخادم Keyman!</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_delete_keyboard" comment="Confirmation to delete a keyboard">هل ترغب بإزالة لوحة المفاتيح هذه؟</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_download_keyboard" comment="Confirmation to download a keyboard update">هل ترغب بتحميل آخر إصدار من لوحة المفاتيح هذه؟</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_update" comment="Confirmation to update several resources">هل ترغب بتحديث لوحات المفاتيح والقواميس الآن؟</string>
<!-- Context: Keyboard Updates -->
<string name="keyboard_updates_channel">تحديثات الموارد</string>
<!-- Context: Keyboard Updates -->
<string name="keyboard_updates_available" comment="Notification when resource updates are available">تتوفر تحديثات موارد</string>
<!-- Context: Keyboard Updates -->
<string name="update_available" comment="Currently installed version of a resource with an available update">%1$s (تحديث متوفر)</string>
<!-- Context: Keyboard Updates -->
<string name="keyboard_update_message" comment="language name: keyboard name">تحديثات متوفرة للوحة مفاتيح %1$s: %2$s </string>
<!-- Context: Keyboard Updates -->
<string name="dictionary_update_message">تحديثات متوفرة لقاموس %1$s: %2$s </string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_version" comment="Label for keyboard version">إصدار لوحة المفاتيح</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="help_link" comment="Label for help link">رابط المساعدة</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="uninstall_keyboard" comment="Label for uninstalling keyboard">إزالة تثبيت لوحة المفاتيح</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_picker_new_keyboard" comment="Mark a language name that's newly installed in keyboard list">[new] %1$s</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_qr_code" comment="QR Code description">امسح هذا الرمز لتحميل \nلوحة المفاتيح هذه على جهاز آخر</string>
<!-- Context: Keyboard Help welcome.htm title -->
<string name="welcome_package" comment="Title to welcome.htm page (name and version)">أهلًا بك في %1$s</string>
<!-- Context: Keyboard app doesn't include FileProvider library needed to view help file -->
<string name="fileprovider_undefined" comment="FileProvider library needed to view help file">مكتبة FileProvider مطلوبة لعرض ملفات المساعدة: %1$s</string>
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">خطأ كبير في لوحة مفاتيح مع %1$s:%2$s لأجل لغة %3$s. سيتم تحميل لوحة المفاتيح الأساسية.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">خطأ في لوحة مفاتيح %1$s:%2$s لأجل لغة %3$s.</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">يجري التحقق من قاموس ذي صلة للتحميل</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download"> لا يمكن الاتصال بخادم Keyman للتحقق من تنزيل القاموس المرتبط</string>
<!-- Context: Model Updates -->
<string name="confirm_download_model" comment="Confirmation to download a dictionary update">هل ترغب بتحملي آخر إصدار من هذا القاموس؟</string>
<!-- Context: No associated dictionary found -->
<string name="no_associated_model" comment="Confirmation there's no available dictionary to download">لا يوجد قاموس لتحميله</string>
<!-- Context: Model Updates -->
<string name="catalog_unavailable" comment="Notification that the cloud resource catalog can't be updated">كتالوغ المصدر غير متوفر</string>
<!-- Context: Background download messages-->
<string name="catalog_download_start_in_background" comment="Notification that the resource catalog update will begin">بدء تحديث الكتالوغ في الخلفية</string>
<!-- Context: Background download messages-->
<string name="catalog_download_is_running_in_background" comment="Notification that the resource catalog update is still downloading">الكتالوغ لا يزال يتم تحميله، يرجى المحاولة مجددًا بعد قليل!</string>
<!-- Context: Background download messages-->
<string name="progress_message_checking_resource_is_running" comment="Progress notification that the check for a resource is still running ">البحث عن مصدر</string>
<!-- Context: Background download messages-->
<string name="keyboard_download_start_in_background" comment="Notification that a keyboard download has started">بدء تحميل لوحة المفاتيح في الخلفية</string>
<!-- Context: Background download messages-->
<string name="keyboard_download_is_running_in_background" comment="Notification that a keyboard download is still running">يجري بالفعل تحميل لوحة المفاتيح المختارة، يرجى المحاولة مجددًا بعد قليل!</string>
<!-- Context: Background download messages-->
<string name="keyboard_download_finished" comment="Notification that a keyboard download has finished">اكتمل تحميل لوحة المفاتيح!</string>
<!-- Context: Background download messages-->
<string name="dictionary_download_start_in_background" comment="Notification that a dictionary download has started">بدأ تحميل القاموس في الخلفية</string>
<!-- Context: Background download messages-->
<string name="dictionary_download_is_running_in_background" comment="Notification that a dictionary download is still running">يجري بالفعل تحميل القاموس، يرجى المحاولة مجددًا بعد قليل!</string>
<!-- Context: Background download messages. Removed in Keyman 17-->
<string name="dictionary_download_finished" comment="Notification that a dictionary download has finished">اكتمل تحميل القاموس.</string>
<!-- Context: Background download messages -->
<string name="download_failed" comment="Notification that a download failed">فشل التحميل</string>
<!-- Context: Background download messages -->
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">فشل استرجاع الملف المحمل</string>
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">فشل الوصول إلى الخادم!</string>
<!-- Context: General Updates -->
<string name="update_check_current" comment="Notification that all resources are up to date">"جميع الموارد محدّثة!"</string>
<!-- Context: General Updates -->
<string name="update_failed" comment="Notification that a resource update failed">فشل تحديث واحد أو أكثر من المصادر!</string>
<!-- Context: General Updates. Removed in Keyman 17 -->
<string name="update_success" comment="Notification that a resource update successfully updated">تم تحديث الموارد بنجاح!</string>
<!-- Context: Model Info -->
<string name="model_version" comment="Title for a dictionary version">إصدار القاموس</string>
<!-- Context: Model Info -->
<string name="uninstall_model" comment="Label to uninstall a dictionary">إزالة تثبيت القاموس</string>
<!-- Context: Model Info -->
<string name="confirm_delete_model" comment="Confirmation to delete a dictionary">هل ترغب بإزالة هذا القاموس؟</string>
<!-- Context: Model Deleted -->
<string name="model_deleted" comment="Notification when a dictionary is deleted">تم حذف القاموس</string>
<!-- Context: Language Settings Keyboard install strings -->
<string name="keyboard_install_toast" comment="Notification when a keyboard is installed">تم تثبيت لوحة مفاتيح %1$s</string>
<!-- Context: Language Settings Keyboard uninstall strings -->
<string name="keyboard_deleted_toast" comment="Notification when a keyboard is deleted">تم حذف لوحة المفاتيح</string>
<!-- Context: Language Settings menu strings -->
<string name="enable_corrections" comment="Enable corrections from the suggestion banner">تفعيل التصحيحات</string>
<!-- Context: Language Settings menu strings -->
<string name="enable_predictions" comment="Enable predictions from the suggestion banner">تفعيل التوقعات</string>
<!-- Context: Language Settings menu strings -->
<string name="model_label">القاموس</string>
<plurals name="model_count">
<item quantity="zero">Dictionaries</item>
<item quantity="one">Dictionary</item>
<item quantity="two">Dictionaries</item>
<item quantity="few">Dictionaries</item>
<item quantity="many">Dictionaries</item>
<item quantity="other">Dictionaries</item>
</plurals>
<!-- Context: Language Settings menu substring - Check for available dictionary -->
<string name="check_available_model" comment="Check for available dictionary">البحث عن قاموس متوفر</string>
<string name="check_model_online" comment="Check for dictionaries online">تحقق من وجود قواميس على الإنترنت</string>
<!-- Context: Language Settings menu strings -->
<string name="model_info_header" comment="Dictionary name">قاموس: %1$s</string>
<!-- Context: Language Settings menu strings -->
<string name="model_picker_header" comment="Dictionaries for a specified language">قواميس %1$s</string>
<!-- Context: Language Settings menu strings -->
<!-- Context: Language Settings menu strings -->
<string name="model_install_toast" comment="Notification when a dictionary is installed">تم تثبيت القاموس</string>
<!-- Context: Language Settings menu strings -->
<plurals name="keyboard_count">
<item quantity="zero">(%1$d اوحات)</item>
<item quantity="one">(لوحة مفاتيح %1$d)</item>
<item quantity="two">(لوحتا مفاتيح %1$d)</item>
<item quantity="few">(%1$d لوحة مفاتيح)</item>
<item quantity="many">(%1$d لوحة مفاتيح)</item>
<item quantity="other">(%1$d لوحة مفاتيح)</item>
</plurals>
<!-- Context: "Change Display Language" menu -->
<string name="default_locale" comment="Use the device's current locale">اللغة الأصلية</string>
<!-- Context: Content descriptions -->
<!-- Context: Content descriptions -->
<!-- Context: Popup menu labels -->
<string name="label_delete" comment="Confirmation to delete an item">حذف</string>
<!-- Context: Shared preferences name -->
<!-- Context: Other strings -->
<string name="help_bubble_text">اضغط هنا لتغيير لوحة المفاتيح</string>
<!-- Context: anywhere -->
<string name="unable_to_open_browser" comment="Notification when a browser activity cannot be launched">غير قادر على تشغيل متصفح الويب</string>
</resources>

View file

@ -27,7 +27,7 @@
<!-- Context: Button label -->
<string name="label_close" comment="Close a dialog">Κλεῖστε</string>
<!-- Context: Button label. Removed in Keyman 15 -->
<string name="label_close_keyman" comment="Close the Keyman application">Κλεῖστε τὸ Κῆμαν</string>
<string name="label_close_keyman" comment="Close the Keyman application">Κλεῖστε τὸ Keyman</string>
<!-- Context: Button label -->
<string name="label_forward" comment="Button to go forward">Προχωρῆστε</string>
<!-- Context: Button label -->
@ -53,11 +53,11 @@
<!-- Context: Keyboard Updates -->
<string name="confirm_download_keyboard" comment="Confirmation to download a keyboard update">Θὰ θέλατε νὰ κατεβάσετε τὴν τελευταία ἔκδοση αὐτοῦ τοῦ πληκτρολογίου;</string>
<!-- Context: Keyboard Updates -->
<string name="confirm_update" comment="Confirmation to update several resources">Θά θέλατε νὰ ἐνημερώσετε τώρα πληκτρολόγια καὶ λεξικά;</string>
<string name="confirm_update" comment="Confirmation to update several resources">Θὰ θέλατε νὰ ἐνημερώσετε τώρα πληκτρολόγια καὶ λεξικά;</string>
<!-- Context: Keyboard Updates -->
<string name="keyboard_updates_channel">Ἐνημερώσεις Πόρων</string>
<string name="keyboard_updates_channel">Ἐνημερώσεις</string>
<!-- Context: Keyboard Updates -->
<string name="keyboard_updates_available" comment="Notification when resource updates are available">Διαθέσιμες Ἐνημερώσεις Πόρων</string>
<string name="keyboard_updates_available" comment="Notification when resource updates are available">Διαθέσιμες Ἐνημερώσεις</string>
<!-- Context: Keyboard Updates -->
<string name="update_available" comment="Currently installed version of a resource with an available update">%1$s (Διαθέσιμη Ἐνημέρωση)</string>
<!-- Context: Keyboard Updates -->
@ -67,13 +67,13 @@
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_version" comment="Label for keyboard version">Ἔκδοση πληκτρολογίου</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="help_link" comment="Label for help link">Σύνδεσμος βοηθείας</string>
<string name="help_link" comment="Label for help link">Βοήθεια</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="uninstall_keyboard" comment="Label for uninstalling keyboard">Ἀπεγκαταστῆστε πληκτρολόγιο</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_picker_new_keyboard" comment="Mark a language name that's newly installed in keyboard list">[νέο] %1$s</string>
<!-- Context: Keyboard Info and Keyboard Settings -->
<string name="keyboard_qr_code" comment="QR Code description">Σαρῶστε αὐτὸν τὸν κωδικό γιὰ νὰ φορτώσετε\nαὐτὸ τὸ πληκτρολόγιο σὲ ἄλλη συσκευή</string>
<string name="keyboard_qr_code" comment="QR Code description">Σαρῶστε αὐτὸν τὸν κωδικὸ γιὰ νὰ φορτώσετε\nαὐτὸ τὸ πληκτρολόγιο σὲ ἄλλη συσκευή</string>
<!-- Context: Keyboard Help welcome.htm title -->
<string name="welcome_package" comment="Title to welcome.htm page (name and version)">Καλωσορίσατε στὸ %1$s</string>
<!-- Context: Keyboard app doesn't include FileProvider library needed to view help file -->
@ -81,22 +81,22 @@
<!-- Context: Fatal keyboard error. Will load default keyboard -->
<string name="fatal_keyboard_error" comment="Fatal keyboard error (keyboard ID::packageID for language). Will load default keyboard">Μοιραῖο σφάλμα πληκτρολογίου στὸ %1$s:%2$s γιὰ τὴν %3$s γλῶσσα. Φορτώνεται προεπιλεγμένο πληκτρολόγιο.</string>
<!-- Context: Fatal keyboard error. -->
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language)."> Error in keyboard %1$s:%2$s for %3$s language.</string>
<string name="fatal_keyboard_error_short" comment="Error in keyboard (keyboard ID::packageID for language).">Σφάλμα στὸ πληκτρολόγιο %1$s:%2$s γιὰ τὴν γλῶσσα \"%3$s\".</string>
<!-- Context: Query for associated dictionary -->
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Ἔλεγχος συσχετισμένου λεξικοῦ πρὸς λῆψιν</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download">Ἀδυναμία συνδέσεως μὲ τὸν διακομιστὴ Κῆμαν γιὰ τὸν ἔλεγχο συσχετισμένου λεξικοῦ πρὸς λῆψιν</string>
<string name="query_associated_model" comment="Check if there's an available dictionary to download">Ἀναζήτηση σχετικοῦ λεξικοῦ πρὸς μεταφόρτωσιν</string>
<string name="cannot_query_associated_model" comment="Cannot reach server to check if there's an available dictionary to download">Ἀδυναμία συνδέσεως μὲ τὸν διακομιστὴ Keyman γιὰ τὴν εὕρεση σχετικοῦ λεξικοῦ πρὸς μεταφόρτωσιν</string>
<!-- Context: Model Updates -->
<string name="confirm_download_model" comment="Confirmation to download a dictionary update">Θὰ θέλατε νὰ κατεβάσετε τὴν τελευταία ἔκδοση αὐτοῦ τοῦ λεξικοῦ;</string>
<!-- Context: No associated dictionary found -->
<string name="no_associated_model" comment="Confirmation there's no available dictionary to download">Δὲν ὑπάρχει λεξικὸ πρὸς λῆψιν</string>
<string name="no_associated_model" comment="Confirmation there's no available dictionary to download">Δὲν ὑπάρχει λεξικὸ πρὸς μεταφόρτωσιν</string>
<!-- Context: Model Updates -->
<string name="catalog_unavailable" comment="Notification that the cloud resource catalog can't be updated">Μὴ διαθέσιμος κατάλογος πόρων</string>
<!-- Context: Background download messages-->
<string name="catalog_download_start_in_background" comment="Notification that the resource catalog update will begin">Ἔχει ξεκινήσει ἐνημέρωση καταλόγου στὸ παρασκήνιο</string>
<!-- Context: Background download messages-->
<string name="catalog_download_is_running_in_background" comment="Notification that the resource catalog update is still downloading">Ἡ λήψη τοῦ καταλόγου συνεχίζεται· παρακαλοῦμε ξαναδοκιμάστε σὲ λίγο!</string>
<string name="catalog_download_is_running_in_background" comment="Notification that the resource catalog update is still downloading">Ἡ μεταφόρτωση τοῦ καταλόγου συνεχίζεται· παρακαλοῦμε ξαναδοκιμάστε σὲ λίγο!</string>
<!-- Context: Background download messages-->
<string name="progress_message_checking_resource_is_running" comment="Progress notification that the check for a resource is still running ">Ἔλεγχος πόρου σὲ ἐξέλιξη</string>
<string name="progress_message_checking_resource_is_running" comment="Progress notification that the check for a resource is still running ">Ἀναζήτηση πόρου σὲ ἐξέλιξη</string>
<!-- Context: Background download messages-->
<string name="keyboard_download_start_in_background" comment="Notification that a keyboard download has started">Ἡ λήψη τοῦ πληκτρολογίου ἔχει ξεκινήσει στὸ παρασκήνιο</string>
<!-- Context: Background download messages-->
@ -112,7 +112,7 @@
<!-- Context: Background download messages -->
<string name="download_failed" comment="Notification that a download failed">Ἡ λήψη ἀπέτυχε</string>
<!-- Context: Background download messages -->
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">Ἀποτυχία ἀνακτήσεως ληφθέντος ἀρχείου</string>
<string name="failed_to_retrieve_file" comment="Failed to retrieve downloaded file">Ἀποτυχία ἀνακλήσεως ληφθέντος ἀρχείου</string>
<!-- Context: General Updates -->
<string name="update_check_unavailable" comment="Error message when a Keyman server can't be reached">Ἀποτυχία προσβάσεως στὸν διακομιστή!</string>
<!-- Context: General Updates -->
@ -120,11 +120,11 @@
<!-- Context: General Updates -->
<string name="update_failed" comment="Notification that a resource update failed">Ἕνας ἢ περισσότεροι πόροι ἀπέτυχαν νὰ ἐνημερωθοῦν!</string>
<!-- Context: General Updates. Removed in Keyman 17 -->
<string name="update_success" comment="Notification that a resource update successfully updated">Οἱ πόροι ἐνημερώθηκαν ἐπιτυχῶς!</string>
<string name="update_success" comment="Notification that a resource update successfully updated">Ὅλες οἱ ἐνημερώσεις ὁλοκληρώθηκαν ἐπιτυχῶς!</string>
<!-- Context: Model Info -->
<string name="model_version" comment="Title for a dictionary version">Ἔκδοση λεξικοῦ</string>
<!-- Context: Model Info -->
<string name="uninstall_model" comment="Label to uninstall a dictionary">Ἀπεγκαταστῆτε λεξικό</string>
<string name="uninstall_model" comment="Label to uninstall a dictionary">Ἀπεγκαταστῆστε λεξικό</string>
<!-- Context: Model Info -->
<string name="confirm_delete_model" comment="Confirmation to delete a dictionary">Θὰ θέλατε νὰ διαγράψετε αὐτὸ τὸ λεξικό;</string>
<!-- Context: Model Deleted -->
@ -136,16 +136,16 @@
<!-- Context: Language Settings menu strings -->
<string name="enable_corrections" comment="Enable corrections from the suggestion banner">Ἐνεργοποιῆστε τὶς διορθώσεις</string>
<!-- Context: Language Settings menu strings -->
<string name="enable_predictions" comment="Enable predictions from the suggestion banner">Ἐνεργοποιῆστε προβλέψεις</string>
<string name="enable_predictions" comment="Enable predictions from the suggestion banner">Ἐνεργοποιῆστε προβλέψεις κειμένου</string>
<!-- Context: Language Settings menu strings -->
<string name="model_label">Λεξικά</string>
<string name="model_label">Λεξικό</string>
<plurals name="model_count">
<item quantity="one">Λεξικό</item>
<item quantity="other">Λεξικά</item>
</plurals>
<!-- Context: Language Settings menu substring - Check for available dictionary -->
<string name="check_available_model" comment="Check for available dictionary">Ἔλεγχος διαθεσίμου λεξικοῦ</string>
<string name="check_model_online" comment="Check for dictionaries online">Ἔλεγχος λεξικῶν ὀνλάϊν</string>
<string name="check_available_model" comment="Check for available dictionary">Ἀναζήτηση διαθεσίμου λεξικοῦ</string>
<string name="check_model_online" comment="Check for dictionaries online">Ἀναζήτηση λεξικῶν ὀνλάϊν</string>
<!-- Context: Language Settings menu strings -->
<string name="model_info_header" comment="Dictionary name">Λεξικό: %1$s</string>
<!-- Context: Language Settings menu strings -->

36
build.sh Executable file
View file

@ -0,0 +1,36 @@
#!/usr/bin/env bash
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
builder_describe "Keyman -- all projects" \
":common" \
":core" \
":android" \
":ios" \
":linux" \
":mac" \
":web" \
":windows" \
":developer" \
clean \
configure \
build \
test \
install \
publish
builder_describe_platform \
:android android-studio \
:ios mac \
:linux linux \
:mac mac \
:windows win
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
builder_run_child_actions clean configure build test publish install

View file

@ -16,6 +16,11 @@ builder_describe "Keyman common and resources modules" \
build \
test
builder_describe_platform \
:linux linux \
:mac mac \
:windows win
builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------

389
common/cpp/km_u16.cpp Normal file
View file

@ -0,0 +1,389 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* std::u16string functions and string conversion utility functions
*/
#include <stdarg.h>
#include "utfcodec.hpp"
#include "km_u16.h"
/** string <- wstring
* @brief Obtain a std::string from a std::wstring
* @param wstr the std::wstring to be converted
* @return a std::string
*/
std::string string_from_wstring(std::wstring const wstr) {
return convert<wchar_t, char>((const std::wstring)wstr);
}
/** wstring <- string
* @brief Obtain a std::wstring from a std::string
* @param str the std::string to be converted
* @return a std::wstring
*/
std::wstring wstring_from_string(std::string const str) {
return convert<char, wchar_t>((const std::string)str);
}
/** u16string <- string
* @brief Obtain a std::u16string from a std::string
* @param str the std::string to be converted
* @return a std::u16string
*/
std::u16string u16string_from_string(std::string const str) {
return convert<char, char16_t>((const std::string)str);
}
/** string <- u16string
* @brief Obtain a std::string from a std::u16string
* @param str16 the std::u16string to be converted
* @return a std::string
*/
std::string string_from_u16string(std::u16string const str16) {
return convert<char16_t, char>((const std::u16string)str16);
}
/** wstring <- u16string
* @brief Obtain a std::wstring from a std::u16string
* @param str16 the std::u16string to be converted
* @return a std::wstring
*/
std::wstring wstring_from_u16string(std::u16string const str16) {
return convert<char16_t, wchar_t>((const std::u16string)str16);
}
/** u16string <- wstring
* @brief Obtain a std::u16string from a std::wstring
* @param wstr the std::wstring to be converted
* @return a std::u16string
*/
std::u16string u16string_from_wstring(std::wstring const wstr) {
return convert<wchar_t, char16_t>((const std::wstring)wstr);
}
/**
* @brief Convert pointer to wchar_t to pointer to char16_t and copy sz elements into dst
* @param dst destination
* @param sz nr of characters to be copied
* @param fmt source to convert and copy
*/
void u16sprintf(KMX_WCHAR* dst, const size_t sz, const wchar_t* fmt, ...) {
wchar_t* wbuf = new wchar_t[sz];
va_list args;
va_start(args, fmt);
vswprintf(wbuf, sz, fmt, args);
va_end(args);
std::u16string u16str = u16string_from_wstring(wbuf);
u16ncpy(dst, u16str.c_str(), sz);
delete[] wbuf;
}
/**
* @brief Convert u16string to long integer
* @param str u16string beginning with the representation of an integral number.
* @param endptr Reference to the next character in str
* @param base Numerical base (radix) that determines the valid characters and their interpretation
* @return a long
*/
long int u16tol(const KMX_WCHAR* str, KMX_WCHAR** endptr, int base) {
auto s = string_from_u16string(str);
char* t;
long int result = strtol(s.c_str(), &t, base);
if (endptr != nullptr)
*endptr = (KMX_WCHAR*)str + (t - s.c_str());
return result;
}
std::string toHex(int num1) {
if (num1 == 0)
return "0";
int num = num1;
std::string s = "";
while (num) {
int temp = num % 16;
if (temp <= 9)
s += (48 + temp);
else
s += (87 + temp);
num = num / 16;
}
std::reverse(s.begin(), s.end());
return s;
}
/**
* @brief Append max characters from u16string
* @param dst Pointer to the destination array
* @param src u16string to be appended
* @param max Maximum number of characters to be appended.
* @return Pointer to dst
*/
const KMX_WCHAR* u16ncat(KMX_WCHAR* dst, const KMX_WCHAR* src, size_t max) {
KMX_WCHAR* o = dst;
dst = (KMX_WCHAR*)u16chr(dst, 0);
// max -= (dst-o);
while (*src && max > 0) {
*dst++ = *src++;
max--;
}
if (max > 0)
*dst = 0;
return o;
}
/**
* @brief Find last '/' or '\\' in an array of char16_t
* @param name Pointer to the source
* @return Pointer to the last slash/backslash
*/
const KMX_WCHAR* u16rchr_slash(KMX_WCHAR const* name) {
const KMX_WCHAR* cp = NULL;
cp = u16rchr(name, '\\');
if (cp == NULL)
cp = u16rchr(name, '/');
return cp;
}
/**
* @brief Find last '/' or '\\' in an array of char
* @param name Pointer to the source
* @return Pointer to the last slash/backslash
*/
KMX_CHAR* strrchr_slash(KMX_CHAR* name) {
KMX_CHAR* cp = NULL;
cp = strrchr(name, '\\');
if (cp == NULL)
cp = strrchr(name, '/');
return cp;
}
/**
* @brief Locate last occurrence of character in u16string
* @param p Pointer to the source
* @param ch The character to be found
* @return A pointer to the last occurrence of character in u16str
*/
const KMX_WCHAR* u16rchr(const KMX_WCHAR* p, KMX_WCHAR ch) {
const KMX_WCHAR* p_end = p + u16len(p) - 1;
if (ch == '\0')
return p_end + 1;
while (p_end >= p) {
if (*p_end == ch)
return p_end;
p_end--;
}
return NULL;
}
/**
* @brief Locate first occurrence of character in u16string
* @param p Pointer to the source
* @param ch The character to be found
* @return A pointer to the first occurrence of character in u16str
*/
const KMX_WCHAR* u16chr(const KMX_WCHAR* p, KMX_WCHAR ch) {
while (*p) {
if (*p == ch)
return p;
p++;
}
return ch == 0 ? p : NULL;
}
/**
* @brief Copy the u16string pointed to by scr into the array pointed to by dst
* @param dst Pointer to the destination
* @param src Pointer to the source to be copied
* @return Pointer to dst
*/
const KMX_WCHAR* u16cpy(KMX_WCHAR* dst, const KMX_WCHAR* src) {
KMX_WCHAR* o = dst;
while (*src) {
*dst++ = *src++;
}
*dst = 0;
return o;
}
/**
* @brief Copy max characters of the u16string pointed to by src into the array pointed by dst
* @param dst Pointer to the destination
* @param src Pointer to the source to be copied
* @param max Maximum number of characters to be copied
* @return Pointer to dst
*/
const KMX_WCHAR* u16ncpy(KMX_WCHAR* dst, const KMX_WCHAR* src, size_t max) {
KMX_WCHAR* o = dst;
while (*src && max > 0) {
*dst++ = *src++;
max--;
}
if (max > 0) {
*dst = 0;
}
return o;
}
/**
* @brief Return the length of the u16string str
* @param p Pointer to the source
* @return The length of u16string
*/
size_t u16len(const KMX_WCHAR* p) {
int i = 0;
while (*p) {
p++;
i++;
}
return i;
}
/**
* @brief Compare two u16strings
* @param p Pointer one u16string
* @param q Pointer another u16string
* @return 0 if strings are equal
* ! = 0 if unequal
*/
int u16cmp(const KMX_WCHAR* p, const KMX_WCHAR* q) {
while (*p && *q) {
if (*p != *q)
return *p - *q;
p++;
q++;
}
return *p - *q;
}
/**
* @brief Case insensitive comparison of up to count characters in two strings
* @param p Pointer one u16string
* @param q Pointer another u16string
* @param count Maximum number of characters to compare
* @return 0 if strings are equal
* ! = 0 if unequal
*/
int u16nicmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count) {
while (*p && *q && count) {
if (toupper(*p) != toupper(*q))
return *p - *q;
p++;
q++;
count--;
}
if (count)
return *p - *q;
return 0;
}
/**
* @brief Case insensitive comparison of two strings
* @param p Pointer one u16string
* @param q Pointer another u16string
* @return 0 if strings are equal
* ! = 0 if unequal
*/
int u16icmp(const KMX_WCHAR* p, const KMX_WCHAR* q) {
while (*p && *q) {
if (toupper(*p) != toupper(*q))
return *p - *q;
p++;
q++;
}
return *p - *q;
}
/**
* @brief Comparison of up to count characters in two strings
* @param p Pointer one u16string
* @param q Pointer another u16string
* @param count Maximum number of characters to compare
* @return 0 if strings are equal
* ! = 0 if unequal
*/
int u16ncmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count) {
while (*p && *q && count) {
if (*p != *q)
return *p - *q;
p++;
q++;
count--;
}
if (count)
return *p - *q;
return 0;
}
/**
* @brief Split u16string into tokens
* @param p Pointer to u16string to parse.
* @param ch the delimiter character
* @param ctx the remaining string after the first delimiter
* @return Pointer to the first token in p
*/
KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx) {
if (!p) {
p = *ctx;
if (!p)
return NULL;
}
KMX_WCHAR* q = p;
while (*q && *q != ch) {
q++;
}
if (*q) {
*q = 0;
q++;
while (*q == ch)
q++;
*ctx = q;
} else {
*ctx = NULL;
}
return *p ? p : NULL;
}
/**
* @brief Split u16string into tokens
* @param p Pointer to u16string to parse.
* @param delimiters an array of delimiter characters
* @param ctx the remaining string after the first delimiter
* @return Pointer to the first token in p
*/
KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR* delimiters, KMX_WCHAR** ctx) {
if (!p) {
p = *ctx;
if (!p)
return NULL;
}
KMX_WCHAR* q = p;
while (*q && !u16chr(delimiters, *q)) {
q++;
}
if (*q) {
*q = 0;
q++;
while (*q && u16chr(delimiters, *q))
q++;
*ctx = q;
} else {
*ctx = NULL;
}
return *p ? p : NULL;
}
/**
* @brief Convert a u16string to a double
* @param str Pointer to u16string
* @return double value equivalent to the string
*/
double u16tof(KMX_WCHAR* str16) {
char* pEnd;
std::string str = string_from_u16string(str16);
return strtof(str.c_str(), &pEnd);
}

76
common/include/km_u16.h Normal file
View file

@ -0,0 +1,76 @@
#pragma once
#include <string>
#include <cstring>
#include <km_types.h>
/** @brief Obtain a std::string from a std::wstring */
std::string string_from_wstring(std::wstring const wstr);
/** @brief Obtain a std::wstring from a std::string */
std::wstring wstring_from_string(std::string const str);
/** @brief Obtain a std::u16string from a std::string */
std::u16string u16string_from_string(std::string const str);
/** @brief Obtain a std::string from a std::u16string */
std::string string_from_u16string(std::u16string const str16);
/** @brief Obtain a std::wstring from a std::u16string */
std::wstring wstring_from_u16string(std::u16string const str16);
/** @brief Obtain a std::u16string from a std::wstring */
std::u16string u16string_from_wstring(std::wstring const wstr);
/** @brief Convert pointer to wchar_t to pointer to char16_t and copy sz elements into dst */
void u16sprintf(KMX_WCHAR* dst, const size_t sz, const wchar_t* fmt, ...);
/** @brief Return the length of the u16string str */
size_t u16len(const KMX_WCHAR* p);
/** @brief Compare two u16strings */
int u16cmp(const KMX_WCHAR* p, const KMX_WCHAR* q);
/** @brief Case insensitive comparison of two strings */
int u16icmp(const KMX_WCHAR* p, const KMX_WCHAR* q);
/** @brief Comparison of up to count characters in two strings */
int u16ncmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count);
/** @brief Case insensitive comparison of up to count characters in two strings */
int u16nicmp(const KMX_WCHAR* p, const KMX_WCHAR* q, size_t count);
/** @brief Copy max characters of the u16string pointed to by src into the array pointed to by dst */
const KMX_WCHAR* u16ncpy(KMX_WCHAR* dst, const KMX_WCHAR* src, size_t max);
/** @brief Copy the u16string pointed to by src into the array pointed to by dst */
const KMX_WCHAR* u16cpy(KMX_WCHAR* dst, const KMX_WCHAR* src);
/** @brief Locate last occurrence of character in u16string */
const KMX_WCHAR* u16rchr(const KMX_WCHAR* p, KMX_WCHAR ch);
/** @brief Locate first occurrence of character in u16string */
const KMX_WCHAR* u16chr(const KMX_WCHAR* p, KMX_WCHAR ch);
/** @brief Append max characters from u16string */
const KMX_WCHAR* u16ncat(KMX_WCHAR* dst, const KMX_WCHAR* src, size_t max);
/** @brief Split u16string into tokens */
KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR ch, KMX_WCHAR** ctx);
/** @brief Split u16string into tokens */
KMX_WCHAR* u16tok(KMX_WCHAR* p, const KMX_WCHAR* delimiters, KMX_WCHAR** ctx);
/** @brief Convert a u16string to a double */
long int u16tol(const KMX_WCHAR* str, KMX_WCHAR** endptr, int base);
/** @brief Convert a u16string to a double */
double u16tof(KMX_WCHAR* str);
/** @brief find last '/' or '\\' in an array of char */
KMX_CHAR* strrchr_slash(KMX_CHAR* Name);
/** @brief find last '/' or '\\' in an array of char16_t */
const KMX_WCHAR* u16rchr_slash(KMX_WCHAR const* Name);
std::string toHex(int num1);

View file

@ -3,5 +3,5 @@ export {
transformToSuggestion, defaultApplyCasing
} from "./common.js";
export { default as QuoteBehavior } from "./quote-behavior.js";
export { Tokenization, tokenize, getLastPreCaretToken, wordbreak } from "./tokenization.js";
export { getLastPreCaretToken, Token, Tokenization, tokenize, wordbreak } from "./tokenization.js";
export { default as TrieModel, TrieModelOptions } from "./trie-model.js";

View file

@ -1,18 +1,23 @@
// While we _could_ define this within @keymanapp/models-wordbreakers instead, it's probably
// better to leave that package as _just_ the wordbreakers.
export interface Token {
text: string,
isWhitespace?: boolean
}
export interface Tokenization {
/**
* An array of tokens to the left of the caret. If the caret is in the middle of a token,
* only the part to the left of the caret is included.
*/
left: USVString[],
left: Token[],
/**
* An array of tokens to the right of the caret. If the caret is in the middle of a token,
* only the part to the right of the caret is included.
*/
right: USVString[],
right: Token[],
/**
* A flag indicating whether or not the caret's position in the context caused a token
@ -22,21 +27,74 @@ export interface Tokenization {
caretSplitsToken: boolean
}
export function tokenize(wordBreaker: WordBreakingFunction, context?: Partial<Context>): Tokenization {
export function tokenize(
wordBreaker: WordBreakingFunction,
context: Partial<Context>,
options?: {
/** Characters to rejoin to preceding tokens if found immediately pre-caret. */
rejoins?: string[]
}
): Tokenization {
// The Unicode word-breaker algorithm looks for places where it's "safe" to
// split a word across lines, operating upon _completed_ words. There are
// some cases where, if placed mid-word, it would add a boundary that does not
// exist at the end of the word. The single-quote character is one such
// location - it's hard to tell if `can'` is the end of a quote or the prefix
// to `can't`. So, if `'` is immediately pre-caret, we "rejoin" it.
const rejoins = options?.rejoins || ["'"];
context = context || {
left: undefined,
startOfBuffer: undefined,
endOfBuffer: undefined
};
let leftSpans = wordBreaker(context.left || '') || [];
let rightSpans = wordBreaker(context.right || '') || [];
const leftSpans = wordBreaker(context.left || '') || [];
const rightSpans = wordBreaker(context.right || '') || [];
let leftTail: Span;
if(leftSpans.length > 0) {
leftTail = leftSpans[leftSpans.length - 1];
const tokenization: Tokenization = {
left: [],
right: [],
caretSplitsToken: false
}
// New step 1: process left-context.
let currentIndex = 0;
while(leftSpans.length > 0) {
const nextSpan = leftSpans[0];
if(Math.max(nextSpan.start, currentIndex) != currentIndex) {
const nextIndex = Math.max(currentIndex, nextSpan.start);
// Implicit whitespace span!
tokenization.left.push({
text: context.left!.substring(currentIndex, nextIndex),
isWhitespace: true
});
currentIndex = nextIndex;
} else {
leftSpans.shift();
// Explicit non-whitespace span.
tokenization.left.push({
text: nextSpan.text
});
currentIndex = Math.max(currentIndex, nextSpan.end);
}
}
// Detect any pre-caret whitespace after the final pre-caret non-whitespace
// token
//
// Note: the default wordbreaker won't need this code, as it emits a `''`
// after final whitespace.
if(context.left != null && currentIndex != context.left.length) {
const nextIndex = Math.max(currentIndex, context.left!.length);
tokenization.left.push({
text: context.left.substring(currentIndex, nextIndex),
isWhitespace: true
});
currentIndex = nextIndex;
}
// New step 2: handle any rejoins needed.
// Handle any desired special handling for directly-pre-caret scenarios - where for this
// _specific_ context, we should not make a token division where one normally would exist otherwise.
//
@ -45,62 +103,88 @@ export function tokenize(wordBreaker: WordBreakingFunction, context?: Partial<Co
// But, if the user is editing text and the caret is directly after a caret, there's a notable
// chance they may in the middle of typing a contraction. Refer to
// https://github.com/keymanapp/keyman/issues/6572.
if(leftSpans.length > 1) {
const leftTailBase = leftSpans[leftSpans.length - 2];
let leftTokenCount = tokenization.left.length;
if(leftTokenCount > 1) {
const leftTailBase = tokenization.left[leftTokenCount - 2];
const leftTail = tokenization.left[leftTokenCount - 1];
// If the final two pre-caret spans are adjacent - without intervening whitespace...
if(leftTailBase.end == leftTail!.start) {
if(!leftTailBase.isWhitespace && !leftTail.isWhitespace) {
// Ideal: if(leftTailBase is standard-char-class && leftTail is single-quote-class)
// But we don't have character class access here; it's all wordbreaker-function internal.
// Upon inspection of the wordbreaker data definitions... the single-quote-class is ONLY "'".
// So... we'll just be lazy for now and append the `'`.
if(leftTail!.text == "'") {
let mergedSpan: Span = {
text: leftTailBase.text + leftTail!.text,
start: leftTailBase.start,
end: leftTail!.end,
length: leftTailBase.length + leftTail!.length
};
leftSpans.pop(); // leftTail
leftSpans.pop(); // leftTailBase
leftSpans.push(mergedSpan);
leftTail = mergedSpan; // don't forget to update the `leftTail` Span!
if(rejoins.indexOf(leftTail!.text) != -1) {
tokenization.left.pop(); // leftTail
tokenization.left.pop(); // leftTailBase
tokenization.left.push({
text: leftTailBase.text + leftTail.text
});
leftTokenCount--;
}
}
}
// With any 'detokenization' cases already handled, we may now begin to build the return object.
let tokenization: Tokenization = {
left: leftSpans.map(span => span.text),
right: rightSpans.map(span => span.text),
// New step 3: right-context tokenization + token split detection
// A default initialization of the value.
caretSplitsToken: false
};
// context.right starts from index 0; it's an 'index reset'.
currentIndex = 0;
// Set a flag for special "first token" processing.
let firstRightToken = true;
// Now the hard part - determining whether or not the caret caused a token split.
if(leftSpans.length > 0 && rightSpans.length > 0) {
let rightHead = rightSpans[0];
// Note: is MOSTLY "WET" with the left-span loop, though the
// `caretSplitsToken` check is additional.
while(rightSpans.length > 0) {
const nextSpan = rightSpans[0];
if(Math.max(nextSpan.start, currentIndex) != currentIndex) {
const nextIndex = Math.max(currentIndex, nextSpan.start);
// Implicit whitespace span!
tokenization.right.push({
text: context.right!.substring(currentIndex, nextIndex),
isWhitespace: true
});
currentIndex = nextIndex;
} else {
const leftTail = tokenization.left[leftTokenCount-1];
if(leftTail) {
// If the first non-whitespace token to the right is non-whitespace,
// and the last token to the left is non-whitespace, the caret may
// be splitting a token.
if(firstRightToken && !leftTail.isWhitespace) {
if(wordBreaker(leftTail!.text + nextSpan.text).length == 1) {
tokenization.caretSplitsToken = true;
}
}
}
// If tokenization includes all characters on each side of the caret,
// we have a good candidate for a caret-splitting scenario.
let leftSuffixWordbreak = leftTail!.end != context.left!.length;
let rightPrefixWordbreak = rightHead.start != 0;
if(leftSuffixWordbreak || rightPrefixWordbreak) {
// Bypass the final test, as we already know the caret didn't split a token.
// (The tokenization process already removed characters between the two.)
return tokenization;
// Explicit non-whitespace span.
rightSpans.shift();
tokenization.right.push({
text: nextSpan.text
});
currentIndex = Math.max(currentIndex, nextSpan.end);
}
// Worth note - some languages don't use wordbreaking characters. So, a final check:
//
// Does the wordbreaker split a merge of the 'two center' tokens?
// If not, then the caret is responsible for the split.
if(wordBreaker(leftTail!.text + rightHead.text).length == 1) {
tokenization.caretSplitsToken = true;
}
// We've always processed the "first right token" after the first iteration.
// Do not run the caret-split check on any future iterations.
firstRightToken = false;
}
// Detect any pre-caret whitespace after the final pre-caret non-whitespace
// token
//
// Note: the default wordbreaker won't need this code, as it emits a `''`
// after final whitespace.
//
// Also note: is pretty much WET with the similar check after the
// leftSpan loop.
if(context.right && currentIndex != context.right.length) {
const nextIndex = Math.max(currentIndex, context.right!.length);
tokenization.right.push({
text: context.right.substring(currentIndex, nextIndex),
isWhitespace: true
});
currentIndex = nextIndex;
}
return tokenization;
@ -108,12 +192,18 @@ export function tokenize(wordBreaker: WordBreakingFunction, context?: Partial<Co
/**
* Get the last word of the phrase before the caret or nothing.
* If the last 'token' before the caret is whitespace, returns `''`.
* @param fullLeftContext the entire left context of the string.
*/
export function getLastPreCaretToken(wordBreaker: WordBreakingFunction, context: Context): string {
let tokenization = tokenize(wordBreaker, context);
if (tokenization.left.length > 0) {
return tokenization.left.pop() as string;
const lastToken = tokenization.left.pop();
if(lastToken!.isWhitespace) {
return '';
} else {
return lastToken!.text;
}
}
return '';

View file

@ -98,13 +98,9 @@ class Traversal implements LexiconTraversal {
}
child(char: USVString): LexiconTraversal | undefined {
/*
Note: would otherwise return the current instance if `char == ''`. If
such a call is happening, it's probably indicative of an implementation
issue elsewhere - let's signal now in order to catch such stuff early.
*/
// May result for blank tokens resulting immediately after whitespace.
if(char == '') {
return undefined;
return this;
}
// Split into individual code units.

View file

@ -0,0 +1,128 @@
/**
* The custom wordbreaker used by the sil.km.gcc model as of
* https://github.com/keymanapp/lexical-models/pull/265.
* @type {WordBreakingFunction}
* */
export function customWordBreakerProper(str) {
const whitespaceRegex = /\s|\u200b|\n|\r/;
const tokens = str.split(whitespaceRegex);
for(let i=0; i < tokens.length; i++) {
const token = tokens[i];
if(token.length == 0) {
tokens.splice(i, 1);
i--;
continue;
} else if(token.length == 1 && whitespaceRegex.test(token)) {
tokens.splice(i, 1);
i--;
continue;
}
// Certain punctuation marks should be considered a separate token from the word they're next to.
const punctuationMarks = ['«', '»', '$', '#' /* add extras here */];
const punctSplitIndices = [];
// Find if and where each mark exists within the token
for(let i = 0; i < punctuationMarks.length; i++) {
const split = token.indexOf(punctuationMarks[i]);
if(split >= 0) {
punctSplitIndices.push(split);
}
}
// Sort and pick the earliest mark's location. If none exists, use -1.
punctSplitIndices.sort();
const splitPoint = punctSplitIndices[0] === undefined ? -1 : punctSplitIndices[0];
if(splitPoint > -1) {
const left = token.substring(0, splitPoint); // (0, -1) => ''
const punct = token.substring(splitPoint, splitPoint+1);
const right = token.substring(splitPoint+1); // Starting past the end of the string => ''
if(left) {
tokens.splice(i++, 0, left);
}
tokens.splice(i++, 1, punct);
if(right) {
tokens.splice(i, 0, right);
}
// Ensure that the next iteration puts `i` immediately after the punctuation token... even if
// there was a `right` portion, as it may have extra marks that also need to be spun off.
i--;
}
}
let latestIndex = 0;
return tokens.map(function(token) {
const start = str.indexOf(token, latestIndex);
latestIndex = start + token.length;
return {
left: start,
start: start,
right: start + token.length,
end: start + token.length,
length: token.length,
text: token
}
});
}
/**
* The version of the custom wordbreaker used by the sil.km.gcc model
* before https://github.com/keymanapp/lexical-models/pull/265, which
* triggered #12200.
* @type {WordBreakingFunction}
* */
export function customWordBreakerFormer (str) {
const tokens = str.split(/\s|\u200b/);
for(let i=0; i < tokens.length; i++) {
const token = tokens[i];
if(token.length == 1) {
continue;
}
// Certain punctuation marks should be considered a separate token from the word they're next to.
const punctuationMarks = ['«', '»', '$', '#' /* add extras here */];
const punctSplitIndices = [];
// Find if and where each mark exists within the token
for(let i = 0; i < punctuationMarks.length; i++) {
const split = token.indexOf(punctuationMarks[i]);
if(split >= 0) {
punctSplitIndices.push(split);
}
}
// Sort and pick the earliest mark's location. If none exists, use -1.
punctSplitIndices.sort();
const splitPoint = punctSplitIndices[0] === undefined ? -1 : punctSplitIndices[0];
if(splitPoint > -1) {
const left = token.substring(0, splitPoint); // (0, -1) => ''
const punct = token.substring(splitPoint, splitPoint+1);
const right = token.substring(splitPoint+1); // Starting past the end of the string => ''
if(left) {
tokens.splice(i++, 0, left);
}
tokens.splice(i++, 1, punct);
if(right) {
tokens.splice(i, 0, right);
}
// Ensure that the next iteration puts `i` immediately after the punctuation token... even if
// there was a `right` portion, as it may have extra marks that also need to be spun off.
i--;
}
}
return tokens.map(function(token) {
return {
left: str.indexOf(token),
start: str.indexOf(token),
right: str.indexOf(token) + token.length,
end: str.indexOf(token) + token.length,
text: token
}
});
}

View file

@ -5,6 +5,29 @@
import { assert } from 'chai';
import * as models from "@keymanapp/models-templates";
import * as wordBreakers from "@keymanapp/models-wordbreakers";
import { customWordBreakerFormer, customWordBreakerProper } from './custom-breakers.def.js';
function asProcessedToken(text) {
// default wordbreaker emits these at the end of each context half if ending with whitespace.
// Indicates a new spot for non-whitespace text.
if(text == '') {
return {
text: text
};
} else if(text.trim() == '') {
// Simple cases using standard Latin-script patterns - can be handled via trim()
return {
text: text,
isWhitespace: true
};
}
// could add simple check for other, non-default cases here.
return {
text: text
};
}
describe('Tokenization functions', function() {
describe('tokenize', function() {
@ -19,8 +42,8 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(wordBreakers.default, context);
let expectedResult = {
left: ['The', 'quick', 'brown', 'fox'],
right: ['jumped', 'over', 'the', 'lazy', 'dog'],
left: ['The', ' ', 'quick', ' ', 'brown', ' ', 'fox'].map(asProcessedToken),
right: [' ', 'jumped', ' ', 'over', ' ', 'the', ' ', 'lazy', ' ', 'dog'].map(asProcessedToken),
caretSplitsToken: false
};
@ -30,7 +53,7 @@ describe('Tokenization functions', function() {
it('tokenizes English using defaults, pre-whitespace caret, partial context', function() {
let context = {
left: "quick brown fox", // No "The"
right: " jumped over the lazy", // No "dog"
right: " jumped over the lazy ", // No "dog"
startOfBuffer: false,
endOfBuffer: false
};
@ -38,8 +61,8 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(wordBreakers.default, context);
let expectedResult = {
left: ['quick', 'brown', 'fox'],
right: ['jumped', 'over', 'the', 'lazy'],
left: ['quick', ' ', 'brown', ' ', 'fox'].map(asProcessedToken),
right: [' ', 'jumped', ' ', 'over', ' ', 'the', ' ', 'lazy', ' ', ''].map(asProcessedToken),
caretSplitsToken: false
};
@ -59,14 +82,35 @@ describe('Tokenization functions', function() {
// Technically, we're editing the start of the first token on the right
// when in this context.
let expectedResult = {
left: ['The', 'quick', 'brown', 'fox', ''],
right: ['jumped', 'over', 'the', 'lazy', 'dog'],
left: ['The', ' ', 'quick', ' ', 'brown', ' ', 'fox', ' ', ''].map(asProcessedToken),
right: ['jumped', ' ', 'over', ' ', 'the', ' ', 'lazy', ' ', 'dog'].map(asProcessedToken),
caretSplitsToken: true
};
assert.deepEqual(tokenization, expectedResult);
});
it('tokenizes English using ascii-breaker, post-whitespace caret', function() {
let context = {
left: "The quick brown fox ",
right: "jumped over the lazy dog ",
startOfBuffer: true,
endOfBuffer: true
};
let tokenization = models.tokenize(wordBreakers.ascii, context);
// Technically, we're editing the start of the first token on the right
// when in this context.
let expectedResult = {
left: ['The', ' ', 'quick', ' ', 'brown', ' ', 'fox', ' '].map(asProcessedToken),
right: ['jumped', ' ', 'over', ' ', 'the', ' ', 'lazy', ' ', 'dog', ' '].map(asProcessedToken),
caretSplitsToken: false
};
assert.deepEqual(tokenization, expectedResult);
});
it('tokenizes English using defaults, post-whitespace caret, partial context', function() {
let context = {
left: "quick brown fox ",
@ -80,8 +124,8 @@ describe('Tokenization functions', function() {
// Technically, we're editing the start of the first token on the right
// when in this context.
let expectedResult = {
left: ['quick', 'brown', 'fox', ''],
right: ['jumped', 'over', 'the', 'lazy'],
left: ['quick', ' ', 'brown', ' ', 'fox', ' ', ''].map(asProcessedToken),
right: ['jumped', ' ', 'over', ' ', 'the', ' ', 'lazy'].map(asProcessedToken),
caretSplitsToken: true
};
@ -99,8 +143,8 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(wordBreakers.default, context);
let expectedResult = {
left: ['The', 'quick', 'brown', 'fox', 'jum'],
right: ['ped', 'over', 'the', 'lazy', 'dog'],
left: ['The', ' ', 'quick', ' ', 'brown', ' ', 'fox', ' ', 'jum'].map(asProcessedToken),
right: ['ped', ' ', 'over', ' ', 'the', ' ', 'lazy', ' ', 'dog'].map(asProcessedToken),
caretSplitsToken: true
};
@ -118,15 +162,15 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(wordBreakers.default, context);
let expectedResult = {
left: ['The', 'quick', 'brown', 'fox', 'jum'],
right: ['ped', 'over', 'the', 'lazy', 'dog'],
left: ['The', ' ', 'quick', ' ', 'brown', ' ', 'fox', ' ', 'jum'].map(asProcessedToken),
right: ['ped', ' ', 'over', ' ', 'the', ' ', 'lazy', ' ', 'dog'].map(asProcessedToken),
caretSplitsToken: true
};
assert.deepEqual(tokenization, expectedResult);
});
it('empty context case', function() {
it('properly handles empty-context cases', function() {
// Wordbreaking on a empty space => no word.
let context = {
left: '', startOfBuffer: true,
@ -144,7 +188,7 @@ describe('Tokenization functions', function() {
assert.deepEqual(tokenization, expectedResult);
});
it('nil context case', function() {
it('properly handles null context cases', function() {
// Wordbreaking on a empty space => no word.
let tokenization = models.tokenize(wordBreakers.default, null);
@ -157,7 +201,7 @@ describe('Tokenization functions', function() {
assert.deepEqual(tokenization, expectedResult);
});
it('near-empty context: one space before caret', function() {
it('properly handles a near-empty context: one space before caret', function() {
// Wordbreaking on a empty space => no word.
let context = {
left: ' ', startOfBuffer: true,
@ -167,7 +211,7 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(wordBreakers.default, context);
let expectedResult = {
left: [''],
left: [' ', ''].map(asProcessedToken),
right: [],
caretSplitsToken: false
};
@ -175,6 +219,85 @@ describe('Tokenization functions', function() {
assert.deepEqual(tokenization, expectedResult);
});
it('properly tokenizes partial English contractions - default setting', function() {
let context = {
left: "I can'",
right: "",
startOfBuffer: true,
endOfBuffer: true
};
let tokenization = models.tokenize(wordBreakers.default, context);
// Technically, we're editing the start of the first token on the right
// when in this context.
let expectedResult = {
left: ['I', ' ', 'can\''].map(asProcessedToken),
right: [].map(asProcessedToken),
caretSplitsToken: false
};
assert.deepEqual(tokenization, expectedResult);
});
it('overly tokenizes partial English contractions when (default) apostrophe rejoin is disabled', function() {
let context = {
left: "I can'",
right: "",
startOfBuffer: true,
endOfBuffer: true
};
let tokenization = models.tokenize(wordBreakers.default, context, { rejoins: [] });
// Technically, we're editing the start of the first token on the right
// when in this context.
let expectedResult = {
left: ['I', ' ', 'can' , '\''].map(asProcessedToken),
right: [].map(asProcessedToken),
caretSplitsToken: false
};
assert.deepEqual(tokenization, expectedResult);
});
it('properly tokenizes English contractions', function() {
// Note: a 'context0' with the caret before the `'` actually
// is not supported well yet; a leading `'` is broken from
// following text when in isolation.
let context1 = {
left: "I can'",
right: "t",
startOfBuffer: true,
endOfBuffer: true
};
let context2 = {
left: "I can't",
right: "",
startOfBuffer: true,
endOfBuffer: true
}
let tokenization1 = models.tokenize(wordBreakers.default, context1);
let tokenization2 = models.tokenize(wordBreakers.default, context2);
let expectedResult1 = {
left: ['I', ' ', 'can\''].map(asProcessedToken),
right: ['t'].map(asProcessedToken),
caretSplitsToken: true
};
let expectedResult2 = {
left: ['I', ' ', 'can\'t'].map(asProcessedToken),
right: [].map(asProcessedToken),
caretSplitsToken: false
};
assert.deepEqual(tokenization1, expectedResult1);
assert.deepEqual(tokenization2, expectedResult2);
});
// For the next few tests: a mocked wordbreaker for Khmer, a language
// without whitespace between words.
let mockedKhmerBreaker = function(text) {
@ -258,8 +381,8 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(mockedKhmerBreaker, context);
let expectedResult = {
left: ['ស្រុក'],
right: ['ខ្មែរ'],
left: ['ស្រុក'].map(asProcessedToken),
right: ['ខ្មែរ'].map(asProcessedToken),
caretSplitsToken: false
};
@ -282,8 +405,8 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(mockedKhmerBreaker, context);
let expectedResult = {
left: ['ស្រុ'],
right: ['ក', 'ខ្មែរ'],
left: ['ស្រុ'].map(asProcessedToken),
right: ['ក', 'ខ្មែរ'].map(asProcessedToken),
caretSplitsToken: true
};
@ -315,7 +438,7 @@ describe('Tokenization functions', function() {
return wordBreakers.default(text, customization);
}
it('treats caret as `eot` for pre-caret text', function() {
it('treats caret as `eot` for pre-caret text tokenization', function() {
let context = {
left: "don-", // We use a hyphen here b/c single-quote is hardcoded.
right: " worry",
@ -326,21 +449,21 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(wordBreakers.default, context);
assert.deepEqual(tokenization, {
left: ["don", "-"],
right: ["worry"],
left: ["don", "-"].map(asProcessedToken),
right: [" ", "worry"].map(asProcessedToken),
caretSplitsToken: false
});
tokenization = models.tokenize(midLetterNonbreaker, context);
assert.deepEqual(tokenization, {
left: ["don-"],
right: ["worry"],
left: ["don-"].map(asProcessedToken),
right: [" ", "worry"].map(asProcessedToken),
caretSplitsToken: false
});
});
it('handles mid-contraction tokenization', function() {
it('handles mid-contraction tokenization (via wordbreaker customization)', function() {
let context = {
left: "don:",
right: "t worry",
@ -351,24 +474,108 @@ describe('Tokenization functions', function() {
let tokenization = models.tokenize(wordBreakers.default, context);
assert.deepEqual(tokenization, {
left: ["don", ":"], // This particular case feels like a possible issue.
right: ["t", "worry"], // It'd be a three-way split token, as "don:t" would
// be a single token were it not for the caret in the middle.
// This particular case feels like a possible issue.
// It'd be a three-way split token, as "don:t" would
// be a single token were it not for the caret in the middle.
left: ["don", ":"].map(asProcessedToken),
right: ["t", " ", "worry"].map(asProcessedToken),
caretSplitsToken: false
})
tokenization = models.tokenize(midLetterNonbreaker, context);
assert.deepEqual(tokenization, {
left: ["don:"],
right: ["t", "worry"],
left: ["don:"].map(asProcessedToken),
right: ["t", " ", "worry"].map(asProcessedToken),
caretSplitsToken: true
});
});
it('mitigates effects of previously-distributed malformed wordbreaker output', function () {
const text = 'the quick brown fox jumped over the lazy dog ';
/** @type { Context } */
const context = {
left: text,
right: '',
startOfBuffer: true,
endOfBuffer: true
}
const tokenized = models.tokenize(customWordBreakerFormer, context);
// Mitigation aims to prevent the _worst_ side-effects that can result from invalidating the
// underlying assumption of a monotonically-increasing index within the context -
// assigning repeated or blank entries the text that preceded them!
assert.notExists(tokenized.left.find((token) => token.text == text));
assert.notExists(tokenized.left.find((token) => token.text.startsWith(text.substring(0, 25))));
// 'the' appears twice in the context, which should result in two separate 'the' tokens here.
// This was improperly handled when we didn't check that assumption.
assert.equal(tokenized.left.filter((token) => token.text == 'the').length, 2);
// Does not address multiple blank-token ('') entries that result from intervening spaces;
// that would add too much extra complexity to the method... and it can already be
// handled decently by the predictive-text engine.
assert.deepEqual(
tokenized.left
.filter((entry) => !entry.isWhitespace)
.filter((entry) => entry.text != '')
.map((entry) => entry.text),
['the', 'quick', 'brown', 'fox', 'jumped', 'over', 'the', 'lazy', 'dog']
);
});
it('properly works with well-formed custom wordbreaker output', function () {
const text = 'the quick brown fox jumped over the lazy dog ';
/** @type { Context } */
const context = {
left: text,
right: '',
startOfBuffer: true,
endOfBuffer: true
}
const tokenized = models.tokenize(customWordBreakerProper, context);
// Easier-to-parse version
assert.deepEqual(
tokenized.left
.filter((entry) => !entry.isWhitespace)
.map((entry) => entry.text),
['the', 'quick', 'brown', 'fox', 'jumped', 'over', 'the', 'lazy', 'dog']
);
// This time, with whitespaces.
assert.deepEqual(
tokenized.left.map((entry) => entry.text), [
'the',
' ',
'quick',
' ',
'brown',
' ',
'fox',
' ',
'jumped',
' ',
'over',
' ',
'the',
' ',
'lazy',
' ',
'dog',
' '
]
);
});
//
});
describe('getLastPreCaretToken', function() {
it('with pre-whitespace caret', function() {
it('operates properly with pre-whitespace caret', function() {
let context = {
left: "The quick brown fox",
right: " jumped over the lazy dog",
@ -381,7 +588,7 @@ describe('Tokenization functions', function() {
assert.equal(tokenization, 'fox');
});
it('with post-whitespace caret', function() {
it('operates properly with post-whitespace caret', function() {
let context = {
left: "The quick brown fox ",
right: "jumped over the lazy dog",
@ -394,7 +601,21 @@ describe('Tokenization functions', function() {
assert.equal(tokenization, '');
});
it('within a token', function() {
it('operates properly with post-whitespace caret, ascii breaker', function() {
let context = {
left: "The quick brown fox ",
right: "jumped over the lazy dog",
startOfBuffer: true,
endOfBuffer: true
};
let tokenization = models.getLastPreCaretToken(wordBreakers.ascii, context);
assert.equal(tokenization, '');
});
it('operates properly within a token', function() {
let context = {
left: "The quick brown fox jum",
right: "ped over the lazy dog",
@ -406,10 +627,15 @@ describe('Tokenization functions', function() {
assert.equal(tokenization, 'jum');
});
it('operates properly with no context', function() {
let tokenization = models.getLastPreCaretToken(wordBreakers.default, null);
assert.equal(tokenization, '');
});
});
describe('wordbreak', function() {
it('with pre-whitespace caret', function() {
it('operates properly with pre-whitespace caret', function() {
let context = {
left: "The quick brown fox",
right: " jumped over the lazy dog",
@ -422,7 +648,7 @@ describe('Tokenization functions', function() {
assert.equal(tokenization, 'fox');
});
it('with post-whitespace caret', function() {
it('operates properly with post-whitespace caret', function() {
let context = {
left: "The quick brown fox ",
right: "jumped over the lazy dog",
@ -437,7 +663,7 @@ describe('Tokenization functions', function() {
// This version is subject to change. In the future, we may wish the wordbreak
// operation to include "the rest of the word" - the post-caret part.
it('within a token', function() {
it('operates properly within a token', function() {
let context = {
left: "The quick brown fox jum",
right: "ped over the lazy dog",

View file

@ -1 +0,0 @@
The presence of this file tells CI to use the new builder_ style parameters for build.sh and unit_tests/test.sh.

View file

@ -10,7 +10,6 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
#
# TODO: future modules may include
# :lm-message-types \
# :sentry-manager \
#

View file

@ -1,15 +0,0 @@
# Keyman: Keyboard Processor Module
The Original Code is (C) 2017-2020 SIL International
The Keyboard Processor module is an internal component of KeymanWeb, seen within this repo at /web/.
## Minimum Compilation Requirements
* A local installation of [Node.js](https://nodejs.org/) v8.9+.
* Builds will call `npm install` to automatically install further necessary build dependencies.
* Linux users can update to LTS version of nodejs by following instructions on the [NodeSource Distributions](https://github.com/nodesource/distributions#table-of-contents) page.
**********************************************************************
The build script may be found at src/build.sh.

View file

@ -1,57 +0,0 @@
#!/usr/bin/env bash
#
# Compile KeymanWeb's 'keyboard-processor' module, one of the components of Web's 'core' module.
#
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
BUNDLE_CMD="node $KEYMAN_ROOT/common/web/es-bundling/build/common-bundle.mjs"
################################ Main script ################################
builder_describe "Builds the standalone, headless form of Keyman Engine for Web's input-processor module" \
"@/common/web/keyman-version" \
"@/common/web/keyboard-processor" \
"@/common/predictive-text" \
"@/developer/src/kmc-model test" \
"clean" \
"configure" \
"build" \
"test" \
"--ci Sets $(builder_term test) action to use CI-based test configurations & reporting"
builder_describe_outputs \
configure /node_modules \
build /common/web/input-processor/build/lib/index.mjs \
builder_parse "$@"
function do_build() {
tsc -b ./tsconfig.json
$BUNDLE_CMD "${KEYMAN_ROOT}/common/web/input-processor/build/obj/index.js" \
--out "${KEYMAN_ROOT}/common/web/input-processor/build/lib/index.mjs" \
--format esm
# Declaration bundling.
tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts
}
function do_test() {
local FLAGS=
if builder_has_option --ci; then
FLAGS="--reporter mocha-teamcity-reporter"
fi
mocha --recursive $FLAGS ./tests/cases/
}
builder_run_action configure verify_npm_setup
builder_run_action clean rm -rf build/
builder_run_action build do_build
builder_run_action test do_test

View file

@ -1,51 +0,0 @@
{
"name": "@keymanapp/input-processor",
"description": "The core text and prediction processing engine for KeymanWeb",
"repository": {
"type": "git",
"url": "git+https://github.com/keymanapp/keyman.git"
},
"keywords": [
"input",
"languages",
"keyboards"
],
"author": "SIL International",
"license": "MIT",
"bugs": {
"url": "https://github.com/keymanapp/keyman/issues"
},
"homepage": "https://github.com/keymanapp/keyman#readme",
"devDependencies": {
"@keymanapp/kmc": "*",
"@keymanapp/resources-gosh": "*",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"typescript": "^5.4.5"
},
"scripts": {
"test": "gosh ./test.sh"
},
"dependencies": {
"@keymanapp/keyboard-processor": "*",
"@keymanapp/lexical-model-layer": "*",
"@keymanapp/models-types": "*",
"@keymanapp/keyman-version": "*",
"@keymanapp/web-utils": "*",
"eventemitter3": "^5.0.0"
},
"type": "module",
"main": "./build/obj/index.js",
"types": "./build/obj/index.d.ts",
"exports": {
".": {
"es6-bundling": "./src/index.ts",
"default": "./build/obj/index.js"
},
"./lib": {
"types": "./build/lib/index.d.ts",
"import": "./build/lib/index.mjs"
},
"./obj/*.js": "./build/obj/*.js"
}
}

View file

@ -1,10 +0,0 @@
/// <reference path="../../../predictive-text/src/worker-interface.d.ts" />
export * from './corrections.js';
export * from './correctionLayout.js';
export { default as InputProcessor } from './text/inputProcessor.js';
export { default as ContextWindow } from './text/contextWindow.js';
export { default as ModelSpec } from './text/prediction/modelSpec.js';
export { default as LanguageProcessor, StateChangeEnum } from './text/prediction/languageProcessor.js';
export { default as PredictionContext } from './text/prediction/predictionContext.js';
export { TranscriptionCache } from './transcriptionCache.js';

View file

@ -1,19 +0,0 @@
{
"extends": "../tsconfig.kmw-main-base.json",
"compilerOptions": {
"baseUrl": "./",
// Necessary for ES5 iteration of Map.keys().
"downlevelIteration": true,
"outDir": "build/obj/",
"tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo",
"rootDir": "./src"
},
"references": [
{ "path": "../utils" },
{ "path": "../keyboard-processor" },
{ "path": "../../predictive-text/tsconfig.all.json" },
],
// The extra .d.ts is necessary to avoid issues with lack of a Worker type definition when not including
// the standard DOM in the TS "lib" configuration.
"include": ["./src/**/*.ts", "../../predictive-text/src/worker-interface.d.ts"]
}

View file

@ -1,18 +0,0 @@
# Build products
src/environment.inc.ts
# Legacy build folders.
output/
build/
embedded/
# Current build output folder (matches standard node publish location).
dist/
# Other local files.
node_modules/
unit_tests/modernizr.js
source/environment.inc.ts
.idea/**/*.xml
*.iml
*.kpj.user

View file

@ -1,95 +0,0 @@
#!/usr/bin/env bash
#
# Compile KeymanWeb's 'keyboard-processor' module, one of the components of Web's 'core' module.
#
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "${KEYMAN_ROOT}/resources/shellHelperFunctions.sh"
BUNDLE_CMD="node ${KEYMAN_ROOT}/common/web/es-bundling/build/common-bundle.mjs"
################################ Main script ################################
builder_describe \
"Compiles the web-oriented utility function module." \
"@/common/web/recorder test" \
"@/common/web/keyman-version" \
"@/common/web/es-bundling" \
"@/common/web/types" \
"@/common/web/utils" \
configure \
clean \
build \
test \
"--ci For use with action $(builder_term test) - emits CI-friendly test reports"
builder_describe_outputs \
configure /node_modules \
build /common/web/keyboard-processor/build/lib/index.mjs
builder_parse "$@"
function do_configure() {
verify_npm_setup
# Configure Web browser-engine testing environments. As is, this should only
# make changes when we update the dependency, even on our CI build agents.
playwright install
}
function do_build() {
tsc --build "${THIS_SCRIPT_PATH}/tsconfig.all.json"
# Base product - the main keyboard processor
builder_echo "Bundle base product - the main keyboard processor"
${BUNDLE_CMD} "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/index.js" \
--out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/index.mjs" \
--format esm
# The DOM-oriented keyboard loader
builder_echo "Bundle the DOM-oriented keyboard loader"
${BUNDLE_CMD} "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/keyboards/loaders/dom-keyboard-loader.js" \
--out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/dom-keyboard-loader.mjs" \
--format esm
# The Node-oriented keyboard loader
builder_echo "Bundle the Node-oriented keyboard loader"
${BUNDLE_CMD} "${KEYMAN_ROOT}/common/web/keyboard-processor/build/obj/keyboards/loaders/node-keyboard-loader.js" \
--out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/lib/node-keyboard-loader.mjs" \
--format esm \
--platform node
# Tests
builder_echo "Bundle tests"
${BUNDLE_CMD} "${KEYMAN_ROOT}/common/web/keyboard-processor/build/tests/dom/cases/domKeyboardLoader.spec.js" \
--out "${KEYMAN_ROOT}/common/web/keyboard-processor/build/tests/dom/domKeyboardLoader.spec.mjs" \
--format esm
# Declaration bundling.
builder_echo "Declaration bundling"
tsc --emitDeclarationOnly --outFile ./build/lib/index.d.ts
tsc --emitDeclarationOnly --outFile ./build/lib/dom-keyboard-loader.d.ts -p src/keyboards/loaders/tsconfig.dom.json
tsc --emitDeclarationOnly --outFile ./build/lib/node-keyboard-loader.d.ts -p src/keyboards/loaders/tsconfig.node.json
}
function do_test() {
local MOCHA_FLAGS=
local WTR_CONFIG=
if builder_has_option --ci; then
echo "Replacing user-friendly test reports with CI-friendly versions."
MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter"
WTR_CONFIG=.CI
fi
c8 mocha --recursive $MOCHA_FLAGS ./tests/node/
web-test-runner --config tests/dom/web-test-runner${WTR_CONFIG}.config.mjs
}
builder_run_action configure do_configure
builder_run_action clean rm -rf ./build
builder_run_action build do_build
builder_run_action test do_test

View file

@ -1,69 +0,0 @@
{
"name": "@keymanapp/keyboard-processor",
"description": "Core module for Keyman keyboard support in KeymanWeb.",
"repository": {
"type": "git",
"url": "git+https://github.com/keymanapp/keyman.git"
},
"keywords": [
"input",
"languages",
"keyboards"
],
"author": "SIL International",
"license": "MIT",
"bugs": {
"url": "https://github.com/keymanapp/keyman/issues"
},
"homepage": "https://github.com/keymanapp/keyman#readme",
"devDependencies": {
"@keymanapp/resources-gosh": "*",
"c8": "^7.12.0",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"typescript": "^5.4.5"
},
"scripts": {
"build": "gosh build.sh",
"clean": "gosh build.sh clean",
"test": "gosh build.sh test"
},
"dependencies": {
"@keymanapp/common-types": "*",
"@keymanapp/models-types": "*",
"@keymanapp/keyman-version": "*",
"@keymanapp/web-utils": "*"
},
"type": "module",
"main": "./build/obj/index.js",
"types": "./build/obj/index.d.ts",
"exports": {
".": {
"es6-bundling": "./src/index.ts",
"default": "./build/obj/index.js"
},
"./node-keyboard-loader": {
"es6-bundling": "./src/keyboards/loaders/node-keyboard-loader.ts",
"types": "./build/obj/keyboards/loaders/node-keyboard-loader.d.ts",
"import": "./build/obj/keyboards/loaders/node-keyboard-loader.js"
},
"./dom-keyboard-loader": {
"es6-bundling": "./src/keyboards/loaders/dom-keyboard-loader.ts",
"types": "./build/obj/keyboards/loaders/dom-keyboard-loader.d.ts",
"import": "./build/obj/keyboards/loaders/dom-keyboard-loader.js"
},
"./lib": {
"types": "./build/lib/index.d.ts",
"import": "./build/lib/index.mjs"
},
"./lib/node-keyboard-loader": {
"types": "./build/lib/node-keyboard-loader.d.ts",
"import": "./build/lib/node-keyboard-loader.mjs"
},
"./lib/dom-keyboard-loader": {
"types": "./build/lib/dom-keyboard-loader.d.ts",
"import": "./build/lib/dom-keyboard-loader.mjs"
},
"./obj/*.js": "./build/obj/*.js"
}
}

View file

@ -1,13 +0,0 @@
{
"extends": "../../../../tsconfig.kmw-main-base.json",
"compilerOptions": {
"baseUrl": "../../../",
"outDir": "../../../build/obj/keyboards/loaders/",
"tsBuildInfoFile": "../../../build/obj/keyboards/loaders/tsconfig.dom.tsbuildinfo",
"rootDir": "."
},
"references": [
{ "path": "../../../tsconfig.json" }
],
"include": ["dom-keyboard-loader.ts", "domKeyboardLoader.ts"],
}

View file

@ -1,14 +0,0 @@
{
"extends": "../../../../tsconfig.kmw-main-base.json",
"compilerOptions": {
"types": [ "node" ],
"baseUrl": "../../../",
"outDir": "../../../build/obj/keyboards/loaders/",
"tsBuildInfoFile": "../../../build/obj/keyboards/loaders/tsconfig.node.tsbuildinfo",
"rootDir": "."
},
"references": [
{ "path": "../../../tsconfig.json" }
],
"include": ["node-keyboard-loader.ts", "nodeKeyboardLoader.ts"],
}

View file

@ -1,5 +0,0 @@
Automated tests in this subfolder and its children are designed to facilitate simple, browser-independent
unit tests that are DOM-reliant.
Tests for anything that may reasonably vary depending upon the browser used to run the code should go under
the "integrated" folder instead.

View file

@ -1,62 +0,0 @@
// @ts-check
import { devices, playwrightLauncher } from '@web/test-runner-playwright';
import { esbuildPlugin } from '@web/dev-server-esbuild';
import { defaultReporter, summaryReporter } from '@web/test-runner';
import { LauncherWrapper, sessionStabilityReporter } from '@keymanapp/common-test-resources/test-runner-stability-reporter.mjs';
import { importMapsPlugin } from '@web/dev-server-import-maps';
import { dirname, resolve } from 'path';
import { fileURLToPath } from 'url';
const dir = dirname(fileURLToPath(import.meta.url));
const KEYMAN_ROOT = resolve(dir, '../../../../..');
/** @type {import('@web/test-runner').TestRunnerConfig} */
export default {
// debug: true,
browsers: [
new LauncherWrapper(playwrightLauncher({ product: 'chromium' })),
new LauncherWrapper(playwrightLauncher({ product: 'firefox' })),
new LauncherWrapper(playwrightLauncher({ product: 'webkit', concurrency: 1 })),
],
concurrency: 10,
nodeResolve: true,
files: [
'build/tests/dom/**/*.spec.mjs'
],
middleware: [
// Rewrites short-hand paths for test resources, making them fully relative to the repo root.
function rewriteResourcePath(context, next) {
if(context.url.startsWith('/resources/')) {
context.url = '/common/test' + context.url;
}
return next();
}
],
plugins: [
esbuildPlugin({ts: true, target: 'auto'}),
importMapsPlugin({
inject: {
importMap: {
// Redirects `eventemitter3` imports to the bundled ESM library. The standard import is an
// ESM wrapper around the CommonJS implementation, and WTR fails when it hits the CommonJS.
imports: {
'eventemitter3': '/node_modules/eventemitter3/dist/eventemitter3.esm.js'
}
}
}
})
],
reporters: [
summaryReporter({}), /* local-dev mocha-style */
sessionStabilityReporter({}),
defaultReporter({})
],
/*
Un-comment the next two lines for easy interactive debugging; it'll launch the
test page in your preferred browser.
*/
// open: true,
// manual: true,
rootDir: KEYMAN_ROOT
}

View file

@ -1,11 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": "../",
"outDir": "../build/tests/",
"tsBuildInfoFile": "../build/tests/tsconfig.tsbuildinfo",
"rootDir": "./"
},
"include": [ "./dom/**/*.ts"],
"exclude": []
}

View file

@ -1,18 +0,0 @@
// Is used by the keyboard-loader submodules.
{
"extends": "../tsconfig.kmw-main-base.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "build/obj/",
"tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo",
"rootDir": "./src/"
},
"references": [
{ "path": "../types" },
{ "path": "../../models/types" },
{ "path": "../keyman-version/" },
{ "path": "../utils/" }
],
"include": [ "./src/**/*.ts"],
"exclude": ["./src/keyboards/loaders/**/*.ts"]
}

View file

@ -1,12 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"declaration": true,
"outDir": "build/",
"sourceMap": true,
"lib": ["es6"],
"target": "es6"
},
"include": ["./*.ts"],
"exclude": ["test.ts"]
}

View file

@ -1,597 +0,0 @@
import { applyTransform, tokenize } from '@keymanapp/models-templates';
import { defaultWordbreaker } from '@keymanapp/models-wordbreakers';
import { ClassicalDistanceCalculation } from './classical-calculation.js';
import { SearchSpace } from './distance-modeler.js';
import TransformUtils from '../transformUtils.js';
function textToCharTransforms(text: string, transformId?: number) {
let perCharTransforms: Transform[] = [];
for(let i=0; i < text.kmwLength(); i++) {
let char = text.kmwCharAt(i); // is SMP-aware
let transform: Transform = {
insert: char,
deleteLeft: 0,
id: transformId
};
perCharTransforms.push(transform);
}
return perCharTransforms;
}
export class TrackedContextSuggestion {
suggestion: Suggestion;
tokenWidth: number;
}
export class TrackedContextToken {
raw: string;
replacementText: string;
transformDistributions: Distribution<Transform>[] = [];
replacements: TrackedContextSuggestion[];
activeReplacementId: number = -1;
get currentText(): string {
if(this.replacementText === undefined || this.replacementText === null) {
return this.raw;
} else {
return this.replacementText;
}
}
get replacement(): TrackedContextSuggestion {
let replacementId = this.activeReplacementId;
return this.replacements.find(function(replacement) {
return replacement.suggestion.id == replacementId;
});
}
revert() {
delete this.activeReplacementId;
}
}
export class TrackedContextState {
// Stores the post-transform Context. Useful as a debugging reference, but also used to
// pre-validate context state matches in case of discarded changes from multitaps.
taggedContext: Context;
model: LexicalModel;
tokens: TrackedContextToken[];
/**
* How many tokens were removed from the start of the best-matching ancestor.
* Useful for restoring older states, e.g., when the user moves the caret backwards, we can recover the context at that position.
*/
indexOffset: number;
// Tracks all search spaces starting at the current token.
// In the lm-layer's current form, this should only ever have one entry.
// Leaves 'design space' for if/when we add support for phrase-level corrections/predictions.
searchSpace: SearchSpace[] = [];
constructor(source: TrackedContextState);
constructor(model: LexicalModel);
constructor(obj: TrackedContextState | LexicalModel) {
if(obj instanceof TrackedContextState) {
let source = obj;
// Be sure to deep-copy the tokens! Pointer-aliasing is bad here.
this.tokens = source.tokens.map(function(token) {
let copy = new TrackedContextToken();
copy.raw = token.raw;
copy.replacements = [].concat(token.replacements);
copy.activeReplacementId = token.activeReplacementId;
copy.transformDistributions = [].concat(token.transformDistributions);
if(token.replacementText) {
copy.replacementText = token.replacementText;
}
return copy;
});
this.indexOffset = 0;
const lexicalModel = this.model = obj.model;
this.taggedContext = obj.taggedContext;
if(lexicalModel?.traverseFromRoot) {
// We need to construct a separate search space from other ContextStates.
//
// In case we are unable to perfectly track context (say, due to multitaps)
// we need to ensure that only fully-utilized keystrokes are considered.
this.searchSpace = obj.searchSpace.map((space) => new SearchSpace(space));
}
} else {
let lexicalModel = obj;
this.tokens = [];
this.indexOffset = Number.MIN_SAFE_INTEGER;
this.model = lexicalModel;
if(lexicalModel && lexicalModel.traverseFromRoot) {
this.searchSpace = [new SearchSpace(lexicalModel)];
}
}
}
get head(): TrackedContextToken {
return this.tokens[0];
}
get tail(): TrackedContextToken {
return this.tokens[this.tokens.length - 1];
}
popHead() {
this.tokens.splice(0, 2);
this.indexOffset -= 1;
}
pushTail(token: TrackedContextToken) {
if(this.model && this.model.traverseFromRoot) {
this.searchSpace = [new SearchSpace(this.model)]; // yeah, need to update SearchSpace for compatibility
} else {
this.searchSpace = [];
}
this.tokens.push(token);
let state = this;
if(state.searchSpace.length > 0) {
token.transformDistributions.forEach(distrib => state.searchSpace[0].addInput(distrib));
}
}
pushWhitespaceToTail(transformDistribution: Distribution<Transform> = null) {
let whitespaceToken = new TrackedContextToken();
// Track the Transform that resulted in the whitespace 'token'.
// Will be needed for phrase-level correction/prediction.
whitespaceToken.transformDistributions = transformDistribution ? [transformDistribution] : [];
whitespaceToken.raw = null;
this.tokens.push(whitespaceToken);
}
/**
* Used for 14.0's backspace workaround, which flattens all previous Distribution<Transform>
* entries because of limitations with direct use of backspace transforms.
* @param tokenText
* @param transformId
*/
replaceTailForBackspace(tokenText: USVString, transformId: number) {
this.tokens.pop();
// It's a backspace transform; time for special handling!
//
// For now, with 14.0, we simply compress all remaining Transforms for the token into
// multiple single-char transforms. Probabalistically modeling BKSP is quite complex,
// so we simplify by assuming everything remaining after a BKSP is 'true' and 'intended' text.
//
// Note that we cannot just use a single, monolithic transform at this point b/c
// of our current edit-distance optimization strategy; diagonalization is currently...
// not very compatible with that.
let backspacedTokenContext: Distribution<Transform>[] = textToCharTransforms(tokenText, transformId).map(function(transform) {
return [{sample: transform, p: 1.0}];
});
let compactedToken = new TrackedContextToken();
compactedToken.raw = tokenText;
compactedToken.transformDistributions = backspacedTokenContext;
this.pushTail(compactedToken);
}
updateTail(transformDistribution: Distribution<Transform>, tokenText?: USVString) {
let editedToken = this.tail;
// Preserve existing text if new text isn't specified.
tokenText = tokenText || (tokenText === '' ? '' : editedToken.raw);
if(transformDistribution && transformDistribution.length > 0) {
editedToken.transformDistributions.push(transformDistribution);
if(this.searchSpace) {
this.searchSpace.forEach(space => space.addInput(transformDistribution));
}
}
// Replace old token's raw-text with new token's raw-text.
editedToken.raw = tokenText;
}
toRawTokenization() {
let sequence: USVString[] = [];
for(let token of this.tokens) {
// Hide any tokens representing wordbreaks. (Thinking ahead to phrase-level possibilities)
if(token.currentText !== null) {
sequence.push(token.currentText);
}
}
return sequence;
}
}
class CircularArray<Item> {
static readonly DEFAULT_ARRAY_SIZE = 5;
private circle: Item[];
private currentHead: number=0;
private currentTail: number=0;
constructor(size: number = CircularArray.DEFAULT_ARRAY_SIZE) {
this.circle = Array(size);
}
get count(): number {
let diff = this.currentHead - this.currentTail;
if(diff < 0) {
diff = diff + this.circle.length;
}
return diff;
}
get maxCount(): number {
return this.circle.length;
}
get oldest(): Item {
if(this.count == 0) {
return undefined;
}
return this.item(0);
}
get newest(): Item {
if(this.count == 0) {
return undefined;
}
return this.item(this.count - 1);
}
enqueue(item: Item): Item {
var prevItem = null;
let nextHead = (this.currentHead + 1) % this.maxCount;
if(nextHead == this.currentTail) {
prevItem = this.circle[this.currentTail];
this.currentTail = (this.currentTail + 1) % this.maxCount;
}
this.circle[this.currentHead] = item;
this.currentHead = nextHead;
return prevItem;
}
dequeue(): Item {
if(this.currentTail == this.currentHead) {
return null;
} else {
let item = this.circle[this.currentTail];
this.currentTail = (this.currentTail + 1) % this.maxCount;
return item;
}
}
popNewest(): Item {
if(this.currentTail == this.currentHead) {
return null;
} else {
let item = this.circle[this.currentHead];
this.currentHead = (this.currentHead - 1 + this.maxCount) % this.maxCount;
return item;
}
}
/**
* Returns items contained within the circular array, ordered from 'oldest' to 'newest' -
* the same order in which the items will be dequeued.
* @param index
*/
item(index: number) {
if(index >= this.count) {
throw "Invalid array index";
}
let mappedIndex = (this.currentTail + index) % this.maxCount;
return this.circle[mappedIndex];
}
}
export class ContextTracker extends CircularArray<TrackedContextState> {
static attemptMatchContext(tokenizedContext: USVString[],
matchState: TrackedContextState,
transformDistribution?: Distribution<Transform>): TrackedContextState {
// Map the previous tokenized state to an edit-distance friendly version.
let matchContext: USVString[] = matchState.toRawTokenization();
// Inverted order, since 'match' existed before our new context.
let mapping = ClassicalDistanceCalculation.computeDistance(matchContext.map(value => ({key: value})),
tokenizedContext.map(value => ({key: value})),
1);
let editPath = mapping.editPath();
let poppedHead = false;
let pushedTail = false;
// Matters greatly when starting from a nil context.
if(editPath.length > 1) {
// First entry: may not be an 'insert' or a 'transpose' op.
// 'insert' allowed if the next token is 'substitute', as this may occur with an edit path of length 2.
if((editPath[0] == 'insert' && !(editPath[1] == 'substitute' && editPath.length == 2)) || editPath[0].indexOf('transpose') >= 0) {
return null;
} else if(editPath[0] == 'delete') {
poppedHead = true; // a token from the previous state has been wholly removed.
}
}
// Last entry: may not be a 'delete' or a 'transpose' op.
let tailIndex = editPath.length -1;
let ignorePenultimateMatch = false;
if(editPath[tailIndex] == 'delete' || editPath[0].indexOf('transpose') >= 0) {
return null;
} else if(editPath[tailIndex] == 'insert') {
pushedTail = true;
} else if(tailIndex > 0 && editPath[tailIndex-1] == 'insert' && editPath[tailIndex] == 'substitute') {
// Tends to happen when accepting suggestions.
pushedTail = true;
ignorePenultimateMatch = true;
}
// Can happen for the first text input after backspace deletes a wordbreaking character,
// thus the new input continues a previous word while dropping the empty word after
// that prior wordbreaking character.
//
// We can't handle it reliably from this match state, but a previous entry (without the empty token)
// should still be in the cache and will be reliable for this example case.
if(tailIndex > 0 && editPath[tailIndex-1] == 'delete' && editPath[tailIndex] == 'substitute') {
return null;
}
// Now to check everything in-between: should be exclusively 'match'es.
for(let index = 1; index < editPath.length - (ignorePenultimateMatch ? 2 : 1); index++) {
if(editPath[index] != 'match') {
return null;
}
}
// If we've made it here... success! We have a context match!
let state: TrackedContextState;
if(pushedTail) {
// On suggestion acceptance, we should update the previous final token.
// We do it first so that the acceptance is replicated in the new TrackedContextState
// as well.
if(ignorePenultimateMatch) {
// For this case, we were likely called by ModelCompositor.acceptSuggestion(), which
// would have marked the accepted suggestion.
matchState.tail.replacementText = tokenizedContext[tokenizedContext.length-2];
}
state = new TrackedContextState(matchState);
} else {
// We're continuing a previously-cached context; create a deep-copy of it.
// We can't just re-use the old instance, unfortunately; predictions break
// with multitaps otherwise - we should avoid tracking keystrokes that were
// rewound.
//
// If there are no incoming transforms, though... yeah, re-use is safe then.
state = !!transformDistribution ? new TrackedContextState(matchState) : matchState;
}
const hasDistribution = transformDistribution && Array.isArray(transformDistribution);
let primaryInput = hasDistribution ? transformDistribution[0].sample : null;
if(primaryInput && primaryInput.insert == "" && primaryInput.deleteLeft == 0 && !primaryInput.deleteRight) {
primaryInput = null;
}
const isWhitespace = primaryInput && TransformUtils.isWhitespace(primaryInput);
const isBackspace = primaryInput && TransformUtils.isBackspace(primaryInput);
const finalToken = tokenizedContext[tokenizedContext.length-1];
/* Assumption: This is an adequate check for its two sub-branches.
*
* Basis:
* - Assumption: one keystroke may only cause a single token to rotate out of context.
* - That is, no "reasonable" keystroke would emit enough code points to 'bump' two words simultaneously.
* - ... This one may need to be loosened a bit... but it should be enough for initial correction testing as-is.
* - Assumption: one keystroke may only cause a single token to be appended to the context
* - That is, no "reasonable" keystroke would emit a Transform adding two separate word tokens
* - For languages using whitespace to word-break, said keystroke would have to include said whitespace to break the assumption.
*/
function maintainLastToken() {
if(isWhitespace && editPath[tailIndex] == 'match') {
/*
We can land here if there are multiple whitespaces in a row.
There's already an implied whitespace to the left, so we conceptually
merge the new whitespace with that one.
*/
return;
} else if(isBackspace) {
// Consider backspace entry for this case?
state.replaceTailForBackspace(finalToken, primaryInput.id);
} else {
state.updateTail(primaryInput ? transformDistribution : null, finalToken);
}
}
// If there is/was more than one context token available...
if(editPath.length > 1) {
// We're removing a context token, but at least one remains.
if(poppedHead) {
state.popHead();
}
// We're adding an additional context token.
if(pushedTail) {
const tokenizedTail = tokenizedContext[tokenizedContext.length - 1];
/*
* Common-case: most transforms that trigger this case are from pure-whitespace Transforms. MOST.
*
* Less-common, but noteworthy: some wordbreaks may occur without whitespace. Example:
* `"o` => ['"', 'o']. Make sure to double-check against `tokenizedContext`!
*/
let pushedToken = new TrackedContextToken();
pushedToken.raw = tokenizedTail;
if(isWhitespace || !primaryInput) {
state.pushWhitespaceToTail(transformDistribution ?? []);
// Continuing the earlier assumption, that 'pure-whitespace Transform' does not emit any initial characters
// for the new word (token), so the input keystrokes do not correspond to the new text token.
pushedToken.transformDistributions = [];
} else {
state.pushWhitespaceToTail();
// Assumption: Since we only allow one-transform-at-a-time changes between states, we shouldn't be missing
// any metadata used to construct the new context state token.
pushedToken.transformDistributions = transformDistribution ? [transformDistribution] : [];
}
state.pushTail(pushedToken);
} else {
// We're editing the final context token.
// TODO: Assumption: we didn't 'miss' any inputs somehow.
// As is, may be prone to fragility should the lm-layer's tracked context 'desync' from its host's.
maintainLastToken();
}
// There is only one word in the context.
} else {
// TODO: Assumption: we didn't 'miss' any inputs somehow.
// As is, may be prone to fragility should the lm-layer's tracked context 'desync' from its host's.
if(editPath[tailIndex] == 'insert') {
// Construct appropriate initial token.
let token = new TrackedContextToken();
token.raw = tokenizedContext[0];
token.transformDistributions = [transformDistribution];
state.pushTail(token);
} else {
// Edit the lone context token.
maintainLastToken();
}
}
return state;
}
private static modelContextState(tokenizedContext: USVString[],
transformDistribution: Distribution<Transform>,
lexicalModel: LexicalModel): TrackedContextState {
let baseTokens = tokenizedContext.map(function(entry) {
let token = new TrackedContextToken();
token.raw = entry;
if(token.raw) {
token.transformDistributions = textToCharTransforms(token.raw).map(function(transform) {
return [{sample: transform, p: 1.0}];
});
} else {
// Helps model context-final wordbreaks.
token.transformDistributions = [];
}
return token;
});
// And now build the final context state object, which includes whitespace 'tokens'.
let state = new TrackedContextState(lexicalModel);
if(baseTokens.length > 0) {
state.pushTail(baseTokens.splice(0, 1)[0]);
}
while(baseTokens.length > 0) {
state.pushWhitespaceToTail();
state.pushTail(baseTokens.splice(0, 1)[0]);
}
if(state.tokens.length == 0) {
let token = new TrackedContextToken();
token.raw = '';
state.pushTail(token);
}
return state;
}
/**
* Compares the current, post-input context against the most recently-seen contexts from previous prediction calls, returning
* the most information-rich `TrackedContextState` possible. If a match is found, the state will be annotated with the
* input information provided to previous prediction calls and persisted correction-search calculations for re-use.
*
* @param model
* @param context
* @param transformDistribution
*/
analyzeState(model: LexicalModel,
context: Context,
transformDistribution?: Distribution<Transform>): TrackedContextState {
if(!model.traverseFromRoot) {
// Assumption: LexicalModel provides a valid traverseFromRoot function. (Is technically optional)
// Without it, no 'corrections' may be made; the model can only be used to predict, not correct.
throw "This lexical model does not provide adequate data for correction algorithms and context reuse";
}
let tokenizedContext = tokenize(model.wordbreaker || defaultWordbreaker, context);
if(tokenizedContext.left.length > 0) {
for(let i = this.count - 1; i >= 0; i--) {
const priorMatchState = this.item(i);
const priorTaggedContext = priorMatchState.taggedContext;
if(priorTaggedContext && transformDistribution && transformDistribution.length > 0) {
// Using the potential `matchState` + the incoming transform, do the results line up for
// our observed context? If not, skip it.
//
// Necessary to properly handle multitaps, as there are context rewinds that the
// predictive-text engine is not otherwise warned about.
//
// `priorTaggedContext` must not be `null`!
const doublecheckContext = applyTransform(transformDistribution[0].sample, priorTaggedContext);
if(doublecheckContext.left != context.left) {
continue;
}
} else if(priorTaggedContext?.left != context.left) {
continue;
}
let resultState = ContextTracker.attemptMatchContext(tokenizedContext.left, this.item(i), transformDistribution);
if(resultState) {
// Keep it reasonably current! And it's probably fine to have it more than once
// in the history. However, if it's the most current already, there's no need
// to refresh it.
if(this.newest != resultState && this.newest != priorMatchState) {
// Already has a taggedContext.
this.enqueue(priorMatchState);
}
resultState.taggedContext = context;
if(resultState != this.item(i)) {
this.enqueue(resultState);
}
return resultState;
}
}
}
// Else: either empty OR we've detected a 'new context'. Initialize from scratch; no prior input information is
// available. Only the results of the prior inputs are known.
//
// Assumption: as a caret needs to move to context before any actual transform distributions occur,
// this state is only reached on caret moves; thus, transformDistribution is actually just a single null transform.
let state = ContextTracker.modelContextState(tokenizedContext.left, transformDistribution, model);
state.taggedContext = context;
this.enqueue(state);
return state;
}
clearCache() {
while(this.count > 0) {
this.dequeue();
}
}
}

View file

@ -1,189 +0,0 @@
import { assert } from 'chai';
import { ContextTracker } from '#./correction/context-tracker.js';
import ModelCompositor from '#./model-compositor.js';
import * as models from '#./models/index.js';
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
describe('ContextTracker', function() {
function toWrapperDistribution(transform) {
return [{
sample: transform,
p: 1.0
}];
}
describe('attemptMatchContext', function() {
it("properly matches and aligns when lead token is removed", function() {
let existingContext = ["an", "apple", "a", "day", "keeps", "the", "doctor"];
let transform = {
insert: '',
deleteLeft: 0
}
let newContext = existingContext.slice(0);
newContext.splice(0, 1);
let rawTokens = ["apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor"];
let existingState = ContextTracker.modelContextState(existingContext);
let state = ContextTracker.attemptMatchContext(newContext, existingState, null, toWrapperDistribution(transform));
assert.isNotNull(state);
assert.deepEqual(state.tokens.map(token => token.raw), rawTokens);
});
it("properly matches and aligns when final token is edited", function() {
let existingContext = ["an", "apple", "a", "day", "keeps", "the", "docto"];
let transform = {
insert: 'r',
deleteLeft: 0
}
let newContext = existingContext.slice(0);
newContext[newContext.length - 1] = 'doctor';
let rawTokens = ["an", null, "apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor"];
let existingState = ContextTracker.modelContextState(existingContext);
let state = ContextTracker.attemptMatchContext(newContext, existingState, null, toWrapperDistribution(transform));
assert.isNotNull(state);
assert.deepEqual(state.tokens.map(token => token.raw), rawTokens);
});
it("properly matches and aligns when a 'wordbreak' is added", function() {
let existingContext = ["an", "apple", "a", "day", "keeps", "the", "doctor"];
let transform = {
insert: ' ',
deleteLeft: 0
}
let newContext = existingContext.slice(0);
newContext.push('');
let rawTokens = ["an", null, "apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor", null, ""];
let existingState = ContextTracker.modelContextState(existingContext);
let state = ContextTracker.attemptMatchContext(newContext, existingState, toWrapperDistribution(transform));
assert.isNotNull(state);
assert.deepEqual(state.tokens.map(token => token.raw), rawTokens);
// The 'wordbreak' transform
assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions);
assert.isEmpty(state.tokens[state.tokens.length - 1].transformDistributions);
});
it("properly matches and aligns when an implied 'wordbreak' occurs (as when following \"'\")", function() {
let existingContext = ["'"];
let transform = {
insert: 'a',
deleteLeft: 0
}
let newContext = existingContext.slice(0);
newContext.push('a'); // The incoming transform should produce a new token WITH TEXT.
let rawTokens = ["'", null, "a"];
let existingState = ContextTracker.modelContextState(existingContext);
let state = ContextTracker.attemptMatchContext(newContext, existingState, toWrapperDistribution(transform));
assert.isNotNull(state);
assert.deepEqual(state.tokens.map(token => token.raw), rawTokens);
// The 'wordbreak' transform
assert.isEmpty(state.tokens[state.tokens.length - 2].transformDistributions);
assert.isNotEmpty(state.tokens[state.tokens.length - 1].transformDistributions);
});
it("properly matches and aligns when lead token is removed AND a 'wordbreak' is added'", function() {
let existingContext = ["an", "apple", "a", "day", "keeps", "the", "doctor"];
let transform = {
insert: ' ',
deleteLeft: 0
}
let newContext = existingContext.slice(0);
newContext.splice(0, 1);
newContext.push('');
let rawTokens = ["apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor", null, ""];
let existingState = ContextTracker.modelContextState(existingContext);
let state = ContextTracker.attemptMatchContext(newContext, existingState, toWrapperDistribution(transform));
assert.isNotNull(state);
assert.deepEqual(state.tokens.map(token => token.raw), rawTokens);
// The 'wordbreak' transform
assert.isNotEmpty(state.tokens[state.tokens.length - 2].transformDistributions);
assert.isEmpty(state.tokens[state.tokens.length - 1].transformDistributions);
});
});
describe('modelContextState', function() {
it('models without final wordbreak', function() {
let context = ["an", "apple", "a", "day", "keeps", "the", "doctor"];
let rawTokens = ["an", null, "apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor"];
let state = ContextTracker.modelContextState(context);
assert.deepEqual(state.tokens.map(token => token.raw), rawTokens);
});
it('models with final wordbreak', function() {
let context = ["an", "apple", "a", "day", "keeps", "the", "doctor", ""];
let rawTokens = ["an", null, "apple", null, "a", null, "day", null, "keeps", null, "the", null, "doctor", null, ""];
let state = ContextTracker.modelContextState(context);
assert.deepEqual(state.tokens.map(token => token.raw), rawTokens);
});
});
describe('suggestion acceptance tracking', function() {
let englishPunctuation = {
quotesForKeepSuggestion: { open: `“`, close: `”`},
insertAfterWord: ' '
};
it('tracks an accepted suggestion', function() {
let baseSuggestion = {
transform: {
insert: 'world ',
deleteLeft: 3,
id: 0
},
transformId: 0,
id: 1,
displayAs: 'world'
};
let baseContext = {
left: 'hello wor', startOfBuffer: true, endOfBuffer: true
};
// Represents the keystroke that triggered the suggestion. It's not technically part
// of the Context when the suggestion is built.
let postTransform = {
insert: 'l',
deleteLeft: 0
};
let options = {
punctuation: englishPunctuation
};
let model = new models.TrieModel(jsonFixture('models/tries/english-1000'), options);
let compositor = new ModelCompositor(model);
let baseContextState = compositor.contextTracker.analyzeState(model, baseContext);
baseContextState.tail.replacements = [{
suggestion: baseSuggestion,
tokenWidth: 1
}];
let reversion = compositor.acceptSuggestion(baseSuggestion, baseContext, postTransform);
// Actual test assertion - was the replacement tracked?
assert.equal(baseContextState.tail.activeReplacementId, baseSuggestion.id);
assert.equal(reversion.id, -baseSuggestion.id);
// Next step - on the followup context, is the replacement still active?
let postContext = models.applyTransform(baseSuggestion.transform, baseContext);
let postContextState = compositor.contextTracker.analyzeState(model, postContext);
// Penultimate token corresponds to whitespace, which does not have a 'raw' representation.
assert.isNull(postContextState.tokens[postContextState.tokens.length - 2].raw);
// Final token is empty (follows a wordbreak)
assert.equal(postContextState.tail.raw, '');
});
});
});

View file

@ -1,13 +0,0 @@
// @ts-check
import BASE_CONFIG from './web-test-runner.config.mjs';
import teamcityReporter from '@keymanapp/common-test-resources/test-runner-TC-reporter.mjs';
import { sessionStabilityReporter } from '@keymanapp/common-test-resources/test-runner-stability-reporter.mjs';
/** @type {import('@web/test-runner').TestRunnerConfig} */
export default {
...BASE_CONFIG,
reporters: [
teamcityReporter(), /* custom-written, for CI-friendly reports */
sessionStabilityReporter({ciMode: true})
]
}

View file

@ -1,26 +0,0 @@
{
"extends": "../../models/tsconfig.kmw-worker-base.json",
"compilerOptions": {
"baseUrl": "./",
"outDir": "build/obj",
"tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo",
"rootDir": "./src/main",
// As this one is the one that directly interfaces with the worker (from the inside)
"lib": ["webworker", "es6"],
},
"references": [
// types
{ "path": "../../models/types" },
{ "path": "../lm-message-types" },
// modules
{ "path": "../keyman-version" },
{ "path": "../utils" },
{ "path": "../../models/templates" },
{ "path": "../../models/wordbreakers" },
],
"include": [
"src/main/**/*.ts"
]
}

View file

@ -1,22 +0,0 @@
{
"extends": "../tsconfig.kmw-main-base.json",
"compilerOptions": {
"types": ["node"],
"baseUrl": "./",
"outDir": "build/obj/",
"tsBuildInfoFile": "build/obj/tsconfig.tsbuildinfo",
"rootDir": "./src"
},
"include": [
"src/**/*.ts"
],
"references": [
{ "path": "../keyman-version" },
{ "path": "../utils/" },
{ "path": "../keyboard-processor/" },
{ "path": "../lm-message-types" }
],
}

View file

@ -73,40 +73,25 @@ function compile_schemas() {
node tools/schema-bundler.js
}
function copy_cldr_imports() {
# Store CLDR imports
# load all versions that have a cldr_info.json
for CLDR_INFO_PATH in "$KEYMAN_ROOT/resources/standards-data/ldml-keyboards/"*/cldr_info.json
do
# TODO-LDML: developer/src/inst/download.in.mak needs these also...
CLDR_PATH=$(dirname "$CLDR_INFO_PATH")
CLDR_VER=$(basename "$CLDR_PATH")
mkdir -p "$THIS_SCRIPT_PATH/build/src/import/$CLDR_VER"
# TODO-LDML: When these are copied, the DOCTYPE will break due to the wrong path. We don't use the DTD so it should be OK.
cp "$CLDR_INFO_PATH" "$CLDR_PATH/import/"*.xml "$THIS_SCRIPT_PATH/build/src/import/$CLDR_VER/"
done
}
function do_configure() {
compile_schemas
verify_npm_setup
}
function do_build() {
copy_cldr_imports
tsc --build
}
function do_test() {
eslint .
tsc --build test
c8 --skip-full --reporter=lcov --reporter=text mocha "${builder_extra_params[@]}"
readonly C8_THRESHOLD=50
c8 -skip-full --reporter=lcov --reporter=text --lines $C8_THRESHOLD --statements $C8_THRESHOLD --branches $C8_THRESHOLD --functions $C8_THRESHOLD mocha "${builder_extra_params[@]}"
builder_echo warning "Coverage thresholds are currently $C8_THRESHOLD%, which is lower than ideal."
builder_echo warning "Please increase threshold in build.sh as test coverage improves."
}
#-------------------------------------------------------------------------------------------------------------------
builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo
builder_run_action configure do_configure
builder_run_action build do_build
builder_run_action build tsc --build
builder_run_action test do_test
builder_run_action publish builder_publish_npm

View file

@ -31,24 +31,17 @@
"dependencies": {
"@keymanapp/ldml-keyboard-constants": "*",
"@keymanapp/keyman-version": "*",
"restructure": "3.0.1",
"semver": "^7.5.2",
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
"restructure": "3.0.1"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/git-diff": "^2.0.3",
"@types/mocha": "^5.2.7",
"@types/node": "^20.4.1",
"@types/semver": "^7.3.12",
"@types/xml2js": "^0.4.5",
"ajv": "^8.12.0",
"ajv-cli": "^5.0.0",
"ajv-formats": "^2.1.1",
"c8": "^7.12.0",
"chalk": "^2.4.2",
"git-diff": "^2.0.6",
"hexy": "^0.3.4",
"mocha": "^8.4.0",
"typescript": "^5.4.5"

View file

@ -0,0 +1,33 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* Modifier key bit-flags
*/
export const ModifierKeyConstants = {
// Define Keyman Developer modifier bit-flags (exposed for use by other modules)
// Compare against /common/include/kmx_file.h. CTRL+F "#define LCTRLFLAG" to find the secton.
LCTRLFLAG: 0x0001, // Left Control flag
RCTRLFLAG: 0x0002, // Right Control flag
LALTFLAG: 0x0004, // Left Alt flag
RALTFLAG: 0x0008, // Right Alt flag
K_SHIFTFLAG: 0x0010, // Either shift flag
K_CTRLFLAG: 0x0020, // Either ctrl flag
K_ALTFLAG: 0x0040, // Either alt flag
K_METAFLAG: 0x0080, // Either Meta-key flag (tentative). Not usable in keyboard rules;
// Used internally (currently, only by KMW) to ensure Meta-key
// shortcuts safely bypass rules
// Meta key = Command key on macOS, Windows key on Windows/Linux
CAPITALFLAG: 0x0100, // Caps lock on
NOTCAPITALFLAG: 0x0200, // Caps lock NOT on
NUMLOCKFLAG: 0x0400, // Num lock on
NOTNUMLOCKFLAG: 0x0800, // Num lock NOT on
SCROLLFLAG: 0x1000, // Scroll lock on
NOTSCROLLFLAG: 0x2000, // Scroll lock NOT on
ISVIRTUALKEY: 0x4000, // It is a Virtual Key Sequence
VIRTUALCHARKEY: 0x8000, // Keyman 6.0: Virtual Key Cap Sequence NOT YET
// Note: OTHER_MODIFIER = 0x10000, used by KMX+ for the
// other modifier flag in layers, > 16 bit so not available here.
// See keys_mod_other in keyman_core_ldml.ts
}

View file

@ -1,6 +1,5 @@
// Define standard keycode numbers (exposed for use by other modules)
// TODO-LDML: merge with common\web\keyboard-processor\src\text\codes.ts
/**
* May include non-US virtual key codes
@ -128,7 +127,7 @@ export const USVirtualKeyCodes = {
'k_?C1':193,
K_oDF:0xDF,
K_ODF:0xDF,
/*K_LOPT:50001,
K_LOPT:50001,
K_ROPT:50002,
K_NUMERALS:50003,
K_SYMBOLS:50004,
@ -139,7 +138,7 @@ export const USVirtualKeyCodes = {
K_SHIFTED:50009,
K_ALTGR:50010,
K_TABBACK:50011,
K_TABFWD:50012*/
K_TABFWD:50012
};
const k = USVirtualKeyCodes;

View file

@ -58,7 +58,7 @@ export const PRIVATE_USE_IDS = [
] as const;
/* A map of key field names with values matching the `typeof` the corresponding property
* exists in common/web/keyboard-processor, keyboards/activeLayout.ts.
* exists in /web/src/engine/keyboard/src/keyboards/activeLayout.ts.
*
* Make sure that when one is updated, the other also is. TS types are compile-time only,
* so the run-time-accessible mapping in activeLayout.ts cannot be auto-generated by TS. */

View file

@ -1,8 +1,9 @@
import { constants } from '@keymanapp/ldml-keyboard-constants';
import { DependencySections, StrsItem, UsetItem } from './kmx-plus.js';
import { ElementParser, ElementSegment, ElementType } from '../ldml-keyboard/pattern-parser.js';
import { MATCH_HEX_ESCAPE } from '../util/consts.js';
import { unescapeOneQuadString } from '../util/util.js';
import { ElementParser, ElementSegment, ElementType } from '../../ldml-keyboard/pattern-parser.js';
import { util } from '@keymanapp/common-types';
import MATCH_HEX_ESCAPE = util.MATCH_HEX_ESCAPE;
import unescapeOneQuadString = util.unescapeOneQuadString;
export enum ElemElementFlags {
none = 0,

View file

@ -1,12 +1,18 @@
import { constants } from '@keymanapp/ldml-keyboard-constants';
import * as r from 'restructure';
import { ElementString } from './element-string.js';
import { ListItem } from './string-list.js';
import { isOneChar, toOneChar, unescapeString, escapeStringForRegex } from '../util/util.js';
import { KMXFile } from './kmx.js';
import { UnicodeSetParser, UnicodeSet } from '@keymanapp/common-types';
import { VariableParser } from '../ldml-keyboard/pattern-parser.js';
import { MarkerParser } from '../ldml-keyboard/pattern-parser.js';
import { ListItem } from '../../ldml-keyboard/string-list.js';
import { util } from '@keymanapp/common-types';
import { KMX } from '@keymanapp/common-types';
import { UnicodeSetParser, UnicodeSet } from '../../ldml-keyboard/unicodeset-parser-api.js';
import { VariableParser } from '../../ldml-keyboard/pattern-parser.js';
import { MarkerParser } from '../../ldml-keyboard/pattern-parser.js';
import isOneChar = util.isOneChar;
import toOneChar = util.toOneChar;
import unescapeString = util.unescapeString;
import escapeStringForRegex = util.escapeStringForRegex;
import KMXFile = KMX.KMXFile;
// Implementation of file structures from /core/src/ldml/C7043_ldml.md
// Writer in kmx-builder.ts

View file

@ -1,4 +1,5 @@
import * as r from 'restructure';
import { ModifierKeyConstants } from '../consts/modifier-key-constants.js';
/* Definitions from kmx_file.h. Must be kept in sync */
@ -343,25 +344,25 @@ export class KMXFile {
public static readonly HK_CTRL = 0x00020000;
public static readonly HK_SHIFT = 0x00040000;
public static readonly LCTRLFLAG = 0x0001; // Left Control flag
public static readonly RCTRLFLAG = 0x0002; // Right Control flag
public static readonly LALTFLAG = 0x0004; // Left Alt flag
public static readonly RALTFLAG = 0x0008; // Right Alt flag
public static readonly K_SHIFTFLAG = 0x0010; // Either shift flag
public static readonly K_CTRLFLAG = 0x0020; // Either ctrl flag
public static readonly K_ALTFLAG = 0x0040; // Either alt flag
public static readonly LCTRLFLAG = ModifierKeyConstants.LCTRLFLAG; // Left Control flag
public static readonly RCTRLFLAG = ModifierKeyConstants.RCTRLFLAG; // Right Control flag
public static readonly LALTFLAG = ModifierKeyConstants.LALTFLAG; // Left Alt flag
public static readonly RALTFLAG = ModifierKeyConstants.RALTFLAG; // Right Alt flag
public static readonly K_SHIFTFLAG = ModifierKeyConstants.K_SHIFTFLAG; // Either shift flag
public static readonly K_CTRLFLAG = ModifierKeyConstants.K_CTRLFLAG; // Either ctrl flag
public static readonly K_ALTFLAG = ModifierKeyConstants.K_ALTFLAG; // Either alt flag
//public static readonly K_METAFLAG = 0x0080; // Either Meta-key flag (tentative). Not usable in keyboard rules;
// Used internally (currently, only by KMW) to ensure Meta-key
// shortcuts safely bypass rules
// Meta key = Command key on macOS, Windows key on Windows
public static readonly CAPITALFLAG = 0x0100; // Caps lock on
public static readonly NOTCAPITALFLAG = 0x0200; // Caps lock NOT on
public static readonly NUMLOCKFLAG = 0x0400; // Num lock on
public static readonly NOTNUMLOCKFLAG = 0x0800; // Num lock NOT on
public static readonly SCROLLFLAG = 0x1000; // Scroll lock on
public static readonly NOTSCROLLFLAG = 0x2000; // Scroll lock NOT on
public static readonly ISVIRTUALKEY = 0x4000; // It is a Virtual Key Sequence
public static readonly VIRTUALCHARKEY = 0x8000; // Keyman 6.0: Virtual Key Cap Sequence NOT YET
public static readonly CAPITALFLAG = ModifierKeyConstants.CAPITALFLAG; // Caps lock on
public static readonly NOTCAPITALFLAG = ModifierKeyConstants.NOTCAPITALFLAG; // Caps lock NOT on
public static readonly NUMLOCKFLAG = ModifierKeyConstants.NUMLOCKFLAG; // Num lock on
public static readonly NOTNUMLOCKFLAG = ModifierKeyConstants.NOTNUMLOCKFLAG; // Num lock NOT on
public static readonly SCROLLFLAG = ModifierKeyConstants.SCROLLFLAG; // Scroll lock on
public static readonly NOTSCROLLFLAG = ModifierKeyConstants.NOTSCROLLFLAG; // Scroll lock NOT on
public static readonly ISVIRTUALKEY = ModifierKeyConstants.ISVIRTUALKEY; // It is a Virtual Key Sequence
public static readonly VIRTUALCHARKEY = ModifierKeyConstants.VIRTUALCHARKEY; // Keyman 6.0: Virtual Key Cap Sequence NOT YET
// Note: OTHER_MODIFIER = 0x10000, used by KMX+ for the
// other modifier flag in layers, > 16 bit so not available here.

View file

@ -1,5 +1,5 @@
import { OrderedStringList } from '../ldml-keyboard/pattern-parser.js';
import { DependencySections, StrsItem, StrsOptions } from './kmx-plus.js';
import { OrderedStringList } from './pattern-parser.js';
import { DependencySections, StrsItem, StrsOptions } from '../kmx/kmx-plus/kmx-plus.js';
/**
* A single entry in a ListItem.

View file

@ -1,53 +1,19 @@
export * as KMX from './kmx/kmx.js';
export * as KMXPlus from './kmx/kmx-plus.js';
export { default as KMXBuilder } from './kmx/kmx-builder.js';
export { KmxFileReader, KmxFileReaderError } from './kmx/kmx-file-reader.js';
export * as KeymanTargets from './kmx/keyman-targets.js';
export * as VisualKeyboard from './kvk/visual-keyboard.js';
export { default as KMXPlusBuilder} from './kmx/kmx-plus-builder/kmx-plus-builder.js';
export { default as KvkFileReader } from './kvk/kvk-file-reader.js';
export { default as KvksFileReader } from './kvk/kvks-file-reader.js';
export { default as KvkFileWriter } from './kvk/kvk-file-writer.js';
export { default as KvksFileWriter } from './kvk/kvks-file-writer.js';
export * as KvkFile from './kvk/kvk-file.js';
export * as KvksFile from './kvk/kvks-file.js';
export * as LDMLKeyboard from './ldml-keyboard/ldml-keyboard-xml.js';
export { LDMLKeyboardTestDataXMLSourceFile } from './ldml-keyboard/ldml-keyboard-testdata-xml.js';
export { UnicodeSetParser, UnicodeSet } from './ldml-keyboard/unicodeset-parser-api.js';
export { VariableParser, MarkerParser } from './ldml-keyboard/pattern-parser.js';
export { LDMLKeyboardXMLSourceFileReader, LDMLKeyboardXMLSourceFileReaderOptions } from './ldml-keyboard/ldml-keyboard-xml-reader.js';
export { USVirtualKeyCodes } from './consts/virtual-key-constants.js';
export * as Constants from './consts/virtual-key-constants.js';
export { defaultCompilerOptions, CompilerBaseOptions, CompilerCallbacks, CompilerOptions, CompilerEvent, CompilerErrorNamespace,
CompilerErrorSeverity, CompilerPathCallbacks, CompilerFileSystemCallbacks, CompilerCallbackOptions,
CompilerError, CompilerMessageSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, CompilerFileCallbacks, compilerErrorSeverityName,
compilerErrorFormatCode, CompilerMessageDef,
compilerLogLevelToSeverity, CompilerLogLevel, compilerEventFormat, ALL_COMPILER_LOG_LEVELS,
ALL_COMPILER_LOG_FORMATS, CompilerLogFormat,
CompilerMessageOverride,
CompilerMessageOverrideMap,
KeymanCompilerArtifact,
KeymanCompilerArtifactOptional,
KeymanCompilerArtifacts,
KeymanCompilerResult,
KeymanCompiler
} from './util/compiler-interfaces.js';
export { CommonTypesMessages } from './util/common-events.js';
export { ModifierKeyConstants } from './consts/modifier-key-constants.js';
export * as TouchLayout from './keyman-touch-layout/keyman-touch-layout-file.js';
export { TouchLayoutFileReader } from './keyman-touch-layout/keyman-touch-layout-file-reader.js';
export { TouchLayoutFileWriter, TouchLayoutFileWriterOptions } from './keyman-touch-layout/keyman-touch-layout-file-writer.js';
export * as KPJ from './kpj/kpj-file.js';
export { KPJFileReader } from './kpj/kpj-file-reader.js';
export { KeymanDeveloperProject, KeymanDeveloperProjectFile, KeymanDeveloperProjectType, } from './kpj/keyman-developer-project.js';
export * as KpsFile from './package/kps-file.js';
export * as KmpJsonFile from './package/kmp-json-file.js';
export { Uni_IsSurrogate1, Uni_IsSurrogate2 } from './util/util.js';
@ -58,4 +24,8 @@ export * as KeymanFileTypes from './util/file-types.js';
export * as Schemas from './schemas.js';
export * as SchemaValidators from './schema-validators.js';
export * as xml2js from './deps/xml2js/xml2js.js';
export * as KMXPlus from './kmx/kmx-plus/kmx-plus.js';
// TODO: these exports are really not well named
export { UnicodeSetParser, UnicodeSet } from './ldml-keyboard/unicodeset-parser-api.js';
export { VariableParser, MarkerParser } from './ldml-keyboard/pattern-parser.js';
export { ElementString } from './kmx/kmx-plus/element-string.js';

Some files were not shown because too many files have changed in this diff Show more