יום ראשון, 16 במרץ 2014

תוכנה רב-לשונית - חלק ב'

בפוסט הקודם עסקנו במינוחים, תיאור תהליך והתרגום והצגנו בעייה אחת של קוד המטפל ב i18n: מיון טקסט (והסברנו גם איך להתמודד).

בפוסט הזה אדבר על עוד מספר בעיות: חיפוש, עיבוד מחרוזות (בשפות שונות), תאריכים, מידות ועוד...
עבור שני הנושאים הראשונים, חשוב להבין קצת איך בנוי טקסט ב Unicode ולכן אפתח את הפוסט ברקע על encoding ו Unicode.

מטרת הפוסט (ביחד עם הפוסט הקודם) היא לספק מקור משמעותי וטוב לנושאי המרת תוכנה לרב-לשוניות: בעיות - ופתרונות.




מבוא ל Unicode


בגדול, ישנן 4 משפחות נפוצות של קידודי טקסט:
  • ASCII, קידוד 7 ביט. עליו דיברנו בפוסט הקודם.
  • ANSI, הרחבות ל ASCII ול 8 ביט כאשר מוסיפים 128 תווים המתארים לרוב שפה נוספת לאנגלית.
    עד כמה שידוע לי אין שום תקן רשמי של ארגון התקנים האמריקאי (ANSI) לגבי קידוד. משום מה השם "ANSI" דבק בסדרה זו של תקנים שדווקא מתחילים בשמות כגון Windows או ISO (אלו ספציפית - באמת תקני ISO).
  • כל מיני קידודים שונים של שפות אסייתיות (GB, EUC, Big5 ועוד), כאשר הגיוון שלהן רחב מאוד.
  • Unicode - תקן יחיד המתאר מספר רב מאוד של שפות (בפועל: כל השפות המוכרות) תחת מרחב אותיות/סימנים אחד. כבר כמה שנים הוא משמש כתקן הבולט.
בעוד שבמסמכים המקודדים ב ASCII או ANSI מוגבלים במספר השפות שיופיעו במסמך (אנגלית + שפה נוספת כגון עברית או רוסית), ב Unicode ניתן לכתוב מסמך המכיל טקסט בכל השפות. בנוסף, שימוש ב Unicode מאפשר לכתוב תוכנה רב-לשונית בעוד הקוד (או המתכנתים) צריכים להכיר קידוד אחד בלבד - ולא עשרות קידודים שונים.

הותיקים ביננו בטח זוכרים כיצד אתרי אינטרנט בעברית שלא הצהירו על הקידוד שלהם אלצו את המשתמש לנסות קידוד אחר קידוד עד שראו עברית.
"אולי הפעם זה ISO-VISUAL...?!"

תקן Unicode יש רק אחד.
בתוך עולם ה Unicode הגדירו מרחב של אותיות / סימנים (כ 110,000 מהם - נכון לשלב זה), המתארים, פחות או יותר, את כל השפות / תחבירים המוכרים לאדם.
תקן ה Unicode ממשיך להתפתח (לינק: מה חדש בגרסאות אחרונות). בעוד רוב העדכונים כוללים שפות ביזריות נוספות (למשל: ארמית עתיקה), הם כוללים גם פה ושם הוספה של סימנים לשפות קיימות, או סימנים כלליים (צורות מיוחדות, סימנים או מטבעות וכו'). סימן המטבע "רופי", למשל, נכנס לתקן בגרסה 6 ולא זמין עדיין ברוב המערכות.

כל סימן ב Unicode מתואר ע"י קוד שנקרא Code Point ונכתב בצורה U+xxxx, כאשר x מתאר ספרה הקסדצימלית. למשל: ה code point של הסימן  הוא U+0164 וה code point של הסימן  ﴿  (סוגריים מעוטרים בערבית?!) הוא U+FD3F.
בגלל צורת הכתיבה הכוללת 4 ספרות ניתן להסיק ש Unicode מוגבל ל 65K סימנים - אך זה לא נכון: מעבר למרחב הבסיסי המתאר את הסימנים של השפות ה "נפוצות", הנקרא BMP (קיצור של Basic Multilingual Plane), ניתן להיתקל באותיות וסימנים ממרחבים נוספים (נקראים supplementary planes) - שם מרחב הסימנים, בפועל, איננו מוגבל.

קידודים

Unicode יש אחד, אבל דרכים לקודד אותו (Encodings) - יש כמה וכמה:
הנפוצים שבהם הם: UTF-32, UTF-16, UTF-8 ואולי גם נתקלתם ב UTF-7.
המחשבה שב UTF-8, קידוד 8 ביט, יש פחות אותיות מאשר ב UTF-32, קידוד 32 ביט, היא שגוייה: בעצם מדובר ב-2 דרכים לתאר את אותו מרחב סימנים של תקן ה Unicode.




הקידוד שהכי פשוט להסביר הוא UTF-32: כל אות מיוצגת ע"י אורך קבוע - 4 בתים / 32 ביט. קידוד זה הוא פשוט ומאפשר לבצע כמה פעולות חישוביות על הטקסט בצורה פשוטה למדי (לדוגמה: לקפוץ לאות ה n-ית). מצד שני, הוא "בזבזני" למדי במקום, מכיוון שהרוב הגדול של הסימנים שבשימוש (BMP) יכולים להיות מתוארים ע"י 16 ביט בלבד.

UTF-8, לעומת זאת הוא יותר חסכוני (והנפוץ מבין השלושה). את האותיות הנפוצות ביותר (בעת הגדרת הסטנדרט: לטיניות, מבוססות ASCII) - מתארים בעזרת בית אחד (8 ביט). אותיות נפוצות פחות (שפות מזרח אירופאיות, עברית ועוד), מתארים בעזרת שני בתים. אותיות יותר נדירות - בעזרת 3 או 4 בתים [א].
מצד אחד חוסכים מקום, מצד שני אורך האותיות הוא משתנה (1 עד 4 בתים) - מה שגורם להתעסקות נוספת.

UTF-16 הוא פתרון ביניים (הוא אגב, הקידוד הראשון שהופיע): כל סימן Unicode מיוצג ע"י 1 או 2 "words" של 16 ביט.
כאשר סימן אינו חלק מה BMP ואינו יכול להיות מתואר ע"י "מילה" בודדת של 16 ביט, UTF-16 ישתמש ב2 מלים של 16 ביט בכדי לתאר את הסימן. צמד מלים נקרא Surrogate Pair (תרגום לעברית של Surrogate: "בא-כח"), כאשר יש lead Surrogate ו trail Surrogate (או high ו low - גם מינוח שבשימוש). למרות שהרעיון של UTF-8 דומה מאוד, לא משתמשים שם, משום מה, במונח Surrogate.
שפת ג'אווה וסביבת NET. מקודדות את כל המחרוזות ב UTF-16 וכך גם מערכת ההפעלה Windows.

UTF-7, מקדד הכל ב 7-ביט בכדי לתאום למערכות ישנות שהניחו הנחות על אורך אות של 7 ביט בהתבסס על קידוד ה ASCII. השימוש הסביר היחידי שלו הוא לקידוד הודעות אימייל, אם השתמשתם בו לצורך אחר - קרוב לוודאי שעשיתם משהו לא נכון.

הערך המספרי הסופי של אות יכולה להיות זהה או שונה בין קידודים שונים. למשל, אומגה בתרשים לעיל זהה בין UTF-16 ו UTF-32, אבל שונה בין UTF-8 ל UTF-16.

שאלה: באיזה סדר שולחים את הערכים על הרשת? בשניהם, כמובן!
בימים הראשונים של Unicode התעקשו שיהיה גם ייצוג BE (קיצור של Big Endian) וגם ייצוג LE (קיצור של Little Endian, או "אינדיאני קטן" - כמו שקראנו לו באוניברסיטה) כדי לנצל מעבדי LE ו BE בצורה יעילה יותר. על כן נוסף לתקן זוג סימנים מיוחד הנקרא BOM (קיצור של Byte Order Mark) - המופיע בתחילת הטקסט ומתאר את הכיוון.
ב UTF-16, לדוגמה, 0xFEFF מתאר את BE ו 0xFFEF מתאר את LE.

יש גם גרסאות של UTF המצהירות מראש את מיקום ה Endian, למשל UTF-16LE או UTF-16BE (אם אתם נתקלים אי פעם בשמות הללו).


לעתים מתייחסים ל UTF-16 ו UTF-32 כ UCS-2 ו UCS-4, בהתאמה. UCS הוא קיצור של Universal Character Set.


את התפריט הזה של ++TextPad אתם אמורים להבין, בשלב זה


לינק: טבלת Code Points של Unicode



השפעות בקוד - עיבוד מחרוזות


כל זה טוב ויפה, מדוע זה אכפת לנו / כיצד זה קשור ל i18n?

נתחיל עם תרגיל ראשון: charAt.
אנו יודעים שבג'אווה השיטה (charAt(n מחזירה את הסימן ה n-י במחרוזת (כל אות היא 16 ביט).
זה נכון לאנגלית, רוסית, יוונית ותאילנדית - אבל יכול להיות לא-נכון עבור שפות אסייתיות (יפנים, סינית בתחביריהם השונים) או סימנים שונים של שפות שונות - שם אנו משתמשים ב Surrogate Pair של 2 chars לתאר סימן בודד!
כלומר, אם יש סימן המיוצג כ Surrogate Pair במקום ה n-4 וה n-3 (זהו זוג סימנים), אזי הפקודה (charAt(n תחזיר בעצם את האות ה n-1. אאוץ של באג!

דוגמה נוספת: אם אתם רוצים לספור כמה אותיות יש במחרוזת - לא ניתן לסמוך על התוצאה של השיטה ()length - אולי רק כערך מקסימום (וגם זה לא כ"כ פשוט... יש סימנים ב Unicode שבהגדרה צריכים לתפוס מקום של 2 אותיות, אבל זה עניין נדיר אחר).

סיפור מהחיים: היה לנו קוד בו הגבלנו את הקלט בשדה מסוים ל25 תווים - משיקולי מקום לפלט על המסך.
לקוח יפני פתח לנו באג שהוא לא מצליח להכניס בשדה הזה יותר מ 10-12 תווים, ובכלל נראה לו מאוד מוזר שבמילה מסוימת אחת - המערכת חוסמת אותו פעם אחרי 10 אותיות, ובמילה אחרת - רק אחרי 12 אותיות.

כנראה שכל התווים בהם השתמש היו surrogate pairs - ועל כן בדיקת ()length החזירה 26 כאשר הוא כתב מילה בת 13 אותיות. כיצד length קיבל ערך של 25 כאשר יש 10 אותיות בלבד? - על זה נדבר בהמשך.

מה עושים? הפתרון פשוט:


כאשר עובדים עם מחרוזות טקסט בשפה שאיננה אנגלית (אלו כנראה מיעוט המחרוזות בקוד), עברו להשתמש בשיטות של String שהן Unicode Aware: למשל codePointAt או CodePointCount.

ביתר פירוט:
  1. חיברנו 2 אותיות יפניות שדורשות לתאורן Surrogate Pairs, כלומר 2 chars כ"א. בג'אווה ניתן לקודד מילים של UTF-16 בעזרת u\.
  2. שימוש ב length יציג אורך 4, למרות שיש לי במחרוזת רק 2 אותיות.
    codePointCount סופר את מספר הסימנים (codePoints) של Unicode בתת-מחרוזת של ה String. אני רוצה לספור את התווים בכל המחרוזת.
  3. בעיה: charAt במקום ה 0 מחזיר סימן מוזר! גם charAt במקום ה 1, 2 או 3!
    הסיבה לכך היא שאין משמעות לחצי של Surrogate Pair, סוג של "חצי סימן" - וריבוע חלול הוא הודעת השגיאה של חלונות למצב שגוי שזה.
    codePointAt יתנהג כפי שהיינו מצפים מ charAt להתנהג במחרוזת אנגלית (ASCII/ANSI) טיפוסית.

עוד טיפ שאזרוק על הדרך הוא השימוש ב toUpperCase / toLowerCase.

לא לכל השפות יש Case (למשל: עברית) - וזו לא בעיה.

מצד שני, יש שפות בהן חוקי ה Case שונים מאנגלית - ואז שימוש בשיטות הנ"ל ייצר שגיאות.
שפה לדוגמה היא טורקית בה upperCase של i הוא לא I, אלא מין i גדול. מצד שני, ה lowerCase של I הוא לא i אלא מין I קטן. אני לא יודע להסביר מדוע זה כך - אבל זו ההתנהגות הרצויה.
הפתרון הוא פשוט: להשתמש בחתימה של ()toUpperCase עם אובייקט Locale. יש כזו - והיא תעשה את העבודה.


עיבוד מחרוזות Unicode - הפתעות לא צפויות


בכדי להבין את התכונה הבאה של Unicode, נפתח בניסוי הקטן הבא:
  1. פתחו אפליקציית notepad והקלידו משפט בעברית.
  2. העמידו את סמן העכבר לאחר אחת האותיות.
  3. וודאו שכפתור NUM LOCK דלוק (כלומר: יש אור).
  4. החזיקו את מקש ה Alt למטה והקלידו את המספר 0198 (כולל האפס לפני).
מה קיבלתֶם?

אם הניסוי עבר בהצלחה, אות שלפני הסמן אמורה לקבל ניקוד סֶגול. זו איננה יכולת של Notepad אלא של "חלונות" (ניתן לקרוא עוד פרטים ושיטות לנקד בלינק הבא)
מה שמעניין אותנו הוא דווקא כיצד מייצג תקן Unicode את האות המנוקדת.
אפשרות אחת הייתה ליצור פרמוטציה של כל האותיות העבריות (27 כולל סופיות) וכל הניקודים (15, על כל הצורות) - מה שידרוש כ 400 תווים ב Unicode.

מילא עברית, אבל מה עם הינדית - לה פי 3 אותיות, והרבה צורות חיבור שונות? מה עם סינית, יפנית, קוריאנית?!

דוגמה לכתיב הודי

מגדירי ה Unicode החליטו להימנע מהכפלת פרמוטציות ויצרו מכניזם יעיל יותר: מה שעשוי להראות למשתמש כאות/סימן אחד על המסך יכול להיות בעצם הרכבה של כמה סימני Unicode (כלומר code points) שונים.

שימו לב: זהו לא Surrogate!
יכול להיות code point יחיד המתואר ע"י שני characters - זהו Surrogate.
יכול להיות סימן ויזואלי אחד (מה שנקרא בעולם הזה grapheme), המתואר ע"י כמה code points, חלקם אולי surrogate - ואחרים לא.

מבלבל? הנה כמה דוגמאות:
  • grapheme של "A" מתואר ע"י char אחד
  • grapheme של האות היפנית "姶" מתואר ע"י 2 chars - כלומר surrogate Pair, מכיוון שהאות לא נמצאת במרחב הסימנים הבסיסי, ה BMP.
  • הסימן המוזר " X͏֞ " הוא grapheme המתואר ע"י שני chars (בעצם, שני code points) אחד הוא האות X שכולנו מכירים, והשני הוא מין גרשיים שמתלבשים על האות הקודמת במקום שהוקצה לה, מה שנקרא combining character - סימן ה"מתנחל" במרחב של הסימן שקדם לו.
הנה כמה דוגמאות ל combining characters:

Graphemes המורכבים מ 2, 3 וארבעה אותיות/סימנים. איך בדיוק הרכיבו את הדוגמה האחרונה - אני לא מבין, אבל אני מאמין שזה נכון.

שימו לב שלסימן ë יש 2 ייצוגים שונים: או כסימן (code point) בודד, או כהרכבה של שתי סימנים, האות e ועוד combining character (כל אחד הוא code point עצמאי).

כדי לסבך מעט העניין, grapheme שונים מצוירים לעתים בצורה שונה ע"פ ההקשר בו הם מופיעים. למשל יכלו להחליט לתאר אותיות סופיות (ם,ן,ץ,ף) בשפה העברית כ glyph (ייצוג ויזואלי מסוים ל grapheme) - אך לא עשו זאת, במקרה.
בשפות שונות יש כל סימנים שמוצגים אחרת ע"פ המיקום שלהם במילה/משפט/אותיות שכנות - למרות שזהו אותו ה code point או אותו ה grapheme. עניין זה נוגע יותר לפונטים ולא לייצוג של Unicode - אך כדאי להכיר את התופעה ולדעת שאם האות נראית אחרת - היא לא בהכרח סימן שונה ב Unicode.


משמעות מיידית למפתחים: עיבוד מחרוזות בשפות-שונות


ראינו קודם איך לבדוק את מספר ה Code Point - ושיטה זו אכן תעבוד ב 95% מהמקרים. מעטים מאוד המקרים בהם משתמשים ב combining characters לכתוב בשפה אירופאית (משתמשים בגרסה המשולבת, למשל ë), אבל בשפות אסייתיות לפעמים אין ברירה אחרת. לפעמים מישהו השתמש בכלי שיצר לו צרופים כאלו.

מה עושים?

הנה מתודה שכתבתי בג'אווה שעושה את העבודה:


המפתח לפתרון הוא שימוש ב breakIterator מסוג Character שהולך תו נראה (grapheme) אחרי תו נראה על המחרוזת, יהיה זה combining character, surrogate או שילוב של שניהם, עד שהוא מגיע לסופה (ואז הוא מחזיר מין ערך ריק - לכן ה 1-).


השימוש ב breakIterators עשוי להיות חשוב מאוד - אין לכם מושג אלו שפות מוזרות יש בעולם:
  • יש שפות, כמו אמהרית או בורמזית, בהן רווח הוא לא המפריד בין מילה למילה. נסו להשתמש ב (" ")split - והגורל יצחק לכם בפנים.
  • בטיבטית, למשל, יש סימן מיוחד לרדת שורה. מי שכותב טיבטית לא יקליד n\ כי אם סימן אחר.
  • ביפנית וסינית משתמשים ברווחים להפריד בין ביטויים / משפטים, ולא בין מלים.
יש breakIterators לרמות תחביריות שונות: מילים, משפטים, שורות וכו'. אם אתם רציניים לגבי i18n ועיבוד מחרוזות בשפות שונות - כדאי שתשתמשו בכלי זה.

אני ממליץ גם להציץ על הטבלה המעניינת הבאה, המציגה רשימת "פיצ'רים" של שפות. אלו הפתעות צפויות - והיכן. הרשימה איננה מלאה.


חיפוש ו Normalization


טוב, בשלב זה עניין ה unicode אמור להיות דיי מובן. אנסה לכסות בקצרה עוד כמה pitfalls אפשריים.
נניח שיש לנו אפליקציית TODO List בה המשתמש מקליד משימות שעליו לבצע. המשימות נשמרות בבסיס נתונים, והמשתמש יכול מאוחר יותר לחפש ע"פ הטקסט שהקליד.

אנו יכולים לספק exact match search - חיפוש שעובד ע"פ שיוון רצפים של characters במחרוזת. מימוש נאיבי שכזה ייכשל למצוא את "Do" כאשר אנו מחפשים את "do".

הנירמול הבסיס ביותר אם כן עבור חיפוש הוא מעבר ל upper case (או lower - לא משנה). כפי שראינו קודם, חשוב לבצע אותו עם אובייקט Locale עם השפה הנכונה.
למשל: מהו ה upper case של Fuβ? אם אתם זוכרים β הוא קיצור של שני s (ב lower case) בגרמנית. צורת ה UpperCase בגרמנית היא לכן FUSS - טקסט בעל אורך אחר של אותיות.

עוד הבחנה מעניינת ב Unicode היא בין שוויון קאנוני ושוויון תאימות (או סמטני):


אם המשתמש שלכם מחפש אחר טקסט עם הסימן Ä, האם אכפת לו איך הוקלד הטקסט במקור / כיצד הוא מיוצג בבסיס הנתונים??
האם כ U+00C4, או כ combination sequence של U+0041 ו U+0308? - כנראה שלא.

אם הוא לא ימצא את הטקסט ויגלה שמשהו שנראה בדיוק אותו הדבר נמצא ברשימה - הוא פשוט יפתח לכם באג. "התוכנה לא עובדת כמו שצריך".

הערה: כאשר אנשים מקלידים את הטקסט אין ממש סיבה שיהיה שימוש ב combination sequence, אלא אם הם יכולים לכתוב את הטקסט בעורך טקסט לא ידוע. כלומר: מבחינת ניהול סיכונים, אם אתם כותבים אפליקציה ל iPhone - יש סיכוי טוב שאתם יכולים להימנע מטיפול normalization. אם המערכת שלכם היא מערכת שאותו טקסט משמש משתמשים רבים, שיכולים לכתוב אותו בעורכים שונים - אזי עניין זה נהיה ממשי.

לגבי שוויון תאימות (compatibility equivalence), שהוא סוג של שוויון סמנטי - ייתכן ומשתמשים רבים יקבלו את רוע הגזרה: חוץ מהאמריקאים, כנראה שכל העמים חווים תוכנות שלא עבודות בצורה מושלמת עם השפה שלהם. בכל זאת, אם כבר עושים טיפול - שווה לכסות כבר את האזור הזה גם כן.
קחו לדוגמה את השפה הערבית, בה יש סימנים שונים של Unicode לאותיות מחוברות () ולא מחוברות (ﻦ , ﻨ). דיי מרגיז לבצע חיפוש ולא למצוא מילה כי אות אחת הייתה מחוברת או לא מחוברת.
עוד דוגמה נפוצה (ומעצבנת) היא ההבדל הסמנטי בין מרכאות ניטראליות ( " ) למרכאות בעלות כיוון ( , ‟ ). יש להן code points שונים בתכלית - אך משמעות סמנטית זהה.

שימו לב ששוויון תאימות (למשל הדוגמה למעלה מהשפה הצרפתית) יוצר טקסט באורך שונה מבחינת מספר ה graphemes - למרות שהוא סמנטית זהה. כלומר: גם BreakIterator אינו חסין במקרים אלו בפני טעויות.

תקן ה Unicode מגדיר 4 צורות לנרמל טקסט:

מקור: וויקיפדיה

האם זה משנה באיזו צורה בוחרים?
ובכן: כן. אם מישהו מקליד טקסט בצורה האלגנטית, תוך שימוש בקיצור כגון   או כתב מחובר ואתם מנרמלים את הטקסט ל fi (שני סימנים) - הוא עלול להתבאס.
ברירת המחדל היא לנרמל ל NFC, אך יש שיקולים שונים לבחור בצורות נרמול אחרות.

בסופו של דבר, את הנרמול ניתן לבצע בקלות בעזרת מחלקה בשם UnicodeNormalizer המגיעה כחלק מספריית ICU, הנה דוגמת קוד לשימוש.

עוד נקודה חשובה: כל צורות הנרמול הן "בטוחות" - לא תפגעו בטקסט אם תנרמלו אותו, באותה השיטה, פעם נוספת.

קישור: FAQs אודות normalization ב Unicode.







BiDi ו RtL ב Unicode

נושא זה הוא לא כ"כ בסיסי, אך בגלל שאנו דוברי עברית - החלטתי לתאר בקצרה כיצד עניין ה Bi-Directional (בקיצור BiDi) עובד בקידוד של Unicode.
מילת המפתח היא Bi-Directional ולא RtL (קרי Right-to-Left). עברית וערבית הן שפות שנכתבות מימין לשמאל, אך מספרים וקטעי טקסט לטיני (למשל: אנגלית) - נכתבים בהן משמאל לימין.

תקן Unicode ממפה את כל האותיות / סימנים ל 3 קבוצות:
  • סימנים שהם LtR - רובם המכריע של הסימנים.
  • סימנים שהם RtL - עברית, ערבית ואולי עוד כמה שפות (בעיקר עתיקות?!)
  • סימנים ניטראליים - כמו סימני פיסוק ("!"), רווחים ועוד.
ברגע שהמחשב נתקל ברצף סימני ה Unicode הבא (סדר לוגי):

פייל40טסאל

הוא מציג אותו על המסך בצורה הבאה (סדר ויזואלי):


לאסט40לייפ

מי שמטפל בקידוד ה Unicode מזהה את טווח הסימנים העבריים שמסומן כ RtL ומציג אותם על המסך מימין לשמאל. כאשר הוא נתקל בסימן "4", המסומן כ LtR הוא הופך את כיוון התצוגה, כנ"ל לגבי האות "ל" לאחר ה "0".

מה קורה עם סימנים ניטראליים? הם מקבלים את הכיוון האחרון שיושם.

ומה אם "כוונת המשורר" היא אחרת (כזו שנשלטת ע"י עורך הטקסט - כמובן)?

ישנם מספר סימנים "בלתי נראים" ב Unicode הקובעים סדר ויזואלי.
למשל:

A?ג

יתפרש באופן טבעי כ ג?A, אך אם נוסיף סימן מפורש של RtL:

A\u200F

אזי הטקסט יוצג כ ?גA. כלומר: ציינו ש "?" מסודר ויזואלית מימין לשמאל.

מלבד סימני RtL ו LtR, יש סימני-כיוון מתקדמים יותר, לא מפורשים, המאפשרים לנהל רצפים של שינויי כיוון ולהתעלם מהכיוון המוגדרת על האות. הלוגיקה שלהם מורכבת יותר, ואם זה רלוונטי עבורכם אתם יכולים לקרוא בלינק הבא מאמר טוב ומסכם.

לינק רלוונטי (זהירות: hardcore): תיאור מלא של האלגוריתם לפענוח BiDi ב Unicode.




סרגל להמרת מידות. משהו שהיה בשימוש עד שנות ה-70


מידות ומדידות


הנושא הבא נוגע יותר לאזורים גאוגרפיים, מאשר שפות.
הביטו בביטוי המספרי הבא:

1,000

בכמה מדובר?
קרוב לוודאי שתאמרו שזהו "אלף". כך גם יאמר האמריקאי או היפני, אך סביר שגרמני דווקא יסיק שמדובר במספר "אחד". אחד?! ... כן.

ישנן 3 צורות נפוצות לתאר את המספר 10000 (עשר אלף):
  1. 10,000.00
  2. 10.000,00
  3. 000,00 10
הצורה הראשונה מקובלת בישראל, ארה"ב, יפן, אנגליה, אוסטרליה ועוד.
הצורה השנייה מקובלת בגרמניה, איטליה, ספרד, ברזיל ועוד.
הצורה השלישית מקובלת ברוסיה, צרפת ועוד.
ויש לפחות עוד כמה צורות אחרות... בשוויץ, כותבים עשר-אלף כך: 000.00'10

ניסיון אישי קצר: יוצא לי להשתמש בתוכנה גרמנית שאינה מותאמת לצורת הכתיבה הישראלית, ואני נדרש לכתוב את המספרים בצורה השנייה. זה אפשרי, אך מבלבל ולא נוח....

הפתרון לבעיה המוצגת למעלה הוא פשוט ביותר, להשתמש באובייקט NumberFormat בג'אווה (אני מניח שיש מקבילה, אולי כספרייה נוספת, בכל השפות הנפוצות). אובייקט זה מקבל בעת יצירת המופע לוקאל ועל פיו הוא יבצע formatting נכון של המספרים.

החכמה פה, כמו בדוגמאות אחרות - היא בעיקר להכיר את הבעיה, ולהיערך אליה נכון.


דוגמה נוספת: תאריך ושעה.
תאריך מושפע מ 3 גורמים עקריים:
  1. לוח השנה: למשל, גרוגאני (מערבי), עברי , יפני ,סיני ועוד.
  2. ה Time zone - היכן נמצא המשתמש הספציפי על הגלובוס, וה day light saving.
  3. הפורמט בו בקובל לכתוב תאריך באותו האזור (region).
יש מדינות בהן היום האחרון במילניום ייכתב כ 31/12/1999 או 12/31/1999 או 1999/12/31.
כאשר מדובר ביום וחודש ששניהם קטנים מ12 - הטעות יכולה להיות קריטית.
יש מדינות בהן יש העדפה ברורה לצורת ההפרדה: נקודה, קו נטוי, מקף או רווח. הנה סיכום יפה של האפשרויות השונות.

בג'אווה, האובייקטים Calendar, TimeZone ו DateFormat יעשו עבורכם את העבודה.

כאשר שומרים תאריך/זמן שייקרא אח"כ פוטנציאלית ע"י משתמש אחר / אותו המשתמש באזור זמן אחר - כדאי לשמור את כל הזמנים ע"י אזור זמן אחיד (בד"כ UTC) ולהתאים אח"כ, בזמן ריצה, לכל משתמש ע"פ הלוקאל הנוכחי שלו.

יש עוד נושאים הדורשים התאמה (Localization) לאזורים מסוימים: מטבע (כיצד קובעים שער חליפין), שיטות מדידה (מטרית, אניצ'ית) הדרך בה כותבים מספרי טלפון, כתובת דואר או מיקוד.
לצערי, אין לי תובנות מיוחדות כיצד להתמודד עם נושאים אלו. פשוט לעשות וללמוד, ללמוד ולעשות.



השלמה: תרגום טקסט עם פרמטרים.

בפוסט הראשון דיברתי לא-מעט על תרגום טקסט וטיפול בקבצי-תרגום, אך שכחתי לציין נקודה חשובה בהכנת הקוד לתרגום שנוטים ליפול בה: לחלק את ה user string לתרגום לחלקים.

כמה דוגמאות לטקסט באנגלית, וצורת העברה לתרגום (i18n) - שהיא בעייתית:
  1. "Do you want to delete item " + getId() + " ?"  --> "{KEY_DEL_APPROVAL_TRNSLT} {0} ?", getId()
  2. "An error occurred with " + "the cooling system" --> "{ERROR_OCCR_W_TRNSLT} {COOLING_SYS_TRNSLT}"
  3. "There are 2 more steps for completion" --> "{TRNSLT_1} 2 {TRNSLT_2}" 


מה הבעיה?
  1. בדוגמה זו יש משתנה שנכנס למשפט. המתכנת (הדמיוני) חילק את המשפט ל"קטע מתורגם", "מספר" ו"סימן שאלה".
    אבל.... בשפות שונות מבנה שאלה הוא אחר:
    בתאילנדית (סתם אני זוכר מהטיול) - על המשפט להתחיל במילת שאלה.
    בגרמנית - הפועל "מחיקה" הוא המילה האחרונה במשפט.
    בספרדית - בנוסף לסימן השאלה, המשפט יכול להתחיל בסימן שאלה הפוך (¿).הכי גרוע: המתרגם עלול לאבד את ההקשר.
  2. במקרה זה המתכנת ניסה לעשות "שימוש חוזר" בחלק המשפט שחוזר על עצמו, אולי בכדי לחסוך בתרגום (מתכנת דמיוני - זוכרים?). חלוקה זו ל 2 משפטים יכולה להתפרק בקלות בשפות שאינן אנגלית, ושוב - המתרגם מאבד את ההקשר של המשפט השלם.
  3. המספר 2 אינו אוניברסלי לכל שפה, ואינו מופיע באותו המקום באותו השפה.
    מספרים ותאריכים - כדאי שיהיו חלק מהטקסט המתורגם.
תרגום לצרפתית של משפטים פשוטים, הכוללים מספרים. אולי לא מה שהייתם מצפים.

על כן, הדרך המומלצת היא להכיל את כל המשתנים בתוך הטקסט המתורגם:
  1. "Do you want to delete item {ITEM_ID} ?"  --> "{TRANSLATED_TEXT1}"
  2. "An error occurred with the cooling system" --> "{TRANSLATED_TEXT2}
  3. "There are {NUMBER} more steps for completion" --> "{TRANSLATED_TEXT3}" 
המתרגם עצמו ינהל היכן הביטוי ("ITEM_ID") נכנס במשפט, וחשוב לתת לו הקשר - באיזה סוג של מידע מדובר (מספר, תאריך, טקסט - ובאיזה צורה) וכו'.



ווב ו SEO

אם מדובר במערכת ווב, זה פשוט: וודאו שיש לכם תגיות lang ו charset תקינות.


סיכום


פווו.... זה היה פוסט קשה לכתיבה!
מרחתי את הכתיבה לאורך שבועיים, כל יום כתבתי איזו פסקה. החלק הקשה: לחפש דוגמאות, ולאמת אותן. להתעסק עם כל הסימנים המוזרים האלו. זה היה מתיש...

ייתכן ואקבל תגובות נוסח "בחרת את הנושא הכי משעמם ביקום, כאילו!". אבל עבורי, למרות הקושי, הנושא היה דווקא דיי מעניין. אני מניח שזה עניין של פתרון בעיות: אחרי שנתקלים בהן ומשקיעים בהן זמן - נושא הפתרון נעשה מעניין, ולא קשור באיזה תחום הוא.

כפי שכתבתי בהקדמה, ניסיתי לתת בסיס ידע משמעותי על נושא ה i18n - כזה שלפי דעתי אין היום בעברית (וקשה גם למצוא באנגלית). אני מקווה שזה יהיה שימושי.

שיהיה בהצלחה!



----

לינקים רלוונטיים

כיצד Etsy מציגה כסף (במדינות שונות בעולם) https://codeascraft.com/2016/04/19/how-etsy-formats-currency/

----

[א] פעם התקן של UTF-8 אפשר עד 6 בתים בכדי לתאר סימן, אך פישטו אותו למקסימים של 4 בתים בלבד.


12 תגובות:

  1. ליאור,

    פוסט יסודי ומעמיק!!
    אני בהלם שאני המגיב הראשון

    השבמחק
  2. פוסט פשוט מצוין... ממש לא "הנושא הכי משעמם" אלא נושא חשוב שהרבה מפתחים מזניחים את החשיבות שלו.

    השבמחק
  3. ליאור תודה, עברתי על צמד הפוסטים - כיסוי מקיף!
    אגב, יש נושא נוסף שראוי להתחשב בו, בעיקר ביישומים חלונאיים אך גם הרבה באתרי אינטרנט - אורך הטקסט המתורגם (כמות המילים ואותיות) יכול להיות שונה באופן קיצוני בין השפות.
    לפעמים מניחים שבאיזור מסוים יכנס כמות מסוימת של טקסט, או בתמונות גרפיות המכילות טקסט יש משקל כבד לאורך הטקסט..

    השבמחק
    תשובות
    1. נקודה טובה, צודק!

      הכלל שאני מכיר הוא להשאיר 30% מעבר לאורך שטקסט אנגלי תופס....
      טיפ: שפה "ארוכה" טיפוסית: בולגרית. אפשר להשתמש ב Google Translate כדי להפיק טקסט בשפה "ארוכה" או עם אותיות מיוחדות לצורך הבדיקה.

      תודה על ההערה.

      מחק
    2. אהבתי את השיטה לתרגם לבולגרית כדי להפיק טקסט בשפה "ארוכה" :) . . .

      מחק
  4. בחרת נושא מאוד מעניין. פוסט יפה מאוד.

    השבמחק
  5. ליאור,

    לא הכרתי את השיטה להכנסת תווי כיוון נסתרים לתוך הטקסט, זה פשוט ענק!!!
    אם עד היום לא יכלנו לרשום בתגובות טקסט עברי משולב אנגלי כי זה היה יוצא ככה: עם הנקודה פסיק משמאל
    int i = 0;
    כעת מוסיפים את התו הנסתר (באמצעות notepad) ואפשר להציג טקסט אנגלי משולב ‪int i = 0;
    ‪int i = 0;

    התגובה שרשמת לי כאן http://www.softwarearchiblog.com/2013/06/unified-theory-architecture-design-code.html?showComment=1372369077169#c8618123230610424696
    יכולה להיות כתובה באנגלית . . .

    השבמחק
  6. פוסט מרשים מאוד!

    אני רוצה להסביר כאן על र्कि, ה-grapheme (גרפמה) בתרשים המכיל דוגמאות ל-combining characters שעליה רשמת כי לא ברורה לך תהליך הווצרותה. העדפתי לפשט ולוותר מעט על הדיוק כדי להקל על ההבנה.

    מדובר בסימן ממערכת כתב המכונה "דוואנגרי" (https://en.wikipedia.org/wiki/Devanagari) המשמשת לכתיבת הינדית, שפת סנסקריט, ועוד.
    בכתב זה יש סימנים המייצגים
    תנועות: अ = a, इ = i, ए = e וכו'.
    עיצורים: בצורתם הבסיסית נהגים עם סיומת a. כך למשל: ब = ba, म = ma, त = ta, प = pa וכו'.
    וכמובן combining characters, שהם כפי שציינת מס' סימנים שהתאחדו לכדי סימן גרפי אחד. הליגטורות הללו מכונות גם conjuncts.

    כעת הסבר על הסימנים בתרשים:

    הסימן ् (U+094D) מכונה Virama (וגם Halant, Hal) ומייצג חוסר תנועה, כמו שווא נח. כאשר מצמידים אותו לסימנים בסיסיים המייצגים עיצורים הוא מייצג את הגיית העיצור בלבד. למשל –עיצור ba יהפוך להיות b, עיצור ma יהפוך ל- m.
    ב- combining characters משמיטים את הסימן הזה במרבית המקרים. לא צריך להיבהל מכך, גם אנחנו למשל נוהגים להשמיט את סימני הניקוד בכתב העברי (אם כי מסיבות אחרות).

    הסימן र הוא (U+0930) העיצור ra. כאשר סימן זה מצטרף כתחילית לפני הברה אחרת הוא הופך למעין קרס שמשורטט עליה (צורה זו מכונה reph).
    למשל र् r + म ma = र्म rma, בתרשים הבא:
    http://www.microsoft.com/typography/otfntdev/devanot/images/ind_reph1.gif

    הסימן क הוא (U+0915) העיצור ka.

    הסימן ि הוא (U+093F) התנועה i ב"צורת הצירוף" שלה עם עיצורים (צורת צירוף של תנועות נקראת בהינדית גם matra). למשל: मि mi =, कि = ki. היא נכתבת משמאל לעיצור למרות שבפועל היא נהגית אחריו.

    כעת, הסימן בתחתית र्कि הוא grapheme המבוטאת rki. כשמעתיקים את הסימן למסמך טקסט, ומנסים למחוק אותו, רואים איך הסימנים המרכיבים אותו נעלמים לפי סדר: קודם i, אח"כ ka, אח"כ virama, ולבסוף ra.

    השבמחק
    תשובות
    1. ניתן להשתמש באתר כמו http://unicodelookup.com/ ע"מ לראות את מרכיבי הסימן ואת ערך ה-unicode שלהם.

      מחק
    2. היי Zutot,

      וואהו! זו תגובה מעמיקה!! :)

      תודה רבה, אני מתרשם מהידע העמוק ומעריך את ההסבר המופרט!

      ליאור

      מחק
  7. תודה על המידע המעניין.

    השבמחק